// // YaPB, based on PODBot by Markus Klinge ("CountFloyd"). // Copyright © YaPB Project Developers . // // SPDX-License-Identifier: MIT // #include ConVar cv_csdm_mode ("csdm_mode", "0", "Enables or disables CSDM / FFA mode for bots.\nAllowed values: '0', '1', '2', '3'.\nIf '0', CSDM / FFA mode is auto-detected.\nIf '1', CSDM mode is enabled, but FFA is disabled.\nIf '2', CSDM and FFA mode is enabled.\nIf '3', CSDM and FFA mode is disabled.", true, 0.0f, 3.0f); ConVar cv_ignore_map_prefix_game_mode ("ignore_map_prefix_game_mode", "0", "If enabled, bots will not apply game modes based on map name prefix (fy_ and ka_ specifically)."); ConVar cv_threadpool_workers ("threadpool_workers", "-1", "Maximum number of threads the bot will run to process some tasks. -1 means half of the CPU cores are used.", true, -1.0f, static_cast (plat.hardwareConcurrency ())); ConVar cv_grenadier_mode ("grenadier_mode", "0", "If enabled, bots will not apply throwing conditions on grenades."); ConVar cv_ignore_enemies_after_spawn_time ("ignore_enemies_after_spawn_time", "0", "Makes bots ignore enemies for a specified time in seconds on a new round. Useful for Zombie Plague mods.", false); ConVar cv_breakable_health_limit ("breakable_health_limit", "500.0", "Specifies the maximum health of a breakable object that the bot will consider destroying.", true, 1.0f, 3000.0); ConVar sv_skycolor_r ("sv_skycolor_r", nullptr, Var::GameRef); ConVar sv_skycolor_g ("sv_skycolor_g", nullptr, Var::GameRef); ConVar sv_skycolor_b ("sv_skycolor_b", nullptr, Var::GameRef); Game::Game () { m_startEntity = nullptr; m_localEntity = nullptr; m_precached = false; m_gameFlags = 0; m_mapFlags = 0; m_oneSecondFrame = 0.0f; m_halfSecondFrame = 0.0f; m_cvars.clear (); } void Game::precache () { if (m_precached) { return; } m_precached = true; m_drawModels[DrawLine::Simple] = m_engineWrap.precacheModel ("sprites/laserbeam.spr"); m_drawModels[DrawLine::Arrow] = m_engineWrap.precacheModel ("sprites/arrow1.spr"); m_engineWrap.precacheSound ("weapons/xbow_hit1.wav"); // node add m_engineWrap.precacheSound ("weapons/mine_activate.wav"); // node delete m_engineWrap.precacheSound ("common/wpn_hudon.wav"); // path add/delete done m_mapFlags = 0; // reset map type as worldspawn is the first entity spawned registerCvars (true); } void Game::levelInitialize (edict_t *entities, int max) { // this function precaches needed models and initialize class variables // enable command handling ctrl.setDenyCommands (false); // re-initialize bot's array bots.destroy (); // startup threaded worker worker.startup (cv_threadpool_workers.as ()); // clear all breakables before initialization m_breakables.clear (); m_checkedBreakables.clear (); // initialize all config files conf.loadConfigs (); // update worldmodel illum.resetWorldModel (); // execute main config conf.loadMainConfig (); // ensure the server admin is confident about features he's using ensureHealthyGameEnvironment (); // load map-specific config conf.loadMapSpecificConfig (); // do level initialization stuff here... graph.loadGraphData (); // initialize quota management bots.initQuota (); // install the sendto hook to fake queries fakequeries.init (); // flush any print queue ctrl.resetFlushTimestamp (); // set the global timer function timerStorage.setTimeAddress (&globals->time); // restart the fakeping timer, so it'll start working after mapchange fakeping.restartTimer (); // go thru the all entities on map, and do whatever we're want for (int i = 0; i < max; ++i) { auto ent = entities + i; // only valid entities if (!ent || ent->v.classname == 0) { continue; } auto classname = ent->v.classname.str (); if (classname == "worldspawn") { m_startEntity = ent; } else if (classname == "player_weaponstrip") { if (is (GameFlags::Legacy) && strings.isEmpty (ent->v.target.chars ())) { ent->v.target = ent->v.targetname = engfuncs.pfnAllocString ("fake"); } else if (!is (GameFlags::ReGameDLL)) { engfuncs.pfnRemoveEntity (ent); } } else if (classname == "info_player_start" || classname == "info_vip_start") { ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; } else if (classname == "info_player_deathmatch") { ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; } else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { m_mapFlags |= MapFlags::Assassination; // assassination map } else if (isHostageEntity (ent)) { m_mapFlags |= MapFlags::HostageRescue; // rescue map } else if (classname == "func_bomb_target" || classname == "info_bomb_target") { m_mapFlags |= MapFlags::Demolition; // defusion map } else if (classname == "func_escapezone") { m_mapFlags |= MapFlags::Escape; // strange thing on some ES maps, where hostage entity present there if (m_mapFlags & MapFlags::HostageRescue) { m_mapFlags &= ~MapFlags::HostageRescue; } } else if (isDoorEntity (ent)) { m_mapFlags |= MapFlags::HasDoors; } else if (classname.startsWith ("func_button")) { m_mapFlags |= MapFlags::HasButtons; } else if (isBreakableEntity (ent, true)) { // add breakable for material check m_checkedBreakables[indexOfEntity (ent)] = ent->v.impulse <= 0; m_breakables.push (ent); } } // next maps doesn't have map-specific entities, so determine it by name if (!cv_ignore_map_prefix_game_mode) { StringRef prefix = getMapName (); if (prefix.startsWith ("fy_")) { m_mapFlags |= MapFlags::FightYard; } else if (prefix.startsWith ("ka_")) { m_mapFlags |= MapFlags::KnifeArena; } else if (prefix.startsWith ("he_")) { m_mapFlags |= MapFlags::GrenadeWar; } } // reset some timers m_oneSecondFrame = 0.0f; m_halfSecondFrame = 0.0f; } void Game::onSpawnEntity (edict_t *ent) { constexpr auto kEntityInfoPlayerStart = StringRef::fnv1a32 ("info_player_start"); constexpr auto kEntityInfoVIPStart = StringRef::fnv1a32 ("info_vip_start"); constexpr auto kEntityInfoPlayerDeathmatch = StringRef::fnv1a32 ("info_player_deathmatch"); if (game.isNullEntity (ent) || ent->v.classname == 0) { return; } const auto classNameHash = ent->v.classname.str ().hash (); if (classNameHash == kEntityInfoPlayerStart || classNameHash == kEntityInfoVIPStart) { ++m_spawnCount[Team::CT]; } else if (classNameHash == kEntityInfoPlayerDeathmatch) { ++m_spawnCount[Team::Terrorist]; } } void Game::levelShutdown () { // save collected practice on shutdown practice.save (); // stop thread pool worker.shutdown (); // destroy global killer entity bots.destroyKillerEntity (); // ensure players are off on xash3d if (is (GameFlags::Xash3DLegacy)) { bots.kickEveryone (true, false); } // set state to unprecached setUnprecached (); // enable lightstyle animations on level change illum.enableAnimation (true); // send message on new map util.setNeedForWelcome (false); // clear local entity setLocalEntity (nullptr); // reset graph state graph.reset (); // suspend any analyzer tasks analyzer.suspend (); // disable command handling ctrl.setDenyCommands (true); // reset spawn counts for (auto &sc : m_spawnCount) { sc = 0; } } void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) const { // this function draws a arrow visible from the client side of the player whose player entity // is pointed to by ent, from the vector location start to the vector location end, // which is supposed to last life tenths seconds, and having the color defined by RGB. if (!isPlayerEntity (ent)) { return; // reliability check } MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent) .writeByte (TE_BEAMPOINTS) .writeCoord (end.x) .writeCoord (end.y) .writeCoord (end.z) .writeCoord (start.x) .writeCoord (start.y) .writeCoord (start.z) .writeShort (m_drawModels[type]) .writeByte (0) // framestart .writeByte (10) // framerate .writeByte (life) // life in 0.1's .writeByte (width) // width .writeByte (noise) // noise .writeByte (color.red) // r, g, b .writeByte (color.green) // r, g, b .writeByte (color.blue) // r, g, b .writeByte (brightness) // brightness .writeByte (speed); // speed } void Game::testModel (const Vector &start, const Vector &end, int hullNumber, edict_t *entToHit, TraceResult *ptr) { engfuncs.pfnTraceModel (start, end, hullNumber, entToHit, ptr); } void Game::testLine (const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult *ptr) { // this function traces a line dot by dot, starting from vecStart in the direction of vecEnd, // ignoring or not monsters (depending on the value of IGNORE_MONSTERS, true or false), and stops // at the first obstacle encountered, returning the results of the trace in the TraceResult structure // ptr. Such results are (amongst others) the distance traced, the hit surface, the hit plane // vector normal, etc. See the TraceResult structure for details. This function allows to specify // whether the trace starts "inside" an entity's polygonal model, and if so, to specify that entity // in ignoreEntity in order to ignore it as a possible obstacle. auto engineFlags = 0; if (ignoreFlags & TraceIgnore::Monsters) { engineFlags = 1; } if (ignoreFlags & TraceIgnore::Glass) { engineFlags |= 0x100; } engfuncs.pfnTraceLine (start, end, engineFlags, ignoreEntity, ptr); } void Game::testHull (const Vector &start, const Vector &end, int ignoreFlags, int hullNumber, edict_t *ignoreEntity, TraceResult *ptr) { // this function traces a hull dot by dot, starting from vecStart in the direction of vecEnd, // ignoring or not monsters (depending on the value of IGNORE_MONSTERS, true or // false), and stops at the first obstacle encountered, returning the results // of the trace in the TraceResult structure ptr, just like TraceLine. Hulls that can be traced // (by parameter hull_type) are point_hull (a line), head_hull (size of a crouching player), // human_hull (a normal body size) and large_hull (for monsters?). Not all the hulls in the // game can be traced here, this function is just useful to give a relative idea of spatial // reachability (i.e. can a hostage pass through that tiny hole ?) Also like TraceLine, this // function allows to specify whether the trace starts "inside" an entity's polygonal model, // and if so, to specify that entity in ignoreEntity in order to ignore it as an obstacle. engfuncs.pfnTraceHull (start, end, !!(ignoreFlags & TraceIgnore::Monsters), hullNumber, ignoreEntity, ptr); } bool Game::isDedicated () { // return true if server is dedicated server, false otherwise static const bool dedicated = engfuncs.pfnIsDedicatedServer () > 0; return dedicated; } const char *Game::getRunningModName () { // this function returns mod name without path static String name {}; if (!name.empty ()) { return name.chars (); } char engineModName[StringBuffer::StaticBufferSize] {}; engfuncs.pfnGetGameDir (engineModName); name = engineModName; size_t slash = name.findLastOf ("\\/"); if (slash != String::InvalidIndex) { name = name.substr (slash + 1); } name = name.trim (" \\/"); return name.chars (); } const char *Game::getMapName () { // this function gets the map name and store it in the map_name global string variable. return strings.format ("%s", globals->mapname.chars ()); } Vector Game::getEntityOrigin (edict_t *ent) { // this expanded function returns the vector origin of a bounded entity, assuming that any // entity that has a bounding box has its center at the center of the bounding box itself. if (isNullEntity (ent)) { return nullptr; } if (ent->v.origin.empty ()) { return ent->v.absmin + ent->v.size * 0.5; } return ent->v.origin; } void Game::registerEngineCommand (const char *command, void func ()) { // this function tells the engine that a new server command is being declared, in addition // to the standard ones, whose name is command_name. The engine is thus supposed to be aware // that for every "command_name" server command it receives, it should call the function // pointed to by "function" in order to handle it. // check for hl pre 1.1.0.4, as it's doesn't have pfnAddServerCommand and many more stuff we need to work if (!plat.isValidPtr (engfuncs.pfnAddServerCommand)) { logger.fatal ("%s's minimum HL engine version is 1.1.0.4 and minimum Counter-Strike is Beta 6.5. Please update your engine / game version.", product.name); } else { engfuncs.pfnAddServerCommand (command, func); } } void Game::playSound (edict_t *ent, const char *sound) { if (isNullEntity (ent)) { return; } engfuncs.pfnEmitSound (ent, CHAN_WEAPON, sound, 1.0f, ATTN_NORM, 0, 100); } void Game::setPlayerStartDrawModels () { static HashMap models { { "info_player_start", "models/player/urban/urban.mdl" }, { "info_player_deathmatch", "models/player/terror/terror.mdl" }, { "info_vip_start", "models/player/vip/vip.mdl" } }; models.foreach ([&] (const String &key, const String &val) { searchEntities ("classname", key, [&] (edict_t *ent) { m_engineWrap.setModel (ent, val.chars ()); return EntitySearchResult::Continue; }); }); } bool Game::checkVisibility (edict_t *ent, uint8_t *set) { if (!set) { return true; } if (ent->headnode < 0) { for (int i = 0; i < ent->num_leafs; ++i) { const auto leaf = ent->leafnums[i]; if (set[leaf >> 3] & cr::bit (leaf & 7)) { return true; } } return false; } for (int i = 0; i < MAX_ENT_LEAFS; ++i) { const auto leaf = ent->leafnums[i]; if (leaf == -1) { break; } if (set[leaf >> 3] & cr::bit (leaf & 7)) { return true; } } return engfuncs.pfnCheckVisibility (ent, set) > 0; } uint8_t *Game::getVisibilitySet (Bot *bot, bool pvs) const { if (is (GameFlags::Xash3DLegacy)) { return nullptr; } auto eyes = bot->getEyesPos (); if (bot->isDucking ()) { eyes += VEC_HULL_MIN - VEC_DUCK_HULL_MIN; } return pvs ? engfuncs.pfnSetFatPVS (eyes) : engfuncs.pfnSetFatPAS (eyes); } void Game::sendClientMessage (bool console, edict_t *ent, StringRef message) { // helper to sending the client message // do not send messages to fake clients if (!isPlayerEntity (ent) || isFakeClientEntity (ent)) { return; } // if console message and destination is listenserver entity, just print via server message instead of through unreliable channel if (console && ent == getLocalEntity ()) { sendServerMessage (message); return; } const String &buffer = message; // used to split messages auto sendTextMsg = [&console, &ent] (StringRef text) { MessageWriter (MSG_ONE_UNRELIABLE, msgs.id (NetMsg::TextMsg), nullptr, ent) .writeByte (console ? HUD_PRINTCONSOLE : HUD_PRINTCENTER) .writeString (text.chars ()); }; // do not excess limit constexpr size_t kMaxSendLength = 125; // split up the string into chunks if needed (maybe check if it's multibyte?) if (buffer.length () > kMaxSendLength) { auto chunks = buffer.split (kMaxSendLength); // send in chunks for (size_t i = 0; i < chunks.length (); ++i) { sendTextMsg (chunks[i]); } return; } sendTextMsg (buffer); } void Game::sendServerMessage (StringRef message) { // helper to sending the client message // do not excess limit constexpr size_t kMaxSendLength = 175; // split up the string into chunks if needed (maybe check if it's multibyte?) if (message.length () > kMaxSendLength) { auto chunks = message.split (kMaxSendLength); // send in chunks for (size_t i = 0; i < chunks.length (); ++i) { engfuncs.pfnServerPrint (chunks[i].chars ()); } return; } engfuncs.pfnServerPrint (message.chars ()); } void Game::sendHudMessage (edict_t *ent, const hudtextparms_t &htp, StringRef message) { constexpr size_t kMaxSendLength = 512; if (isNullEntity (ent)) { return; } MessageWriter msg (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent); msg.writeByte (TE_TEXTMESSAGE); msg.writeByte (htp.channel & 0xff); msg.writeShort (MessageWriter::fs16 (htp.x, 13.0f)); msg.writeShort (MessageWriter::fs16 (htp.y, 13.0f)); msg.writeByte (htp.effect); msg.writeByte (htp.r1); msg.writeByte (htp.g1); msg.writeByte (htp.b1); msg.writeByte (htp.a1); msg.writeByte (htp.r2); msg.writeByte (htp.g2); msg.writeByte (htp.b2); msg.writeByte (htp.a2); msg.writeShort (MessageWriter::fu16 (htp.fadeinTime, 8.0f)); msg.writeShort (MessageWriter::fu16 (htp.fadeoutTime, 8.0f)); msg.writeShort (MessageWriter::fu16 (htp.holdTime, 8.0f)); if (htp.effect == 2) { msg.writeShort (MessageWriter::fu16 (htp.fxTime, 8.0f)); } msg.writeString (message.substr (0, kMaxSendLength).chars ()); } void Game::prepareBotArgs (edict_t *ent, String str) { // the purpose of this function is to provide fakeclients (bots) with the same client // command-scripting advantages (putting multiple commands in one line between semicolons) // as real players. It is an improved version of botman's FakeClientCommand, in which you // supply directly the whole string as if you were typing it in the bot's "console". It // is supposed to work exactly like the pfnClientCommand (server-sided client command). m_botArgs.clear (); // always clear args if (str.empty ()) { return; } // helper to parse single (not multi) command auto parsePartArgs = [&] (String &args) { args.trim ("\r\n\t\" "); // trim new lines // we're have empty commands? if (args.empty ()) { return; } // find first space const size_t space = args.find (' ', 0); // if found space if (space != String::InvalidIndex) { const auto quote = space + 1; // check for quote next to space // check if we're got a quoted string if (quote < args.length () && args[quote] == '\"') { m_botArgs.emplace (args.substr (0, space)); // add command m_botArgs.emplace (args.substr (quote, args.length () - 1).trim ("\"")); // add string with trimmed quotes } else { for (auto &&arg : args.split (" ")) { m_botArgs.emplace (arg); } } } else { m_botArgs.emplace (args); // move all the part to args } MDLL_ClientCommand (ent); // clear space for next cmd m_botArgs.clear (); }; if (str.find (';', 0) != String::InvalidIndex) { for (auto &&part : str.split (";")) { parsePartArgs (part.trim ()); } } else { parsePartArgs (str); } } bool Game::isSoftwareRenderer () { // xash always use "hw" structures if (is (GameFlags::Xash3D)) { return false; } // dedicated server (except xash) always use "sw" structures if (isDedicated ()) { return true; } auto model = illum.getWorldModel (); if (model->nodes[0].parent != nullptr) { return false; } const auto child = model->nodes[0].children[0]; if (child < model->nodes || child > model->nodes + model->numnodes) { return false; } if (child->parent != &model->nodes[0]) { return false; } // and on only windows version you can use software-render game. Linux, macOS always defaults to OpenGL if (plat.win) { return plat.hasModule ("sw"); } return false; } bool Game::is25thAnniversaryUpdate () { static ConVarRef sv_use_steam_networking ("sv_use_steam_networking"); static ConVarRef host_hl25_extended_structs ("host_hl25_extended_structs"); return sv_use_steam_networking.exists () || host_hl25_extended_structs.value () > 0.0f; } void Game::pushConVar (StringRef name, StringRef value, StringRef info, bool bounded, float min, float max, int32_t varType, bool missingAction, StringRef regval, ConVar *self) { // this function adds globally defined variable to registration stack ConVarReg reg {}; reg.reg.name = name.chars (); reg.reg.string = value.chars (); reg.name = name; reg.missing = missingAction; reg.init = value; reg.info = info; reg.bounded = bounded; if (!regval.empty ()) { reg.regval = regval; } if (bounded) { reg.min = min; reg.max = max; reg.initial = value.as (); } int eflags = FCVAR_EXTDLL; if (varType == Var::Normal) { eflags |= FCVAR_SERVER; } else if (varType == Var::ReadOnly) { eflags |= FCVAR_SERVER | FCVAR_SPONLY | FCVAR_PRINTABLEONLY; } else if (varType == Var::Password) { eflags |= FCVAR_PROTECTED; } reg.reg.flags = eflags; reg.self = self; reg.type = varType; m_cvars.push (cr::move (reg)); } void ConVar::revert () { if (!ptr) { return; } const auto &cvars = game.getCvars (); for (const auto &var : cvars) { if (var.name == ptr->name) { set (var.init.chars ()); break; } } } void ConVar::setPrefix (StringRef name, int32_t type) { if (type == Var::GameRef) { name_ = name; return; } name_.assignf ("%s_%s", product.cmdPri, name); } void Game::checkCvarsBounds () { for (const auto &var : m_cvars) { if (!var.self || !var.self->ptr) { continue; } // read only cvar is not changeable if (var.type == Var::ReadOnly && !var.init.empty ()) { if (var.init != var.self->as ()) { var.self->set (var.init.chars ()); } continue; } if (!var.bounded || !var.self) { continue; } auto value = var.self->as (); auto str = String (var.self->as ()); // check the bounds and set default if out of bounds if (value > var.max || value < var.min || (!str.empty () && isalpha (str[0]))) { var.self->set (var.initial); // notify about that ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.name, var.min, var.max, str, var.initial); continue; } /// prevent min/max problems if (var.name.contains ("_max")) { String minVar = String (var.name); minVar.replace ("_max", "_min"); for (auto &mv : m_cvars) { if (mv.name == minVar) { const auto minValue = mv.self->as (); if (minValue > value) { var.self->set (minValue); mv.self->set (value); // notify about that ctrl.msg ("Bogus value for min/max cvar '%s' can't be higher than '%s'. Values swapped.", mv.name, var.name); } } } } } // special case for xash3d, by default engine is not calling startframe if no players on server, but our quota management and bot adding // mechanism assumes that starframe is called even if no players on server, so, set the xash3d's sv_forcesimulating cvar to 1 in case it's not if (is (GameFlags::Xash3DLegacy)) { static ConVarRef sv_forcesimulating ("sv_forcesimulating"); if (sv_forcesimulating.exists () && !cr::fequal (sv_forcesimulating.value (), 1.0f)) { print ("Force-enable Xash3D sv_forcesimulating cvar."); sv_forcesimulating.set ("1.0"); } } } void Game::setCvarDescription (const ConVar &cv, StringRef info) { for (auto &var : m_cvars) { if (var.name == cv.name ()) { var.info = info; break; } } } void Game::registerCvars (bool gameVars) { // this function pushes all added global variables to engine registration for (auto &var : m_cvars) { ConVar &self = *var.self; cvar_t ® = var.reg; if (var.type != Var::GameRef) { if (var.type == Var::Xash3D && !is (GameFlags::Xash3D)) { continue; } self.ptr = engfuncs.pfnCVarGetPointer (reg.name); if (!self.ptr) { static cvar_t reg_ {}; // fix metamod' memlocs not found if (is (GameFlags::Metamod)) { reg_ = var.reg; engfuncs.pfnCVarRegister (®_); } else { engfuncs.pfnCVarRegister (&var.reg); } self.ptr = engfuncs.pfnCVarGetPointer (reg.name); } } else if (gameVars) { self.ptr = engfuncs.pfnCVarGetPointer (reg.name); if (var.missing && !self.ptr) { if (reg.string == nullptr && !var.regval.empty ()) { reg.string = const_cast (var.regval.chars ()); reg.flags |= FCVAR_SERVER; } engfuncs.pfnCVarRegister (&var.reg); self.ptr = engfuncs.pfnCVarGetPointer (reg.name); } if (!self.ptr) { logger.error ("Got nullptr on cvar %s!", reg.name); } } } } void Game::constructCSBinaryName (StringArray &libs) { String suffix {}; if (plat.android) { suffix = "_android"; if (plat.x64) { suffix += "_arm64"; } else if (plat.arm) { suffix += "_armv7l"; } } else if (plat.psvita) { suffix = "_psvita"; } else if (plat.x64) { if (plat.arm) { suffix = "_arm64"; } else if (plat.ppc) { suffix = "_ppc64le"; } else if (plat.riscv) { suffix = "_riscv64d"; } else { suffix = "_amd64"; } } else if (plat.arm) { // non-android arm32 suffix = "_armv7hf"; } else if (!plat.nix && !plat.win && !plat.macos) { // fallback for unknown 32-bit x86 (e.g., legacy linux/bsd) suffix = "_i386"; } // else: suffix remains empty (e.g., x86 linux/windows/macos) // build base names if (plat.android) { // only "libcs" with suffix (no "mp", and must have "lib" prefix) libs.push ("libcs" + suffix); } else { // Standard: "mp" and "cs" with suffix libs.push ("cs" + suffix); libs.push ("mp" + suffix); } } bool Game::loadCSBinary () { StringRef modname = getRunningModName (); if (modname.empty ()) { return false; } StringArray libs {}; constructCSBinaryName (libs); auto libCheck = [&] (StringRef mod, StringRef dll) { // try to load gamedll if (!m_gameLib) { logger.fatal ("Unable to load gamedll \"%s\". Exiting... (gamedir: %s)", dll, mod); } auto ent = m_gameLib.resolve ("trigger_random_unique"); // detect regamedll by addon entity they provide if (ent != nullptr) { m_gameFlags |= GameFlags::ReGameDLL; } return true; }; // search the libraries inside game dlls directory for (const auto &lib : libs) { String path {}; if (plat.android) { // this will be removed as soon as mod downloader will be implemented on engine side auto gamelibdir = plat.env ("XASH3D_GAMELIBDIR"); path = strings.joinPath (gamelibdir, lib) + kLibrarySuffix; // if we can't read file, skip it if (!plat.fileExists (path.chars ())) { path = ""; } } if (plat.emscripten) { path = String (plat.env ("XASH3D_GAMELIBPATH")); // defined by launcher } if (path.empty ()) { path = strings.joinPath (modname, "dlls", lib) + kLibrarySuffix; // if we can't read file, skip it if (!plat.fileExists (path.chars ())) { continue; } } // special case, czero is always detected first, as it's has custom directory if (modname == "czero") { m_gameFlags |= (GameFlags::ConditionZero | GameFlags::HasBotVoice | GameFlags::HasFakePings); if (is (GameFlags::Metamod)) { return false; } m_gameLib.load (path); // verify dll is OK return libCheck (modname, lib); } else { m_gameLib.load (path); // verify dll is OK if (!libCheck (modname, lib)) { return false; } // detect if we're running modern game auto entity = m_gameLib.resolve ("weapon_famas"); // detect legacy xash3d branch if (engfuncs.pfnCVarGetPointer ("build") != nullptr) { m_gameFlags |= GameFlags::Xash3DLegacy; } // detect xash engine if (engfuncs.pfnCVarGetPointer ("host_ver") != nullptr) { m_gameFlags |= (GameFlags::Modern | GameFlags::Xash3D); if (entity != nullptr) { m_gameFlags |= GameFlags::HasBotVoice; } if (is (GameFlags::Metamod)) { return false; } return true; } if (entity != nullptr) { m_gameFlags |= (GameFlags::Modern | GameFlags::HasBotVoice); // no fake pings on xash3d if (!(m_gameFlags & (GameFlags::Xash3D | GameFlags::Xash3DLegacy))) { m_gameFlags |= GameFlags::HasFakePings; } } else { m_gameFlags |= GameFlags::Legacy; // clear modern flag just in case m_gameFlags &= ~GameFlags::Modern; } // allow to enable hitbox-based aiming on fresh games if (is (GameFlags::Modern)) { m_gameFlags |= GameFlags::HasStudioModels; } if (is (GameFlags::Metamod)) { return false; } return true; } } return false; } bool Game::postload () { bstor.checkInstallLocation (); // check if installed just as in manual // register logger logger.initialize (bstor.buildPath (BotFile::LogFile), [] (const char *msg) { game.print (msg); }); auto ensureBotPathExists = [] (StringRef dir1, StringRef dir2) { File::makePath (strings.joinPath (bstor.getRunningPath (), dir1, dir2).chars ()); }; // ensure we're have all needed directories ensureBotPathExists (folders.config, folders.lang); ensureBotPathExists (folders.data, folders.train); ensureBotPathExists (folders.data, folders.graph); ensureBotPathExists (folders.data, folders.logs); ensureBotPathExists (folders.data, folders.podbot); // set out user agent for http stuff http.setUserAgent (strings.format ("%s/%s", product.name, product.version)); // set the app name plat.setAppName (product.name.chars ()); // register bot cvars registerCvars (); // set custom cvar descriptions after registering them util.setCustomCvarDescriptions (); // handle prefixes static StringArray prefixes = { product.cmdPri, product.cmdSec }; // register all our handlers for (const auto &prefix : prefixes) { registerEngineCommand (prefix.chars (), [] () { ctrl.handleEngineCommands (); }); } // register fake metamod command handler if we not! under mm if (!(is (GameFlags::Metamod))) { registerEngineCommand ("meta", [] () { game.print ("You're launched standalone version of %s. Metamod is not installed or not enabled!", product.name); }); } // is 25th anniversary if (is25thAnniversaryUpdate ()) { m_gameFlags |= GameFlags::AnniversaryHL25; } // initialize weapons conf.initWeapons (); // register engine lib handle m_engineLib.locate (reinterpret_cast (engfuncs.pfnPrecacheModel)); if (plat.android || plat.emscripten) { m_gameFlags |= (GameFlags::Xash3D | GameFlags::Mobility | GameFlags::HasBotVoice | GameFlags::ReGameDLL); if (is (GameFlags::Metamod)) { return true; // we should stop the attempt for loading the real gamedll, since metamod handle this for us } } const bool binaryLoaded = loadCSBinary (); if (!binaryLoaded && !is (GameFlags::Metamod)) { logger.fatal ("Mod that you has started, not supported by this bot (gamedir: %s)", getRunningModName ()); } if (is (GameFlags::Metamod)) { m_gameLib.unload (); return true; } return false; } void Game::applyGameModes () { if (!is (GameFlags::Metamod | GameFlags::ReGameDLL)) { return; } // handle cvar cases switch (cv_csdm_mode.as ()) { default: case 0: break; // force CSDM mode case 1: m_gameFlags |= GameFlags::CSDM; m_gameFlags &= ~GameFlags::FreeForAll; return; // force CSDM FFA mode case 2: m_gameFlags |= GameFlags::CSDM | GameFlags::FreeForAll; return; // force disable everything case 3: m_gameFlags &= ~(GameFlags::CSDM | GameFlags::FreeForAll); return; } static StringRef csdmActiveCvarName = conf.fetchCustom ("CSDMDetectCvar"); static StringRef zmActiveCvarName = conf.fetchCustom ("ZMDetectCvar"); static StringRef zmDelayCvarName = conf.fetchCustom ("ZMDelayCvar"); static ConVarRef csdm_active (csdmActiveCvarName); static ConVarRef csdm_version ("csdm_version"); static ConVarRef redm_active ("redm_active"); static ConVarRef mp_freeforall ("mp_freeforall"); // csdm is only with amxx and metamod if (csdm_active.exists () || redm_active.exists () || csdm_version.exists ()) { if (csdm_active.value () > 0.0f || redm_active.value () > 0.0f) { m_gameFlags |= GameFlags::CSDM; } else if (is (GameFlags::CSDM)) { m_gameFlags &= ~GameFlags::CSDM; } } // but this can be provided by regamedll if (mp_freeforall.exists ()) { if (mp_freeforall.value () > 0.0f) { m_gameFlags |= (GameFlags::FreeForAll | GameFlags::CSDM); } else if (is (GameFlags::FreeForAll)) { m_gameFlags &= ~(GameFlags::FreeForAll | GameFlags::CSDM); } } // does zombie mod is in use static ConVarRef zm_active (zmActiveCvarName); // do a some little support for zombie plague if (zm_active.exists ()) { static ConVarRef zm_delay (zmDelayCvarName); // update our ignore timer if zp_delay exists if (zm_delay.exists () && zm_delay.value () > 0.0f) { cv_ignore_enemies_after_spawn_time.set (zm_delay.value () + 3.5f); } m_gameFlags |= GameFlags::ZombieMod; } else { m_gameFlags &= ~GameFlags::ZombieMod; } } void Game::slowFrame () { const auto nextUpdate = cr::clamp (75.0f * globals->frametime, 0.5f, 1.0f); // run something that is should run more if (m_halfSecondFrame < time ()) { // refresh bomb origin in case some plugin moved it out gameState.setBombOrigin (); // ensure the server admin is confident about features he's using ensureHealthyGameEnvironment (); // maintain round restart for first human join bots.maintainRoundRestart (); // update next update time m_halfSecondFrame = nextUpdate * 0.25f + time (); } if (m_oneSecondFrame > time ()) { return; } ctrl.maintainAdminRights (); // update bot difficulties to newly selected from cvar bots.updateBotDifficulties (); // check if we're need to autokill bots bots.maintainAutoKill (); // maintain leaders selection upon round start bots.maintainLeaders (); // initialize light levels graph.initLightLevels (); // initialize corridors graph.initNarrowPlaces (); // detect csdm applyGameModes (); // check the cvar bounds checkCvarsBounds (); // display welcome message util.checkWelcome (); // kick failed bots bots.checkNeedsToBeKicked (); // refresh bot infection (creature) status bots.refreshCreatureStatus (); // update client pings fakeping.calculate (); // update next update time m_oneSecondFrame = nextUpdate + time (); } void Game::searchEntities (StringRef field, StringRef value, EntitySearch functor) { edict_t *ent = nullptr; while (!isNullEntity (ent = engfuncs.pfnFindEntityByString (ent, field.chars (), value.chars ()))) { if ((ent->v.flags & EF_NODRAW) || (ent->v.flags & FL_CLIENT)) { continue; } if (functor (ent) == EntitySearchResult::Break) { break; } } } void Game::searchEntities (const Vector &position, float radius, EntitySearch functor) const { edict_t *ent = nullptr; const Vector &pos = position.empty () ? m_startEntity->v.origin : position; while (!isNullEntity (ent = engfuncs.pfnFindEntityInSphere (ent, pos, radius))) { if ((ent->v.flags & EF_NODRAW) || (ent->v.flags & FL_CLIENT)) { continue; } if (functor (ent) == EntitySearchResult::Break) { break; } } } bool Game::hasEntityInGame (StringRef classname) const { return !isNullEntity (engfuncs.pfnFindEntityByString (nullptr, "classname", classname.chars ())); } void Game::printBotVersion () const { String gameVersionStr {}; StringArray botRuntimeFlags {}; if (is (GameFlags::Legacy)) { gameVersionStr.assign ("Legacy"); } else if (is (GameFlags::ConditionZero)) { gameVersionStr.assign ("Condition Zero"); } else if (is (GameFlags::Modern)) { gameVersionStr.assign ("v1.6"); } if (is (GameFlags::Xash3D)) { if (is (GameFlags::Xash3DLegacy)) { gameVersionStr.append (" @ Xash3D-NG"); } else { gameVersionStr.append (" @ Xash3D FWGS"); } if (is (GameFlags::Mobility)) { gameVersionStr.append (" Mobile"); } gameVersionStr.replace ("Legacy", "1.6 Limited"); } if (is (GameFlags::HasBotVoice)) { botRuntimeFlags.push ("BotVoice"); } if (is (GameFlags::ReGameDLL)) { botRuntimeFlags.push ("ReGameDLL"); } if (is (GameFlags::HasFakePings)) { botRuntimeFlags.push ("FakePing"); } if (is (GameFlags::Metamod)) { botRuntimeFlags.push ("Metamod"); } if (is (GameFlags::AnniversaryHL25)) { botRuntimeFlags.push ("HL25"); } if (botRuntimeFlags.empty ()) { botRuntimeFlags.push ("None"); } // print if we're using sse 4.x instructions if (plat.simd && (cpuflags.sse41 || cpuflags.sse42 || cpuflags.neon)) { Array simdLevels {}; if (cpuflags.sse41) { simdLevels.push ("4.1"); } if (cpuflags.sse42) { simdLevels.push ("4.2"); } if (cpuflags.neon) { simdLevels.push ("Neon"); } botRuntimeFlags.push (strings.format ("SIMD: %s", String::join (simdLevels, " & "))); } ctrl.msg ("\n%s v%s successfully loaded for game: Counter-Strike %s.\n\tFlags: %s.\n", product.name, product.version, gameVersionStr, botRuntimeFlags.empty () ? "None" : String::join (botRuntimeFlags, ", ")); } void Game::ensureHealthyGameEnvironment () { const bool dedicated = isDedicated (); if (!dedicated || is (GameFlags::Legacy | GameFlags::Xash3D)) { if (!dedicated) { // force enable pings on listen servers if disabled at all if (is (GameFlags::Modern) && cv_show_latency.as () == 0) { cv_show_latency.set (2); } } return; // listen servers doesn't care about it at all } // magic string that's enables the features constexpr auto kAllowHash = StringRef::fnv1a32 ("i'm confident for what i'm doing"); constexpr auto kAllowHash2 = StringRef::fnv1a32 ("\"i'm confident for what i'm doing\""); // fetch custom variable, so fake features are explicitly enabled static auto enableFakeFeatures = StringRef::fnv1a32 (conf.fetchCustom ("EnableFakeBotFeatures").chars ()); // if string matches, do not affect the cvars if (enableFakeFeatures == kAllowHash || enableFakeFeatures == kAllowHash2) { return; } auto notifyPeacefulRevert = [] (const ConVar &cv) { game.print ("Cvar \"%s\" reverted to peaceful value.", cv.name ()); }; // disable fake latency if (cv_show_latency.as () > 1) { cv_show_latency.set (0); notifyPeacefulRevert (cv_show_latency); } // disable fake avatars if (cv_show_avatars) { cv_show_avatars.set (0); notifyPeacefulRevert (cv_show_avatars); } // disable fake queries if (cv_enable_query_hook) { cv_enable_query_hook.set (0); notifyPeacefulRevert (cv_enable_query_hook); } } edict_t *Game::createFakeClient (StringRef name) { auto ent = engfuncs.pfnCreateFakeClient (name.chars ()); if (isNullEntity (ent)) { return nullptr; } auto netname = ent->v.netname; ent->v = {}; // reset entire the entvars structure (fix from regamedll) // restore containing entity, name and client flags ent->v.pContainingEntity = ent; ent->v.flags = FL_FAKECLIENT | FL_CLIENT; ent->v.netname = netname; if (ent->pvPrivateData != nullptr) { engfuncs.pfnFreeEntPrivateData (ent); } ent->pvPrivateData = nullptr; return ent; } void Game::markBreakableAsInvalid (edict_t *ent) { m_checkedBreakables[indexOfEntity (ent)] = false; } bool Game::isDeveloperMode () const { static ConVarRef developer { "developer" }; return developer.exists () && developer.value () > 0.0f; } bool Game::isAliveEntity (edict_t *ent) const { if (isNullEntity (ent)) { return false; } return ent->v.deadflag == DEAD_NO && ent->v.health > 0.0f && ent->v.movetype != MOVETYPE_NOCLIP; } bool Game::isPlayerEntity (edict_t *ent) const { if (isNullEntity (ent)) { return false; } if (ent->v.flags & FL_PROXY) { return false; } if ((ent->v.flags & (FL_CLIENT | FL_FAKECLIENT)) || bots[ent] != nullptr) { return !strings.isEmpty (ent->v.netname.chars ()); } return false; } bool Game::isMonsterEntity (edict_t *ent) const { if (isNullEntity (ent)) { return false; } if (~ent->v.flags & FL_MONSTER) { return false; } if (isHostageEntity (ent)) { return false; } return true; } bool Game::isItemEntity (edict_t *ent) const { return ent && ent->v.classname.str ().contains ("item_"); } bool Game::isPlayerVIP (edict_t *ent) const { if (!mapIs (MapFlags::Assassination)) { return false; } if (!isPlayerEntity (ent)) { return false; } return *(engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "model")) == 'v'; } bool Game::isDoorEntity (edict_t *ent) const { if (isNullEntity (ent)) { return false; } const auto classHash = ent->v.classname.str ().hash (); constexpr auto kFuncDoor = StringRef::fnv1a32 ("func_door"); constexpr auto kFuncDoorRotating = StringRef::fnv1a32 ("func_door_rotating"); return classHash == kFuncDoor || classHash == kFuncDoorRotating; } bool Game::isHostageEntity (edict_t *ent) const { if (isNullEntity (ent)) { return false; } const auto classHash = ent->v.classname.str ().hash (); constexpr auto kHostageEntity = StringRef::fnv1a32 ("hostage_entity"); constexpr auto kMonsterScientist = StringRef::fnv1a32 ("monster_scientist"); return classHash == kHostageEntity || classHash == kMonsterScientist; } bool Game::isBreakableEntity (edict_t *ent, bool initialSeed) const { if (!initialSeed) { if (!hasBreakables ()) { return false; } } if (isNullEntity (ent) || ent == getStartEntity () || (!initialSeed && !game.isBreakableValid (ent))) { return false; } const auto limit = cv_breakable_health_limit.as (); // not shoot-able if (ent->v.health < 5 || ent->v.health >= limit) { return false; } constexpr auto kFuncBreakable = StringRef::fnv1a32 ("func_breakable"); constexpr auto kFuncPushable = StringRef::fnv1a32 ("func_pushable"); constexpr auto kFuncWall = StringRef::fnv1a32 ("func_wall"); if (ent->v.takedamage > 0.0f && ent->v.impulse <= 0 && !(ent->v.flags & FL_WORLDBRUSH) && !(ent->v.spawnflags & SF_BREAK_TRIGGER_ONLY)) { const auto classHash = ent->v.classname.str ().hash (); if (classHash == kFuncBreakable || (classHash == kFuncPushable && (ent->v.spawnflags & SF_PUSH_BREAKABLE)) || classHash == kFuncWall) { return ent->v.movetype == MOVETYPE_PUSH || ent->v.movetype == MOVETYPE_PUSHSTEP; } } return false; } bool Game::isFakeClientEntity (edict_t *ent) const { return bots[ent] != nullptr || (!isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT)); } bool Game::isEntityModelMatches (const edict_t *ent, StringRef model) const { return model.startsWith (ent->v.model.chars (9)); } void LightMeasure::initializeLightstyles () { // this function initializes lighting information... // reset all light styles for (auto &ls : m_lightstyle) { ls.length = 0; ls.map[0] = kNullChar; } for (auto &lsv : m_lightstyleValue) { lsv = 264; } } void LightMeasure::animateLight () { // this function performs light animations if (!m_doAnimation) { return; } // 'm' is normal light, 'a' is no light, 'z' is double bright const auto index = static_cast (game.time () * 10.0f); for (auto j = 0; j < MAX_LIGHTSTYLES; ++j) { if (!m_lightstyle[j].length) { m_lightstyleValue[j] = MAX_LIGHTSTYLEVALUE; continue; } m_lightstyleValue[j] = static_cast (m_lightstyle[j].map[index % m_lightstyle[j].length] - 'a') * 22u; } } void LightMeasure::updateLight (int style, char *value) { if (!m_doAnimation) { return; } if (style >= MAX_LIGHTSTYLES) { return; } if (strings.isEmpty (value)) { m_lightstyle[style].length = 0u; m_lightstyle[style].map[0] = kNullChar; return; } const auto copyLimit = sizeof (m_lightstyle[style].map) - sizeof (kNullChar); strings.copy (m_lightstyle[style].map, value, copyLimit); m_lightstyle[style].map[copyLimit] = kNullChar; m_lightstyle[style].length = static_cast (strlen (m_lightstyle[style].map)); } template bool LightMeasure::recursiveLightPoint (const M *node, const Vector &start, const Vector &end) { if (!node || node->contents < 0) { return false; } // determine which side of the node plane our points are on, fixme: optimize for axial const auto plane = node->plane; const float front = (start | plane->normal) - plane->dist; const float back = (end | plane->normal) - plane->dist; const int side = front < 0.0f; // if they're both on the same side of the plane, don't bother to split just check the appropriate child if ((back < 0.0f) == side) { return recursiveLightPoint (reinterpret_cast (node->children[side]), start, end); } // calculate mid point const float frac = front / (front - back); auto mid = start + (end - start) * frac; // go down front side if (recursiveLightPoint (reinterpret_cast (node->children[side]), start, mid)) { return true; // hit something } // blow it off if it doesn't split the plane... if ((back < 0.0f) == !!side) { return false; // didn't hit anything } // check for impact on this node // lightspot = mid; // lightplane = plane; auto surf = reinterpret_cast (m_worldModel->surfaces) + node->firstsurface; for (int i = 0; i < node->numsurfaces; ++i, ++surf) { if (surf->flags & SURF_DRAWTILED) { continue; // no lightmaps } const auto tex = surf->texinfo; // see where in lightmap space our intersection point is const int s = static_cast ((mid | Vector (tex->vecs[0])) + tex->vecs[0][3]); const int t = static_cast ((mid | Vector (tex->vecs[1])) + tex->vecs[1][3]); // not in the bounds of our lightmap? punt... if (s < surf->texturemins[0] || t < surf->texturemins[1]) { continue; } // assuming a square lightmap (fixme: which ain't always the case), lets see if it lies in that rectangle. if not, punt... int ds = s - surf->texturemins[0]; int dt = t - surf->texturemins[1]; if (ds > surf->extents[0] || dt > surf->extents[1]) { continue; } if (!surf->samples) { return true; } ds >>= 4; dt >>= 4; m_point.reset (); // reset point color. const int smax = (surf->extents[0] >> 4) + 1; const int tmax = (surf->extents[1] >> 4) + 1; const int size = smax * tmax; auto lightmap = surf->samples + dt * smax + ds; // compute the lightmap color at a particular point for (int maps = 0; maps < MAX_LIGHTMAPS && surf->styles[maps] != 255; ++maps) { const auto scale = static_cast (m_lightstyleValue[surf->styles[maps]]); m_point.red += lightmap->r * scale; m_point.green += lightmap->g * scale; m_point.blue += lightmap->b * scale; lightmap += size; // skip to next lightmap } m_point.red >>= 8u; m_point.green >>= 8u; m_point.blue >>= 8u; return true; } return recursiveLightPoint (reinterpret_cast (node->children[!side]), mid, end); // go down back side } float LightMeasure::getLightLevel (const Vector &point) { if (game.is (GameFlags::Legacy)) { return kInvalidLightLevel; } if (!m_worldModel) { return kInvalidLightLevel; } if (!m_worldModel->lightdata) { return 255.0f; } Vector endPoint (point); endPoint.z -= 2048.0f; static bool isSoftRenderer = game.isSoftwareRenderer (); static bool is25Anniversary = game.is25thAnniversaryUpdate (); // it's depends if we're are on dedicated or on listenserver auto recursiveCheck = [&] () -> bool { if (!isSoftRenderer) { if (is25Anniversary) { return recursiveLightPoint (reinterpret_cast (m_worldModel->nodes), point, endPoint); } return recursiveLightPoint (reinterpret_cast (m_worldModel->nodes), point, endPoint); } return recursiveLightPoint (m_worldModel->nodes, point, endPoint); }; return !recursiveCheck () ? kInvalidLightLevel : 100 * cr::sqrtf (cr::min (75.0f, static_cast (m_point.avg ())) / 75.0f); } float LightMeasure::getSkyColor () { return static_cast (Color (sv_skycolor_r.as (), sv_skycolor_g.as (), sv_skycolor_b.as ()).avg ()); } Vector PlayerHitboxEnumerator::get (edict_t *ent, int part, float updateTimestamp) { auto parts = &m_parts[game.indexOfEntity (ent) % kGameMaxPlayers]; if (game.time () > parts->updated) { update (ent); parts->updated = game.time () + updateTimestamp; } switch (part) { default: case PlayerPart::Head: return parts->head; case PlayerPart::Stomach: return parts->stomach; case PlayerPart::LeftArm: return parts->left; case PlayerPart::RightArm: return parts->right; case PlayerPart::Feet: return parts->feet; case PlayerPart::RightLeg: return { parts->right.x, parts->right.y, parts->feet.z }; case PlayerPart::LeftLeg: return { parts->left.x, parts->left.y, parts->feet.z }; } } void PlayerHitboxEnumerator::update (edict_t *ent) { constexpr auto kInvalidHitbox = -1; if (!game.isAliveEntity (ent)) { return; } // get info about player auto parts = &m_parts[game.indexOfEntity (ent) % kGameMaxPlayers]; // set the feet without bones parts->feet = ent->v.origin; constexpr auto kStandFeet = 34.0f; constexpr auto kCrouchFeet = 14.0f; // legs position isn't calculated to reduce cpu usage, just use some universal feet spot if (ent->v.flags & FL_DUCKING) { parts->feet.z = ent->v.origin.z - kCrouchFeet; } else { parts->feet.z = ent->v.origin.z - kStandFeet; } auto getHitbox = [&] (studiohdr_t *hdr, mstudiobbox_t *bb, int part) { int hitbox = kInvalidHitbox; for (auto i = 0; i < hdr->numhitboxes; ++i) { const auto set = &bb[i]; if (set->group != part) { continue; } hitbox = i; break; } return hitbox; }; auto model = engfuncs.pfnGetModelPtr (ent); auto studiohdr = reinterpret_cast (model); // this can be null ? if (model && studiohdr) { auto bboxset = reinterpret_cast (reinterpret_cast (studiohdr) + studiohdr->hitboxindex); // get the head auto hitbox = getHitbox (studiohdr, bboxset, PlayerPart::Head); if (hitbox != kInvalidHitbox) { engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->head, nullptr); parts->head.z += bboxset[hitbox].bbmax.z; parts->head = { ent->v.origin.x, ent->v.origin.y, parts->head.z }; } // get the body (stomach) hitbox = getHitbox (studiohdr, bboxset, PlayerPart::Stomach); if (hitbox != kInvalidHitbox) { engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->stomach, nullptr); } // get the left (arm) hitbox = getHitbox (studiohdr, bboxset, PlayerPart::LeftArm); if (hitbox != kInvalidHitbox) { engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->left, nullptr); } // get the right (arm) hitbox = getHitbox (studiohdr, bboxset, PlayerPart::RightArm); if (hitbox != kInvalidHitbox) { engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->right, nullptr); } return; } else { game.clearGameFlag (GameFlags::HasStudioModels); // yes, only a single fail will disable this } parts->head = ent->v.origin + ent->v.view_ofs; parts->stomach = ent->v.origin; parts->left = parts->head; parts->right = parts->head; } void PlayerHitboxEnumerator::reset () { for (auto &part : m_parts) { part = {}; } } void GameState::setBombOrigin (bool reset, const Vector &pos) { // this function stores the bomb position as a vector if (!game.mapIs (MapFlags::Demolition) || !gameState.isBombPlanted ()) { return; } if (reset) { m_bombOrigin.clear (); setBombPlanted (false); return; } if (!pos.empty ()) { m_bombOrigin = pos; return; } bool wasFound = false; auto bombModel = conf.getBombModelName (); game.searchEntities ("classname", "grenade", [&] (edict_t *ent) { if (game.isEntityModelMatches (ent, bombModel)) { m_bombOrigin = game.getEntityOrigin (ent); wasFound = true; return EntitySearchResult::Break; } return EntitySearchResult::Continue; }); if (!wasFound) { m_bombOrigin.clear (); setBombPlanted (false); } } void GameState::roundStart () { m_roundOver = false; m_timeBombPlanted = 0.0f; // tell the bots bots.initRound (); setBombOrigin (true); // calculate the round mid/end in world time m_timeRoundStart = game.time () + mp_freezetime.as (); m_timeRoundMid = m_timeRoundStart + mp_roundtime.as () * 60.0f * 0.5f; m_timeRoundEnd = m_timeRoundStart + mp_roundtime.as () * 60.0f; m_interestingEntities.clear (); m_activeGrenades.clear (); m_activeGrenadesUpdateTime.reset (); m_interestingEntitiesUpdateTime.reset (); } float GameState::getBombTimeLeft () const { if (!m_bombPlanted) { return 0.0f; } return cr::max (m_timeBombPlanted + mp_c4timer.as () - game.time (), 0.0f); } void GameState::setBombPlanted (bool isPlanted) { if (cv_ignore_objectives) { m_bombPlanted = false; return; } if (isPlanted) { m_timeBombPlanted = game.time (); } m_bombPlanted = isPlanted; } void GameState::updateActiveGrenade () { constexpr auto kUpdateTime = 0.25f; if (m_activeGrenadesUpdateTime.lessThen (kUpdateTime)) { return; } m_activeGrenades.clear (); // clear previously stored grenades // need to ignore bomb model in active grenades... auto bombModel = conf.getBombModelName (); // search the map for any type of grenade game.searchEntities ("classname", "grenade", [&] (edict_t *e) { // do not count c4 as a grenade if (!game.isEntityModelMatches (e, bombModel)) { m_activeGrenades.push (e); } return EntitySearchResult::Continue; // continue iteration }); m_activeGrenadesUpdateTime.start (); } void GameState::updateInterestingEntities () { constexpr auto kUpdateTime = 0.5f; if (m_interestingEntitiesUpdateTime.lessThen (kUpdateTime)) { return; } m_interestingEntities.clear (); // clear previously stored entities // search the map for any type of grenade game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) { auto classname = e->v.classname.str (); // search for grenades, weaponboxes, weapons, items and armoury entities if (classname.startsWith ("weaponbox") || classname.startsWith ("grenade") || game.isItemEntity (e) || classname.startsWith ("armoury")) { m_interestingEntities.push (e); } // pickup some hostage if on cs_ maps if (game.mapIs (MapFlags::HostageRescue) && game.isHostageEntity (e)) { m_interestingEntities.push (e); } // add buttons if (game.mapIs (MapFlags::HasButtons) && classname.startsWith ("func_button")) { m_interestingEntities.push (e); } // pickup some csdm stuff if we're running csdm if (game.is (GameFlags::CSDM) && classname.startsWith ("csdm")) { m_interestingEntities.push (e); } if (cv_attack_monsters && game.isMonsterEntity (e)) { m_interestingEntities.push (e); } // continue iteration return EntitySearchResult::Continue; }); m_interestingEntitiesUpdateTime.start (); }