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();