diff --git a/README.md b/README.md
index 90f58990d..d1a770cc7 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
| mp_ammo_respawn_time | 20 | 0.0 | - | The respawn time for ammunition. |
| mp_vote_flags | km | 0 | - | Vote systems enabled in server.
`0` voting disabled
`k` votekick enabled via `vote` command
`m` votemap enabled via `votemap` command |
| mp_votemap_min_time | 180 | 0.0 | - | Minimum seconds that must elapse on map before `votemap` command can be used. |
+| mp_randomspawn | 0 | 0 | 1 | Random player spawns
`0` disabled
`1` enabled
`NOTE`: Navigation `maps/.nav` file required |
diff --git a/dist/game.cfg b/dist/game.cfg
index c5dad01c9..85dfaa2c5 100644
--- a/dist/game.cfg
+++ b/dist/game.cfg
@@ -521,7 +521,7 @@ mp_give_c4_frags "3"
// Default value: "1.0"
mp_hostages_rescued_ratio "1.0"
-// Legacy func_vehicle behavior when blocked by another entity.
+// Legacy func_vehicle behavior when blocked by another entity.
// New one is more useful for playing multiplayer.
//
// 0 - New behavior
@@ -620,3 +620,12 @@ mp_vote_flags "km"
//
// Default value: "180"
mp_votemap_min_time "180"
+
+// Random player spawns
+// 0 - disabled (default behaviour)
+// 1 - enabled
+//
+// NOTE: Navigation "maps/.nav" file required
+//
+// Default value: "0"
+mp_randomspawn "0"
diff --git a/regamedll/dlls/bot/cs_bot_manager.cpp b/regamedll/dlls/bot/cs_bot_manager.cpp
index 730805bbf..6654b8efb 100644
--- a/regamedll/dlls/bot/cs_bot_manager.cpp
+++ b/regamedll/dlls/bot/cs_bot_manager.cpp
@@ -1146,6 +1146,166 @@ class CollectOverlappingAreas
CCSBotManager::Zone *m_zone;
};
+#ifdef REGAMEDLL_ADD
+LINK_ENTITY_TO_CLASS(info_spawn_point, CPointEntity, CCSPointEntity)
+
+inline bool IsFreeSpace(Vector vecOrigin, int iHullNumber, edict_t *pSkipEnt = nullptr)
+{
+ if (UTIL_PointContents(vecOrigin) != CONTENTS_EMPTY)
+ return false;
+
+ TraceResult trace;
+ UTIL_TraceHull(vecOrigin, vecOrigin, dont_ignore_monsters, iHullNumber, pSkipEnt, &trace);
+
+ return (!trace.fStartSolid && !trace.fAllSolid && trace.fInOpen);
+}
+
+inline bool pointInRadius(Vector vecOrigin, float radius)
+{
+ CBaseEntity *pEntity = nullptr;
+ while ((pEntity = UTIL_FindEntityInSphere(pEntity, vecOrigin, radius)))
+ {
+ if (FClassnameIs(pEntity->edict(), "info_spawn_point"))
+ return true;
+ }
+
+ return false;
+}
+
+// a simple algorithm that searches for the farthest point (so that the player does not look at the wall)
+inline Vector GetBestAngle(const Vector &vecStart)
+{
+ const float ANGLE_STEP = 30.0f;
+ float bestAngle = 0.0f;
+ float bestDistance = 0.0f;
+ Vector vecBestAngle = Vector(0, -1, 0);
+
+ for (float angleYaw = 0.0f; angleYaw <= 360.0f; angleYaw += ANGLE_STEP)
+ {
+ TraceResult tr;
+ UTIL_MakeVectors(Vector(0, angleYaw, 0));
+
+ Vector vecEnd(vecStart + gpGlobals->v_forward * 8192);
+ UTIL_TraceLine(vecStart, vecEnd, ignore_monsters, nullptr, &tr);
+
+ float distance = (vecStart - tr.vecEndPos).Length();
+
+ if (distance > bestDistance)
+ {
+ bestDistance = distance;
+ vecBestAngle.y = angleYaw;
+ }
+ }
+
+ return vecBestAngle;
+}
+
+// this function from leaked csgo sources 2020y
+inline bool IsValidArea(CNavArea *area)
+{
+ ShortestPathCost cost;
+ bool bNotOrphaned;
+
+ // check that we can path from the nav area to a ct spawner to confirm it isn't orphaned.
+ CBaseEntity *CTSpawn = UTIL_FindEntityByClassname(nullptr, "info_player_start");
+
+ if (CTSpawn)
+ {
+ CNavArea *CTSpawnArea = TheNavAreaGrid.GetNearestNavArea(&CTSpawn->pev->origin);
+ bNotOrphaned = NavAreaBuildPath(area, CTSpawnArea, nullptr, cost);
+
+ if (bNotOrphaned)
+ return true;
+ }
+
+ // double check that we can path from the nav area to a t spawner to confirm it isn't orphaned.
+ CBaseEntity *TSpawn = UTIL_FindEntityByClassname(nullptr, "info_player_deathmatch");
+
+ if (TSpawn)
+ {
+ CNavArea *TSpawnArea = TheNavAreaGrid.GetNearestNavArea(&TSpawn->pev->origin);
+ bNotOrphaned = NavAreaBuildPath(area, TSpawnArea, nullptr, cost);
+
+ if (bNotOrphaned)
+ return true;
+ }
+
+ return false;
+}
+
+void GetSpawnPositions()
+{
+ const float MIN_AREA_SIZE = 32.0f;
+ const int MAX_SPAWNS_POINTS = 128;
+ const float MAX_SLOPE = 0.85f;
+
+ int totalSpawns = 0;
+
+ for (NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); iter++)
+ {
+ if (totalSpawns >= MAX_SPAWNS_POINTS)
+ break;
+
+ CNavArea *area = *iter;
+
+ if (!area)
+ continue;
+
+ // ignore small areas
+ if (area->GetSizeX() < MIN_AREA_SIZE || area->GetSizeY() < MIN_AREA_SIZE)
+ continue;
+
+ // ignore areas jump, crouch etc
+ if (area->GetAttributes())
+ continue;
+
+ if (area->GetAreaSlope() < MAX_SLOPE)
+ {
+ //CONSOLE_ECHO("Skip area slope: %0.3f\n", area->GetAreaSlope());
+ continue;
+ }
+
+ Vector vecOrigin = *area->GetCenter() + Vector(0, 0, HalfHumanHeight + 5);
+
+ if (!IsFreeSpace(vecOrigin, human_hull))
+ {
+ //CONSOLE_ECHO("No free space!\n");
+ continue;
+ }
+
+ if (pointInRadius(vecOrigin, 128.0f))
+ continue;
+
+ if (!IsValidArea(area))
+ continue;
+
+ Vector bestAngle = GetBestAngle(vecOrigin);
+
+ if (bestAngle.y != -1)
+ {
+ CBaseEntity* pPoint = CBaseEntity::Create("info_spawn_point", vecOrigin, bestAngle, nullptr);
+
+ if (pPoint)
+ {
+ totalSpawns++;
+ //CONSOLE_ECHO("Add spawn at x:%f y:%f z:%f with angle %0.1f slope %0.3f \n", vecOrigin.x, vecOrigin.y, vecOrigin.z, bestAngle.y, area->GetAreaSlope());
+
+ // use only for debugging
+ if (randomspawn.value > 1.0f)
+ {
+ SET_MODEL(ENT(pPoint->pev), "models/player.mdl");
+ pPoint->pev->sequence = ACT_IDLE;
+ pPoint->pev->rendermode = kRenderTransAdd;
+ pPoint->pev->renderamt = 150.0f;
+ }
+ }
+ }
+ }
+
+ CONSOLE_ECHO("Total spawns points: %i\n", totalSpawns);
+}
+#endif
+
// Search the map entities to determine the game scenario and define important zones.
void CCSBotManager::ValidateMapData()
{
diff --git a/regamedll/dlls/bot/cs_bot_manager.h b/regamedll/dlls/bot/cs_bot_manager.h
index 9552c6978..0d5e6e37d 100644
--- a/regamedll/dlls/bot/cs_bot_manager.h
+++ b/regamedll/dlls/bot/cs_bot_manager.h
@@ -269,3 +269,6 @@ inline bool AreBotsAllowed()
}
void PrintAllEntities();
+#ifdef REGAMEDLL_ADD
+void GetSpawnPositions();
+#endif
\ No newline at end of file
diff --git a/regamedll/dlls/client.cpp b/regamedll/dlls/client.cpp
index bc223ed7d..6b855ed0d 100644
--- a/regamedll/dlls/client.cpp
+++ b/regamedll/dlls/client.cpp
@@ -3814,8 +3814,10 @@ void EXT_FUNC ServerActivate(edict_t *pEdictList, int edictCount, int clientMax)
#ifdef REGAMEDLL_ADD
CSGameRules()->ServerActivate();
- if (location_area_info.value)
- LoadNavigationMap();
+ if (LoadNavigationMap() == NAV_OK)
+ {
+ GetSpawnPositions();
+ }
#endif
}
diff --git a/regamedll/dlls/game.cpp b/regamedll/dlls/game.cpp
index 789435938..aeaa878a3 100644
--- a/regamedll/dlls/game.cpp
+++ b/regamedll/dlls/game.cpp
@@ -188,6 +188,8 @@ cvar_t ammo_respawn_time = { "mp_ammo_respawn_time", "20", FCVAR_SERVER, 2
cvar_t vote_flags = { "mp_vote_flags", "km", 0, 0.0f, nullptr };
cvar_t votemap_min_time = { "mp_votemap_min_time", "180", 0, 180.0f, nullptr };
+cvar_t randomspawn = { "mp_randomspawn", "0", FCVAR_SERVER, 0.0f, nullptr };
+
void GameDLL_Version_f()
{
if (Q_stricmp(CMD_ARGV(1), "version") != 0)
@@ -459,6 +461,7 @@ void EXT_FUNC GameDLLInit()
CVAR_REGISTER(&vote_flags);
CVAR_REGISTER(&votemap_min_time);
+ CVAR_REGISTER(&randomspawn);
// print version
CONSOLE_ECHO("ReGameDLL version: " APP_VERSION "\n");
diff --git a/regamedll/dlls/game.h b/regamedll/dlls/game.h
index 09da5305a..48cec02af 100644
--- a/regamedll/dlls/game.h
+++ b/regamedll/dlls/game.h
@@ -208,6 +208,7 @@ extern cvar_t weapon_respawn_time;
extern cvar_t ammo_respawn_time;
extern cvar_t vote_flags;
extern cvar_t votemap_min_time;
+extern cvar_t randomspawn;
#endif
diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp
index 9782921c1..35d43e0ef 100644
--- a/regamedll/dlls/player.cpp
+++ b/regamedll/dlls/player.cpp
@@ -5335,17 +5335,24 @@ void EXT_FUNC CBasePlayer::__API_HOOK(PostThink)()
}
// checks if the spot is clear of players
-BOOL IsSpawnPointValid(CBaseEntity *pPlayer, CBaseEntity *pSpot)
+BOOL IsSpawnPointValid(CBaseEntity *pPlayer, CBaseEntity *pSpot, float fRadius)
{
if (!pSpot->IsTriggered(pPlayer))
return FALSE;
CBaseEntity *pEntity = nullptr;
- while ((pEntity = UTIL_FindEntityInSphere(pEntity, pSpot->pev->origin, MAX_PLAYER_USE_RADIUS)))
+
+ while ((pEntity = UTIL_FindEntityInSphere(pEntity, pSpot->pev->origin, fRadius)))
{
// if ent is a client, don't spawn on 'em
- if (pEntity->IsPlayer() && pEntity != pPlayer)
+ if (pEntity->IsPlayer() && pEntity != pPlayer
+#ifdef REGAMEDLL_FIXES
+ && pEntity->IsAlive()
+#endif
+ )
+ {
return FALSE;
+ }
}
return TRUE;
@@ -5370,17 +5377,34 @@ bool CBasePlayer::SelectSpawnSpot(const char *pEntClassName, CBaseEntity *&pSpot
{
if (pSpot)
{
- // check if pSpot is valid
- if (IsSpawnPointValid(this, pSpot))
+#ifdef REGAMEDLL_ADD
+ if (FClassnameIs(pSpot->edict(), "info_spawn_point"))
{
- if (pSpot->pev->origin == Vector(0, 0, 0))
+ if (!IsSpawnPointValid(this, pSpot, 512.0f) || pSpot->pev->origin == Vector(0, 0, 0))
{
pSpot = UTIL_FindEntityByClassname(pSpot, pEntClassName);
continue;
}
+ else
+ {
+ return true;
+ }
+ }
+ else
+#endif
+ {
+ // check if pSpot is valid
+ if (IsSpawnPointValid(this, pSpot, MAX_PLAYER_USE_RADIUS))
+ {
+ if (pSpot->pev->origin == Vector(0, 0, 0))
+ {
+ pSpot = UTIL_FindEntityByClassname(pSpot, pEntClassName);
+ continue;
+ }
- // if so, go to pSpot
- return true;
+ // if so, go to pSpot
+ return true;
+ }
}
}
@@ -5438,6 +5462,24 @@ edict_t *EXT_FUNC CBasePlayer::__API_HOOK(EntSelectSpawnPoint)()
if (!FNullEnt(pSpot))
goto ReturnSpot;
}
+#ifdef REGAMEDLL_ADD
+ else if (randomspawn.value > 0)
+ {
+ pSpot = g_pLastSpawn;
+
+ if (SelectSpawnSpot("info_spawn_point", pSpot))
+ {
+ g_pLastSpawn = pSpot;
+
+ return pSpot->edict();
+ }
+
+ if (m_iTeam == CT)
+ goto CTSpawn;
+ else if (m_iTeam == TERRORIST)
+ goto TSpawn;
+ }
+#endif
// VIP spawn point
else if (g_pGameRules->IsDeathmatch() && m_bIsVIP)
{
@@ -5465,6 +5507,7 @@ edict_t *EXT_FUNC CBasePlayer::__API_HOOK(EntSelectSpawnPoint)()
// The terrorist spawn points
else if (g_pGameRules->IsDeathmatch() && m_iTeam == TERRORIST)
{
+TSpawn:
pSpot = g_pLastTerroristSpawn;
if (SelectSpawnSpot("info_player_deathmatch", pSpot))
diff --git a/regamedll/dlls/player.h b/regamedll/dlls/player.h
index b13c2c820..6890d30b8 100644
--- a/regamedll/dlls/player.h
+++ b/regamedll/dlls/player.h
@@ -1044,7 +1044,7 @@ int TrainSpeed(int iSpeed, int iMax);
void LogAttack(CBasePlayer *pAttacker, CBasePlayer *pVictim, int teamAttack, int healthHit, int armorHit, int newHealth, int newArmor, const char *killer_weapon_name);
bool CanSeeUseable(CBasePlayer *me, CBaseEntity *pEntity);
void FixPlayerCrouchStuck(edict_t *pPlayer);
-BOOL IsSpawnPointValid(CBaseEntity *pPlayer, CBaseEntity *pSpot);
+BOOL IsSpawnPointValid(CBaseEntity *pPlayer, CBaseEntity *pSpot, float fRadius);
CBaseEntity *FindEntityForward(CBaseEntity *pMe);
real_t GetPlayerPitch(const edict_t *pEdict);
real_t GetPlayerYaw(const edict_t *pEdict);
diff --git a/regamedll/extra/Toolkit/GameDefinitionFile/regamedll-cs.fgd b/regamedll/extra/Toolkit/GameDefinitionFile/regamedll-cs.fgd
index 79fe8333a..1e09872ed 100644
--- a/regamedll/extra/Toolkit/GameDefinitionFile/regamedll-cs.fgd
+++ b/regamedll/extra/Toolkit/GameDefinitionFile/regamedll-cs.fgd
@@ -3098,3 +3098,7 @@
@PointClass base(BaseCommand) size(-8 -8 -8, 8 8 8) = point_clientcommand : "It issues commands to the client console"
[
]
+
+@PointClass iconsprite("sprites/CS/info_player_start.spr") base(PlayerClass) = info_spawn_point : "Random spawn start"
+[
+]
\ No newline at end of file
diff --git a/regamedll/game_shared/bot/nav_area.h b/regamedll/game_shared/bot/nav_area.h
index c4cf8a1ff..10eca2f85 100644
--- a/regamedll/game_shared/bot/nav_area.h
+++ b/regamedll/game_shared/bot/nav_area.h
@@ -345,6 +345,25 @@ class CNavArea
void AddLadderUp(CNavLadder *ladder) { m_ladder[LADDER_UP].push_back(ladder); }
void AddLadderDown(CNavLadder *ladder) { m_ladder[LADDER_DOWN].push_back(ladder); }
+ inline float GetAreaSlope()
+ {
+ Vector u, v;
+
+ // compute our unit surface normal
+ u.x = m_extent.hi.x - m_extent.lo.x;
+ u.y = 0.0f;
+ u.z = m_neZ - m_extent.lo.z;
+
+ v.x = 0.0f;
+ v.y = m_extent.hi.y - m_extent.lo.y;
+ v.z = m_swZ - m_extent.lo.z;
+
+ Vector normal = CrossProduct(u, v);
+ normal.NormalizeInPlace();
+
+ return normal.z;
+ }
+
private:
friend void ConnectGeneratedAreas();
friend void MergeGeneratedAreas();