Randomspawn (#1013)

add new cvar mp_randomspawn
improved search for the best angle
added mp_randomspawn 2 for debugging
added info_spawn_point entity to .fgd
This commit is contained in:
Vaqtincha 2025-03-28 03:17:02 +05:00 committed by GitHub
parent 316405b00d
commit dbec1b589c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 256 additions and 11 deletions

View File

@ -124,6 +124,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
| mp_vote_flags | km | 0 | - | Vote systems enabled in server.<br/>`0` voting disabled<br/>`k` votekick enabled via `vote` command<br/>`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. |
| bot_excellent_morale | 0 | 0 | 1 | Bots always have great morale regardless of defeat or victory. |
| mp_randomspawn | 0 | 0 | 1 | Random player spawns<br/>`0` disabled <br/>`1` enabled<br/>`NOTE`: Navigation `maps/.nav` file required |
</details>

9
dist/game.cfg vendored
View File

@ -627,3 +627,12 @@ mp_votemap_min_time "180"
//
// Default value: "0"
bot_excellent_morale "0"
// Random player spawns
// 0 - disabled (default behaviour)
// 1 - enabled
//
// NOTE: Navigation "maps/.nav" file required
//
// Default value: "0"
mp_randomspawn "0"

View File

@ -1144,6 +1144,166 @@ private:
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()
{

View File

@ -269,3 +269,6 @@ inline bool AreBotsAllowed()
}
void PrintAllEntities();
#ifdef REGAMEDLL_ADD
void GetSpawnPositions();
#endif

View File

@ -3834,8 +3834,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
}

View File

@ -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);
CVAR_REGISTER(&cv_bot_enable);
CVAR_REGISTER(&cv_hostage_ai_enable);

View File

@ -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

View File

@ -5338,17 +5338,24 @@ pt_end:
}
// 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;
@ -5373,17 +5380,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;
}
}
}
@ -5441,6 +5465,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)
{
@ -5468,6 +5510,7 @@ CTSpawn:
// The terrorist spawn points
else if (g_pGameRules->IsDeathmatch() && m_iTeam == TERRORIST)
{
TSpawn:
pSpot = g_pLastTerroristSpawn;
if (SelectSpawnSpot("info_player_deathmatch", pSpot))

View File

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

View File

@ -3143,3 +3143,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"
[
]

View File

@ -345,6 +345,25 @@ public:
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();