From 6af77ac785b368741c1178b7a1feaaca3363e582 Mon Sep 17 00:00:00 2001 From: Sergey Shorokhov Date: Thu, 28 Mar 2024 19:43:14 +0300 Subject: [PATCH] Feature: New spawns style "random" (#106) * Add `rog.inc` - ROG is an include file which allows you to generate random valid origins around the map. It could be used to create random player spawns or in Battle Royale mods(PUBG/Fortnite) to spawn the floor loot/drop crates/llamas, etc. * redm_spawns: add `random` spawn preset * ConVar `redm_spawn_preset` hot change support * CvarChange_redm_spawn_preset: rework api usage * better ConVar desc --- .../configs/redm/gamemode_deathmatch.json | 5 +- .../ReDeathmatch/ReDM_spawn_manager.inc | 10 + .../addons/amxmodx/scripting/include/redm.inc | 5 +- .../addons/amxmodx/scripting/include/rog.inc | 361 ++++++++++++++++++ .../addons/amxmodx/scripting/redm_spawns.sma | 52 +++ 5 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 cstrike/addons/amxmodx/scripting/include/rog.inc diff --git a/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json b/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json index aeb5e2e..6a9d00b 100644 --- a/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json +++ b/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json @@ -57,7 +57,10 @@ // Minimum distance to the enemy to enable spawn checks. "redm_randomspawn_dist": "1500", - // Name of the spawn manager + /* Name of the spawn manager style. + `none`, + `preset`, + `random` - (beta) */ "redm_spawn_preset": "preset", // ReDM: Features diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc index a8b0192..d8b935a 100644 --- a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc @@ -20,12 +20,22 @@ SpawnManager_Init() { redm_spawn_preset, charsmax(redm_spawn_preset) ) + hook_cvar_change(get_cvar_pointer("redm_spawn_preset"), "CvarChange_redm_spawn_preset") RegisterHookChain(RG_CSGameRules_GetPlayerSpawnSpot, "CSGameRules_GetPlayerSpawnSpot", .post = false) redm_setstyle(redm_spawn_preset) } +public CvarChange_redm_spawn_preset(const cvar, const oldValue[], const value[]) { + new bool: result = redm_setstyle(value) + if (result) + return + + set_pcvar_string(cvar, oldValue) + LogMessageEx(Warning, "CvarChange_redm_spawn_preset: Spawn preset style `%s` not found!", value) +} + public CSGameRules_GetPlayerSpawnSpot(const player) { if (!IsActive()) return HC_CONTINUE diff --git a/cstrike/addons/amxmodx/scripting/include/redm.inc b/cstrike/addons/amxmodx/scripting/include/redm.inc index d92b3ce..0ea68bd 100644 --- a/cstrike/addons/amxmodx/scripting/include/redm.inc +++ b/cstrike/addons/amxmodx/scripting/include/redm.inc @@ -70,8 +70,11 @@ native redm_addstyle(const name[], const function[]) /** * Sets the current spawn style handler by name. * The handler registered to this name will be called after every spawn. + * + * @param name Style preset name. + * @return True if the style has been set, otherwise false. */ -native redm_setstyle(const name[]) +native bool: redm_setstyle(const name[]) /** * Returns the current style ID. diff --git a/cstrike/addons/amxmodx/scripting/include/rog.inc b/cstrike/addons/amxmodx/scripting/include/rog.inc new file mode 100644 index 0000000..a6f46ed --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/include/rog.inc @@ -0,0 +1,361 @@ +#if defined _rog_included +#endinput +#endif +#define _rog_included + +#include +#include + +const MAX_SEARCH_ITERATIONS = 100000 + +new const PrimaryDefaultMapEntities[][] = +{ + "func_bomb_target", + "func_hostage_rescue", + "info_player_start", + "info_player_deathmatch", + "func_escapezone", + "func_vip_safetyzone", + "info_vip_start" +} + +new const SecondaryDefaultMapEntities[][] = +{ + "info_bomb_target", + "info_hostage_rescue" +} + +new Array:DefaultMapEntitiesArray +new Array:FoundOriginsArray + +new CurrentPositionInArray + +/** + * Initializes the random origin generator and fills the arrays. + * + * @param MinDistance The minimum distance between points + * @param CheckFunction Allows you to set custom conditions for origins (optional) + * + * @noreturn + */ +stock ROGInitialize(Float:MinDistance, const CheckFunction[] = "") +{ + new i + if(DefaultMapEntitiesArray == Invalid_Array) + { + //We find the origin of default map entities and search around this places + DefaultMapEntitiesArray = ArrayCreate() + for(i = 0; i < sizeof PrimaryDefaultMapEntities; i++) + { + if(!AddEntityIfFound(PrimaryDefaultMapEntities[i])) + { + if(i < 2) + { + AddEntityIfFound(SecondaryDefaultMapEntities[i]) + } + } + } + } + + new Size = ArraySize(DefaultMapEntitiesArray) + if(Size) + { + new Float:EntityOrigin[3], EntityIndex + if(FoundOriginsArray == Invalid_Array) + { + FoundOriginsArray = ArrayCreate(3) + } + else + { + //Support for multiple calls for ROGInitialize + ArrayClear(FoundOriginsArray) + } + + for(i = 0; i < Size; i++) + { + EntityIndex = ArrayGetCell(DefaultMapEntitiesArray, i) + new class[32] + pev(EntityIndex, pev_classname, class, 31) + + if(class[0] == 'i') + { + pev(EntityIndex, pev_origin, EntityOrigin) + } + else + { + fm_get_brush_entity_origin(EntityIndex, EntityOrigin) + } + + SearchForOrigins(EntityOrigin, 5000.0, CheckFunction, Float:MinDistance) + } + } +} + +/** + * Retrieves a random origin. + * + * @note Once all origins are used it will start over, meaning it will never run out of points. + * @note Gets the points one by one from the array. + * + * @param Origin Origin array + * + * @noreturn + */ +stock ROGGetOrigin(Float:Origin[3]) +{ + if(CurrentPositionInArray >= ArraySize(FoundOriginsArray)) + { + CurrentPositionInArray = 0 + } + + ArrayGetArray(FoundOriginsArray, CurrentPositionInArray, Origin) + CurrentPositionInArray++; +} + +/** + * Retrieves a random origin. + * + * @note Gets the points from the array in random order + * + * @param Origin Origin array + * + * @noreturn + */ +stock ROGGetRandomOrigin(Float:Origin[3]) +{ + ArrayGetArray(FoundOriginsArray, random_num(0, ArraySize(FoundOriginsArray) - 1), Origin) +} + +/** + * Randomizes the origin array. + * + * @noreturn + */ +stock ROGShuffleOrigins() +{ + new Size = ArraySize(FoundOriginsArray), j + for (new i = Size - 1; i > 0; i--) + { + j = random_num(0, i) + ArraySwap(FoundOriginsArray, i, j) + } +} + +/** + * Shows all generated origins and their count. + * + * @noreturn + */ +stock ROGDumpOriginData() +{ + new Float:EntityOrigin[3], i + new Size = ArraySize(FoundOriginsArray) + + for(i = 0; i < Size; i++) + { + ArrayGetArray(FoundOriginsArray, i, EntityOrigin) + server_print("[%d] %f %f %f", i, EntityOrigin[0], EntityOrigin[1], EntityOrigin[2]) + } + + server_print("Generated %d random origins", i) +} + +/** + * Returns the total amount of random origins in the array. + * + * @return Total number of random origins in the array + */ +stock ROGGetOriginsNum() +{ + return ArraySize(FoundOriginsArray); +} + +stock AddEntityIfFound(const ClassName[]) +{ + new Entity = fm_find_ent_by_class(MaxClients, ClassName) + if(pev_valid(Entity)) + { + ArrayPushCell(DefaultMapEntitiesArray, Entity) + return 1 + } + + return 0 +} + +stock SearchForOrigins(Float:ReferenceOrigin[3], Float:Radius, const CheckFunction[], Float:MinDistance) +{ + new Float:RandomOrigin[3], Float:SkyOrigin[3], Float:FloorOrigin[3], Float:PlaneNormal[3], Float:BackupOrigin[3], j + new CallState, CallReturnValue + + new Array:test = ArrayCreate(3) + + for(new i = 1; i <= MAX_SEARCH_ITERATIONS; i++) + { + RandomOrigin = ReferenceOrigin + for(j = 0; j < 3; j++) + { + //Get a random origin starting from a reference point + RandomOrigin[j] += random_float((-1) * Radius, Radius) + } + + //Detect the floor, so we don't spawn the player in air + FloorOrigin[0] = RandomOrigin[0] + FloorOrigin[1] = RandomOrigin[1] + FloorOrigin[2] = -8192.0 + + BackupOrigin = RandomOrigin + + engfunc(EngFunc_TraceLine, RandomOrigin, FloorOrigin, DONT_IGNORE_MONSTERS, 0, 0) + get_tr2(0, TR_vecEndPos, RandomOrigin) + + get_tr2(0, TR_vecPlaneNormal, PlaneNormal) + if(PlaneNormal[2] < 0.7) + { + //ground is too steep, player will start sliding down most likely + //also if it's that steep players shouldn't be able to climb it, could be an unreachable spot + //0.7 seems to be used in the bot navigation code from regamedll + continue + } + + engfunc(EngFunc_TraceHull, BackupOrigin, RandomOrigin, IGNORE_MONSTERS, HULL_HUMAN, 0, 0) + new HitEntity = get_tr2(0, TR_pHit) + if(pev_valid(HitEntity)) + { + continue + } + + RandomOrigin[2] += 38.0 //make sure we don't spawn in ground + + if(fm_point_contents(RandomOrigin) == CONTENTS_EMPTY) + { + if(ValidSpotFound(RandomOrigin)) + { + //Find where the sky/roof is + SkyOrigin[0] = RandomOrigin[0] + SkyOrigin[1] = RandomOrigin[1] + SkyOrigin[2] = 8192.0 + + engfunc(EngFunc_TraceLine, RandomOrigin, SkyOrigin, DONT_IGNORE_MONSTERS, 0, 0) + get_tr2(0, TR_vecEndPos, SkyOrigin) + + if(fm_point_contents(SkyOrigin) == CONTENTS_SKY) + { + if(get_distance_f(RandomOrigin, SkyOrigin) < 250) + { + //On maps like de_dust2 players could spawn on the map texture + //All this points are less than 249 units from the sky + //By detecting where the sky is and checking the distance we avoid this scenario + continue + } + } + + if(CheckFunction[0] != EOS) + { + CallState = callfunc_begin(CheckFunction) + if(CallState == 1) + { + callfunc_push_array(_:RandomOrigin, sizeof(RandomOrigin)) + CallReturnValue = callfunc_end() + if(!CallReturnValue) + { + continue + } + } + } + + //Don't add the origin if it's too close to another origin + new OriginsCount = ArraySize(FoundOriginsArray) + new Float:OriginToCompare[3] + new bool:IsTooClose + new Float:ClosestOrigin[3], Float:MinimumDistance = 9999.9 + + for(new j = 0; j < OriginsCount; j++) + { + ArrayGetArray(FoundOriginsArray, j, OriginToCompare) + + new Float:Distance = get_distance_f(RandomOrigin, OriginToCompare) + if(Distance < MinDistance) + { + IsTooClose = true + break + } + + if(Distance < MinimumDistance) + { + MinimumDistance = Distance + ClosestOrigin = OriginToCompare + } + + } + + if(!IsTooClose) + { + if(MinimumDistance == 9999.9) + { + ClosestOrigin = ReferenceOrigin + } + + if(CheckPointsForVisibility(RandomOrigin, ClosestOrigin)) + { + ArrayPushArray(test, RandomOrigin) + ArrayPushArray(FoundOriginsArray, RandomOrigin) + } + } + } + } + } + + ArrayDestroy(test) +} + +stock bool:ValidSpotFound(Float:Origin[3]) +{ + new HandleTraceHull + engfunc(EngFunc_TraceHull, Origin, Origin, DONT_IGNORE_MONSTERS, HULL_HUMAN, 0, HandleTraceHull) + if(get_tr2(HandleTraceHull, TR_InOpen) && !(get_tr2(HandleTraceHull, TR_StartSolid) || get_tr2(HandleTraceHull, TR_AllSolid))) + { + return true + } + + return false +} + +CheckPointsForVisibility(Float:RandomOrigin[3], Float:ReferenceOrigin[3]) +{ + new Float:CopyReferenceOrigin[3]; CopyReferenceOrigin = ReferenceOrigin + new Float:CopyRandomOrigin[3]; CopyRandomOrigin = RandomOrigin + new Float:InitialRandomOrigin[3], Float:InitialReferenceOrigin[3] + InitialRandomOrigin = CopyRandomOrigin + InitialReferenceOrigin = CopyReferenceOrigin + + new Float:Fraction, Float:TraceEndOrigin[3] + for(new i = 0; i < 7; i++) + { + CopyRandomOrigin[2] = CopyRandomOrigin[2] + 100.0 + CopyReferenceOrigin[2] = CopyReferenceOrigin[2] + 100.0 + + engfunc(EngFunc_TraceLine, InitialRandomOrigin, CopyRandomOrigin, IGNORE_GLASS, -1, 0) + get_tr2(0, TR_vecEndPos, TraceEndOrigin) + if(fm_point_contents(TraceEndOrigin) == CONTENTS_SKY) + { + break + } + + engfunc(EngFunc_TraceLine, InitialReferenceOrigin, CopyReferenceOrigin, IGNORE_GLASS, -1, 0) + get_tr2(0, TR_vecEndPos, TraceEndOrigin) + if(fm_point_contents(TraceEndOrigin) == CONTENTS_SKY) + { + break + } + + engfunc(EngFunc_TraceLine, CopyRandomOrigin, CopyReferenceOrigin, IGNORE_GLASS, -1, 0) + get_tr2(0, TR_flFraction, Fraction) + + if(Fraction == 1.0) + { + return true + } + } + + return false +} \ No newline at end of file diff --git a/cstrike/addons/amxmodx/scripting/redm_spawns.sma b/cstrike/addons/amxmodx/scripting/redm_spawns.sma index a3d7f55..a04eeca 100644 --- a/cstrike/addons/amxmodx/scripting/redm_spawns.sma +++ b/cstrike/addons/amxmodx/scripting/redm_spawns.sma @@ -6,6 +6,7 @@ #include #include +#include static MENU_FLAG = ADMIN_MAP @@ -76,6 +77,9 @@ public plugin_init() { register_clcmd("enter_spawnGroup", "ClCmd_EnterSpawnGroup") redm_addstyle("preset", "SpawnPreset_DefaultPreset") + redm_addstyle("random", "SpawnPreset_Random") + + ROGInitialize(.MinDistance = 150.0) } public plugin_cfg() { @@ -874,6 +878,54 @@ static GameDLLSpawnsCountFix() { set_member_game(m_iSpawnPointCount_Terrorist, 32) } +public bool: SpawnPreset_Random(const player) { + new attempts + const maxAttempts = 15 + +startSearch: + new Float: origin[3] + ROGGetOrigin(origin) + + new Float: bestvAngle[3] + + // TODO: mb ConVar ? + const Float: searchPrecision = 8.0 + for (new Float: addAngle = -90.0, Float: bestFraction; addAngle < 270.0; addAngle += (360.0 / searchPrecision)) { + new Float: angle[3] + angle[1] = addAngle + + engfunc(EngFunc_MakeVectors, angle) + global_get(glb_v_forward, angle) + xs_vec_mul_scalar(angle, 9999.0, angle) + + new Float: endOrigin[3] + xs_vec_add(origin, angle, endOrigin) + + engfunc(EngFunc_TraceLine, origin, endOrigin, IGNORE_MONSTERS, player, 0) + new Float: fraction + get_tr2(0, TR_flFraction, fraction) + + if (fraction > bestFraction) { + bestFraction = fraction + bestvAngle[1] = addAngle + } + } + + new bool: res = Spawn_EntitySetPosition(player, origin, bestvAngle, bestvAngle) + if (!res) { + if (attempts++ < maxAttempts) + goto startSearch + + LogMessageEx(Debug, "Player %n can't use a random spawns. (Max attempts reached!)", + player + ) + + return false + } + + return true +} + public bool: SpawnPreset_DefaultPreset(const player) { if (g_arrSpawns == Invalid_JSON) return false