// // Yet Another POD-Bot, based on PODBot by Markus Klinge ("CountFloyd"). // Copyright (c) YaPB Development Team. // // This software is licensed under the BSD-style license. // Additional exceptions apply. For full license details, see LICENSE.txt or visit: // https://yapb.ru/license // #include // console vars ConVar yb_ignore_cvars_on_changelevel ("yb_ignore_cvars_on_changelevel", "yb_quota,yb_autovacate"); ConVar yb_password ("yb_password", "", VT_PASSWORD); ConVar yb_password_key ("yb_password_key", "_ybpw"); ConVar yb_language ("yb_language", "en"); ConVar yb_version ("yb_version", PRODUCT_VERSION, VT_READONLY); ConVar mp_startmoney ("mp_startmoney", nullptr, VT_NOREGISTER, true, "800"); int handleBotCommands (edict_t *ent, const char *arg0, const char *arg1, const char *arg2, const char *arg3, const char *arg4, const char *arg5, const char *self) { // adding one bot with random parameters to random team if (stricmp (arg0, "addbot") == 0 || stricmp (arg0, "add") == 0) { bots.addbot (arg4, arg1, arg2, arg3, arg5, true); } // adding one bot with high difficulty parameters to random team else if (stricmp (arg0, "addbot_hs") == 0 || stricmp (arg0, "addhs") == 0) { bots.addbot (arg4, "4", "1", arg3, arg5, true); } // adding one bot with random parameters to terrorist team else if (stricmp (arg0, "addbot_t") == 0 || stricmp (arg0, "add_t") == 0) { bots.addbot (arg4, arg1, arg2, "1", arg5, true); } // adding one bot with random parameters to counter-terrorist team else if (stricmp (arg0, "addbot_ct") == 0 || stricmp (arg0, "add_ct") == 0) { bots.addbot (arg4, arg1, arg2, "2", arg5, true); } // kicking off one bot from the terrorist team else if (stricmp (arg0, "kickbot_t") == 0 || stricmp (arg0, "kick_t") == 0) { bots.kickFromTeam (TEAM_TERRORIST); } // kicking off one bot from the counter-terrorist team else if (stricmp (arg0, "kickbot_ct") == 0 || stricmp (arg0, "kick_ct") == 0) { bots.kickFromTeam (TEAM_COUNTER); } // kills all bots on the terrorist team else if (stricmp (arg0, "killbots_t") == 0 || stricmp (arg0, "kill_t") == 0) { bots.killAllBots (TEAM_TERRORIST); } // kills all bots on the counter-terrorist team else if (stricmp (arg0, "killbots_ct") == 0 || stricmp (arg0, "kill_ct") == 0) { bots.killAllBots (TEAM_COUNTER); } // list all bots playeing on the server else if (stricmp (arg0, "listbots") == 0 || stricmp (arg0, "list") == 0) { bots.listBots (); } // kick off all bots from the played server else if (stricmp (arg0, "kickbots") == 0 || stricmp (arg0, "kickall") == 0) { bots.kickEveryone (); } // kill all bots on the played server else if (stricmp (arg0, "killbots") == 0 || stricmp (arg0, "killall") == 0) { bots.killAllBots (); } // kick off one random bot from the played server else if (stricmp (arg0, "kickone") == 0 || stricmp (arg0, "kick") == 0) { bots.kickRandom (); } // fill played server with bots else if (stricmp (arg0, "fillserver") == 0 || stricmp (arg0, "fill") == 0) { bots.serverFill (atoi (arg1), isEmptyStr (arg2) ? -1 : atoi (arg2), isEmptyStr (arg3) ? -1 : atoi (arg3), isEmptyStr (arg4) ? -1 : atoi (arg4)); } // select the weapon mode for bots else if (stricmp (arg0, "weaponmode") == 0 || stricmp (arg0, "wmode") == 0) { int selection = atoi (arg1); // check is selected range valid if (selection >= 1 && selection <= 7) bots.setWeaponMode (selection); else engine.clientPrint (ent, "Choose weapon from 1 to 7 range"); } // force all bots to vote to specified map else if (stricmp (arg0, "votemap") == 0) { if (!isEmptyStr (arg1)) { int nominatedMap = atoi (arg1); // loop through all players for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot != nullptr) bot->m_voteMap = nominatedMap; } engine.clientPrint (ent, "All dead bots will vote for map #%d", nominatedMap); } } // displays version information else if (stricmp (arg0, "version") == 0 || stricmp (arg0, "ver") == 0) { char versionData[] = "------------------------------------------------\n" "Name: %s\n" "Version: %s (Build: %u)\n" "Compiled: %s, %s\n" "Git Hash: %s\n" "Git Commit Author: %s\n" "------------------------------------------------"; engine.clientPrint (ent, versionData, PRODUCT_NAME, PRODUCT_VERSION, buildNumber (), __DATE__, __TIME__, PRODUCT_GIT_HASH, PRODUCT_GIT_COMMIT_AUTHOR); } // display some sort of help information else if (strcmp (arg0, "?") == 0 || strcmp (arg0, "help") == 0) { engine.clientPrint (ent, "Bot Commands:"); engine.clientPrint (ent, "%s version\t - display version information.", self); engine.clientPrint (ent, "%s add\t - create a bot in current game.", self); engine.clientPrint (ent, "%s fill\t - fill the server with random bots.", self); engine.clientPrint (ent, "%s kickall\t - disconnects all bots from current game.", self); engine.clientPrint (ent, "%s killbots\t - kills all bots in current game.", self); engine.clientPrint (ent, "%s kick\t - disconnect one random bot from game.", self); engine.clientPrint (ent, "%s weaponmode\t - select bot weapon mode.", self); engine.clientPrint (ent, "%s votemap\t - allows dead bots to vote for specific map.", self); engine.clientPrint (ent, "%s cmenu\t - displaying bots command menu.", self); if (strcmp (arg1, "full") == 0 || strcmp (arg1, "?") == 0) { engine.clientPrint (ent, "%s add_t\t - creates one random bot to terrorist team.", self); engine.clientPrint (ent, "%s add_ct\t - creates one random bot to ct team.", self); engine.clientPrint (ent, "%s kick_t\t - disconnect one random bot from terrorist team.", self); engine.clientPrint (ent, "%s kick_ct\t - disconnect one random bot from ct team.", self); engine.clientPrint (ent, "%s kill_t\t - kills all bots on terrorist team.", self); engine.clientPrint (ent, "%s kill_ct\t - kills all bots on ct team.", self); engine.clientPrint (ent, "%s list\t - display list of bots currently playing.", self); engine.clientPrint (ent, "%s deletewp\t - erase waypoint file from hard disk (permanently).", self); if (!engine.isDedicated ()) { engine.print ("%s autowp\t - toggle autowaypointing.", self); engine.print ("%s wp\t - toggle waypoint showing.", self); engine.print ("%s wp on noclip\t - enable noclip cheat", self); engine.print ("%s wp save nocheck\t - save waypoints without checking.", self); engine.print ("%s wp add\t - open menu for waypoint creation.", self); engine.print ("%s wp delete\t - delete waypoint nearest to host edict.", self); engine.print ("%s wp menu\t - open main waypoint menu.", self); engine.print ("%s wp addbasic\t - creates basic waypoints on map.", self); engine.print ("%s wp find\t - show direction to specified waypoint.", self); engine.print ("%s wp load\t - load the waypoint file from hard disk.", self); engine.print ("%s wp check\t - checks if all waypoints connections are valid.", self); engine.print ("%s wp cache\t - cache nearest waypoint.", self); engine.print ("%s wp teleport\t - teleport hostile to specified waypoint.", self); engine.print ("%s wp setradius\t - manually sets the wayzone radius for this waypoint.", self); engine.print ("%s wp clean \t - cleans useless paths for specified or all waypoints.", self); engine.print ("%s path autodistance - opens menu for setting autopath maximum distance.", self); engine.print ("%s path cache\t - remember the nearest to player waypoint.", self); engine.print ("%s path create\t - opens menu for path creation.", self); engine.print ("%s path delete\t - delete path from cached to nearest waypoint.", self); engine.print ("%s path create_in\t - creating incoming path connection.", self); engine.print ("%s path create_out\t - creating outgoing path connection.", self); engine.print ("%s path create_both\t - creating both-ways path connection.", self); engine.print ("%s exp save\t - save the experience data.", self); } } } else if (stricmp (arg0, "bot_takedamage") == 0 && !isEmptyStr (arg1)) { bool isOn = !!(atoi (arg1) == 1); for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot != nullptr) { bot->pev->takedamage = isOn ? 0.0f : 1.0f; } } } // displays main bot menu else if (stricmp (arg0, "botmenu") == 0 || stricmp (arg0, "menu") == 0) { showMenu (ent, BOT_MENU_MAIN); } // display command menu else if (stricmp (arg0, "cmdmenu") == 0 || stricmp (arg0, "cmenu") == 0) { if (isAlive (ent)) { showMenu (ent, BOT_MENU_COMMANDS); } else { showMenu (ent, BOT_MENU_INVALID); // reset menu display engine.centerPrint ("You're dead, and have no access to this menu"); } } // waypoint manimupulation (really obsolete, can be edited through menu) (supported only on listen server) else if (stricmp (arg0, "waypoint") == 0 || stricmp (arg0, "wp") == 0 || stricmp (arg0, "wpt") == 0) { if (engine.isDedicated () || engine.isNullEntity (g_hostEntity)) { return 2; } // enables or disable waypoint displaying if (stricmp (arg1, "on") == 0) { g_waypointOn = true; engine.print ("Waypoint Editing Enabled"); // enables noclip cheat if (!isEmptyStr (arg2) && stricmp (arg2, "noclip") == 0) { if (g_editNoclip) { g_hostEntity->v.movetype = MOVETYPE_WALK; engine.print ("Noclip Cheat Disabled"); g_editNoclip = false; } else { g_hostEntity->v.movetype = MOVETYPE_NOCLIP; engine.print ("Noclip Cheat Enabled"); g_editNoclip = true; } } engine.execCmd ("yapb wp mdl on"); } // switching waypoint editing off else if (stricmp (arg1, "off") == 0) { g_waypointOn = false; g_editNoclip = false; g_hostEntity->v.movetype = MOVETYPE_WALK; engine.print ("Waypoint Editing Disabled"); engine.execCmd ("yapb wp mdl off"); } // toggles displaying player models on spawn spots else if (stricmp (arg1, "mdl") == 0 || stricmp (arg1, "models") == 0) { auto toggleDraw = [] (bool draw, const String &stuff) { edict_t *ent = nullptr; while (!engine.isNullEntity (ent = g_engfuncs.pfnFindEntityByString (ent, "classname", stuff.chars ()))) { if (draw) { ent->v.effects &= ~EF_NODRAW; } else { ent->v.effects |= EF_NODRAW; } } }; StringArray entities; entities.push ("info_player_start"); entities.push ("info_player_deathmatch"); entities.push ("info_vip_start"); if (stricmp (arg2, "on") == 0) { for (auto &type : entities) { toggleDraw (true, type); } engine.execCmd ("mp_roundtime 9"); // reset round time to maximum engine.execCmd ("mp_timelimit 0"); // disable the time limit engine.execCmd ("mp_freezetime 0"); // disable freezetime } else if (stricmp (arg2, "off") == 0) { for (auto &type : entities) { toggleDraw (false, type); } } } // show direction to specified waypoint else if (stricmp (arg1, "find") == 0) { waypoints.setSearchIndex (atoi (arg2)); } // opens adding waypoint menu else if (stricmp (arg1, "add") == 0) { g_waypointOn = true; // turn waypoints on showMenu (g_hostEntity, BOT_MENU_WAYPOINT_TYPE); } // creates basic waypoints on the map (ladder/spawn points/goals) else if (stricmp (arg1, "addbasic") == 0) { waypoints.addBasic (); engine.centerPrint ("Basic waypoints was Created"); } // delete nearest to host edict waypoint else if (stricmp (arg1, "delete") == 0) { g_waypointOn = true; // turn waypoints on if (!isEmptyStr (arg2)) { waypoints.erase (atoi (arg2)); } else { waypoints.erase (INVALID_WAYPOINT_INDEX); } } // save waypoint data into file on hard disk else if (stricmp (arg1, "save") == 0) { const char *waypointSaveMessage = "Waypoints Saved"; if (strcmp (arg2, "nocheck") == 0) { waypoints.save (); engine.print (waypointSaveMessage); } else if (waypoints.checkNodes ()) { waypoints.save (); engine.print (waypointSaveMessage); } } // remove waypoint and all corresponding files from hard disk else if (stricmp (arg1, "erase") == 0) { waypoints.eraseFromDisk (); } // load all waypoints again (overrides all changes, that wasn't saved) else if (stricmp (arg1, "load") == 0) { if (waypoints.load ()) engine.print ("Waypoints loaded"); } // check all nodes for validation else if (stricmp (arg1, "check") == 0) { if (waypoints.checkNodes ()) engine.centerPrint ("Nodes work Fine"); } // opens menu for setting (removing) waypoint flags else if (stricmp (arg1, "flags") == 0) { showMenu (g_hostEntity, BOT_MENU_WAYPOINT_FLAG); } // setting waypoint radius else if (stricmp (arg1, "setradius") == 0) { waypoints.setRadius (atoi (arg2)); } // remembers nearest waypoint else if (stricmp (arg1, "cache") == 0) { waypoints.cachePoint (); } // do a cleanup else if (stricmp (arg1, "clean") == 0) { if (!isEmptyStr (arg2)) { if (stricmp (arg2, "all") == 0) { int totalCleared = 0; for (int i = 0; i < waypoints.length (); i++) { totalCleared += waypoints.removeUselessConnections (i, true); } engine.print ("Done. Processed %d waypoints. %d useless paths cleared.", totalCleared); } else { int clearIndex = atoi (arg2), totalCleared = 0; if (waypoints.exists (clearIndex)) { totalCleared += waypoints.removeUselessConnections (clearIndex); engine.print ("Done. Waypoint %d had %d useless paths.", clearIndex, totalCleared); } } } } // teleport player to specified waypoint else if (stricmp (arg1, "teleport") == 0) { int teleportPoint = atoi (arg2); if (waypoints.exists (teleportPoint)) { Path &path = waypoints[teleportPoint]; g_engfuncs.pfnSetOrigin (g_hostEntity, path.origin); g_waypointOn = true; engine.print ("Player '%s' teleported to waypoint #%d (x:%.1f, y:%.1f, z:%.1f)", STRING (g_hostEntity->v.netname), teleportPoint, path.origin.x, path.origin.y, path.origin.z); //-V807 g_editNoclip = true; } } // displays waypoint menu else if (stricmp (arg1, "menu") == 0) { showMenu (g_hostEntity, BOT_MENU_WAYPOINT_MAIN_PAGE1); } // otherwise display waypoint current status else { engine.print ("Waypoints are %s", g_waypointOn == true ? "Enabled" : "Disabled"); } } // path waypoint editing system (supported only on listen server) else if (stricmp (arg0, "pathwaypoint") == 0 || stricmp (arg0, "path") == 0 || stricmp (arg0, "pwp") == 0) { if (engine.isDedicated () || engine.isNullEntity (g_hostEntity)) { return 2; } // opens path creation menu if (stricmp (arg1, "create") == 0) { showMenu (g_hostEntity, BOT_MENU_WAYPOINT_PATH); } // creates incoming path from the cached waypoint else if (stricmp (arg1, "create_in") == 0) { waypoints.pathCreate (CONNECTION_INCOMING); } // creates outgoing path from current waypoint else if (stricmp (arg1, "create_out") == 0) { waypoints.pathCreate (CONNECTION_OUTGOING); } // creates bidirectional path from cached to current waypoint else if (stricmp (arg1, "create_both") == 0) { waypoints.pathCreate (CONNECTION_BOTHWAYS); } // delete special path else if (stricmp (arg1, "delete") == 0) { waypoints.erasePath (); } // sets auto path maximum distance else if (stricmp (arg1, "autodistance") == 0) { showMenu (g_hostEntity, BOT_MENU_WAYPOINT_AUTOPATH); } } // automatic waypoint handling (supported only on listen server) else if (stricmp (arg0, "autowaypoint") == 0 || stricmp (arg0, "autowp") == 0) { if (engine.isDedicated () || engine.isNullEntity (g_hostEntity)) { return 2; } // enable autowaypointing if (stricmp (arg1, "on") == 0) { g_autoWaypoint = true; g_waypointOn = true; // turn this on just in case } // disable autowaypointing else if (stricmp (arg1, "off") == 0) { g_autoWaypoint = false; } // display status engine.print ("Auto-Waypoint %s", g_autoWaypoint ? "Enabled" : "Disabled"); } // experience system handling (supported only on listen server) else if (stricmp (arg0, "experience") == 0 || stricmp (arg0, "exp") == 0) { if (engine.isDedicated () || engine.isNullEntity (g_hostEntity)) { return 2; } // write experience table (and visibility table) to hard disk if (stricmp (arg1, "save") == 0) { waypoints.saveExperience (); waypoints.saveVisibility (); engine.print ("Experience tab saved"); } } else { return 0; // command is not handled by bot } return 1; // command was handled by bot } void execBotConfigs (bool onlyMain) { static bool setMemoryPointers = true; if (setMemoryPointers) { MemoryLoader::ref ().setup (g_engfuncs.pfnLoadFileForMe, g_engfuncs.pfnFreeFile); setMemoryPointers = true; } auto isCommentLine = [] (const char *line) { char ch = *line; return ch == '#' || ch == '/' || ch == '\r' || ch == ';' || ch == 0 || ch == ' '; }; MemFile fp; char lineBuffer[512]; // this is does the same as exec of engine, but not overwriting values of cvars spcified in yb_ignore_cvars_on_changelevel if (onlyMain) { static bool firstLoad = true; auto needsToIgnoreVar = [](StringArray &list, const char *needle) { for (auto &var : list) { if (var == needle) { return true; } } return false; }; if (openConfig ("yapb.cfg", "YaPB main config file is not found.", &fp, false)) { while (fp.gets (lineBuffer, 255)) { if (isCommentLine (lineBuffer)) { continue; } if (firstLoad) { engine.execCmd (lineBuffer); continue; } auto keyval = String (lineBuffer).split (" "); if (keyval.length () > 1) { auto ignore = String (yb_ignore_cvars_on_changelevel.str ()).split (","); auto key = keyval[0].trim ().chars (); auto cvar = g_engfuncs.pfnCVarGetPointer (key); if (cvar != nullptr) { auto value = const_cast (keyval[1].trim ().trim ("\"").trim ().chars ()); if (needsToIgnoreVar (ignore, key) && !!stricmp (value, cvar->string)) { engine.print ("Bot CVAR '%s' differs from the stored in the config (%s/%s). Ignoring.", cvar->name, cvar->string, value); // ensure cvar will have old value g_engfuncs.pfnCvar_DirectSet (cvar, cvar->string); } else { g_engfuncs.pfnCvar_DirectSet (cvar, value); } } else engine.execCmd (lineBuffer); } } fp.close (); } firstLoad = false; return; } KeywordFactory replies; // reserve some space for chat g_chatFactory.reserve (CHAT_TOTAL); g_chatterFactory.reserve (CHATTER_MAX); g_botNames.reserve (CHATTER_MAX); // NAMING SYSTEM INITIALIZATION if (openConfig ("names.cfg", "Name configuration file not found.", &fp, true)) { g_botNames.clear (); while (fp.gets (lineBuffer, 255)) { if (isCommentLine (lineBuffer)) { continue; } StringArray pair = String (lineBuffer).split ("\t\t"); if (pair.length () > 1) { strncpy (lineBuffer, pair[0].trim ().chars (), cr::bufsize (lineBuffer)); } String::trimChars (lineBuffer); lineBuffer[32] = 0; BotName item; item.name = lineBuffer; item.usedBy = 0; if (pair.length () > 1) { item.steamId = pair[1].trim (); } g_botNames.push (cr::move (item)); } fp.close (); } // CHAT SYSTEM CONFIG INITIALIZATION if (openConfig ("chat.cfg", "Chat file not found.", &fp, true)) { int chatType = -1; while (fp.gets (lineBuffer, 255)) { if (isCommentLine (lineBuffer)) { continue; } String section (lineBuffer, strlen (lineBuffer) - 1); section.trim (); if (section == "[KILLED]") { chatType = 0; continue; } else if (section == "[BOMBPLANT]") { chatType = 1; continue; } else if (section == "[DEADCHAT]") { chatType = 2; continue; } else if (section == "[REPLIES]") { chatType = 3; continue; } else if (section == "[UNKNOWN]") { chatType = 4; continue; } else if (section == "[TEAMATTACK]") { chatType = 5; continue; } else if (section == "[WELCOME]") { chatType = 6; continue; } else if (section == "[TEAMKILL]") { chatType = 7; continue; } if (chatType != 3) { lineBuffer[79] = 0; } switch (chatType) { case 0: g_chatFactory[CHAT_KILLING].push (lineBuffer); break; case 1: g_chatFactory[CHAT_BOMBPLANT].push (lineBuffer); break; case 2: g_chatFactory[CHAT_DEAD].push (lineBuffer); break; case 3: if (strstr (lineBuffer, "@KEY") != nullptr) { if (!replies.keywords.empty () && !replies.replies.empty ()) { g_replyFactory.push (cr::forward (replies)); replies.replies.clear (); } replies.keywords.clear (); replies.keywords = String (&lineBuffer[4]).split (","); for (auto &keywords : replies.keywords) { keywords.trim ().trim ("\""); } } else if (!replies.keywords.empty ()) { replies.replies.push (lineBuffer); } break; case 4: g_chatFactory[CHAT_NOKW].push (lineBuffer); break; case 5: g_chatFactory[CHAT_TEAMATTACK].push (lineBuffer); break; case 6: g_chatFactory[CHAT_WELCOME].push (lineBuffer); break; case 7: g_chatFactory[CHAT_TEAMKILL].push (lineBuffer); break; } } fp.close (); } else { extern ConVar yb_chat; yb_chat.set (0); } // GENERAL DATA INITIALIZATION if (openConfig ("general.cfg", "General configuration file not found. Loading defaults", &fp)) { while (fp.gets (lineBuffer, 255)) { if (isCommentLine (lineBuffer)) { continue; } auto pair = String (lineBuffer).split ("="); if (pair.length () != 2) { continue; } for (auto &trim : pair) { trim.trim (); } auto splitted = pair[1].split (","); if (pair[0] == "MapStandard") { if (splitted.length () != NUM_WEAPONS) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < NUM_WEAPONS; i++) { g_weaponSelect[i].teamStandard = splitted[i].toInt32 (); } } else if (pair[0] == "MapAS") { if (splitted.length () != NUM_WEAPONS) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < NUM_WEAPONS; i++) { g_weaponSelect[i].teamAS = splitted[i].toInt32 (); } } else if (pair[0] == "GrenadePercent") { if (splitted.length () != 3) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < 3; i++) { g_grenadeBuyPrecent[i] = splitted[i].toInt32 (); } } else if (pair[0] == "Economics") { if (splitted.length () != 11) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < 11; i++) { g_botBuyEconomyTable[i] = splitted[i].toInt32 (); } } else if (pair[0] == "PersonalityNormal") { if (splitted.length () != NUM_WEAPONS) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < NUM_WEAPONS; i++) { g_normalWeaponPrefs[i] = splitted[i].toInt32 (); } } else if (pair[0] == "PersonalityRusher") { if (splitted.length () != NUM_WEAPONS) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < NUM_WEAPONS; i++) { g_rusherWeaponPrefs[i] = splitted[i].toInt32 (); } } else if (pair[0] == "PersonalityCareful") { if (splitted.length () != NUM_WEAPONS) { logEntry (true, LL_FATAL, "%s entry in general config is not valid.", pair[0].chars ()); } for (int i = 0; i < NUM_WEAPONS; i++) { g_carefulWeaponPrefs[i] = splitted[i].toInt32 (); } } } fp.close (); } // CHATTER SYSTEM INITIALIZATION if ((g_gameFlags & GAME_SUPPORT_BOT_VOICE) && yb_communication_type.integer () == 2 && openConfig ("chatter.cfg", "Couldn't open chatter system configuration", &fp)) { struct EventMap { const char *str; int code; float repeat; } chatterEventMap[] = { { "Radio_CoverMe", RADIO_COVER_ME, MAX_CHATTER_REPEAT }, { "Radio_YouTakePoint", RADIO_YOU_TAKE_THE_POINT, MAX_CHATTER_REPEAT }, { "Radio_HoldPosition", RADIO_HOLD_THIS_POSITION, 10.0f }, { "Radio_RegroupTeam", RADIO_REGROUP_TEAM, 10.0f }, { "Radio_FollowMe", RADIO_FOLLOW_ME, 15.0f }, { "Radio_TakingFire", RADIO_TAKING_FIRE, 5.0f }, { "Radio_GoGoGo", RADIO_GO_GO_GO, MAX_CHATTER_REPEAT }, { "Radio_Fallback", RADIO_TEAM_FALLBACK, MAX_CHATTER_REPEAT }, { "Radio_StickTogether", RADIO_STICK_TOGETHER_TEAM, MAX_CHATTER_REPEAT }, { "Radio_GetInPosition", RADIO_GET_IN_POSITION, MAX_CHATTER_REPEAT }, { "Radio_StormTheFront", RADIO_STORM_THE_FRONT, MAX_CHATTER_REPEAT }, { "Radio_ReportTeam", RADIO_REPORT_TEAM, MAX_CHATTER_REPEAT }, { "Radio_Affirmative", RADIO_AFFIRMATIVE, MAX_CHATTER_REPEAT }, { "Radio_EnemySpotted", RADIO_ENEMY_SPOTTED, 4.0f }, { "Radio_NeedBackup", RADIO_NEED_BACKUP, MAX_CHATTER_REPEAT }, { "Radio_SectorClear", RADIO_SECTOR_CLEAR, 10.0f }, { "Radio_InPosition", RADIO_IN_POSITION, 10.0f }, { "Radio_ReportingIn", RADIO_REPORTING_IN, MAX_CHATTER_REPEAT }, { "Radio_ShesGonnaBlow", RADIO_SHES_GONNA_BLOW, MAX_CHATTER_REPEAT }, { "Radio_Negative", RADIO_NEGATIVE, MAX_CHATTER_REPEAT }, { "Radio_EnemyDown", RADIO_ENEMY_DOWN, 10.0f }, { "Chatter_DiePain", CHATTER_PAIN_DIED, MAX_CHATTER_REPEAT }, { "Chatter_GoingToPlantBomb", CHATTER_GOING_TO_PLANT_BOMB, 5.0f }, { "Chatter_GoingToGuardVIPSafety", CHATTER_GOING_TO_GUARD_VIP_SAFETY, MAX_CHATTER_REPEAT }, { "Chatter_RescuingHostages", CHATTER_RESCUING_HOSTAGES, MAX_CHATTER_REPEAT }, { "Chatter_TeamKill", CHATTER_TEAM_ATTACK, MAX_CHATTER_REPEAT }, { "Chatter_GuardingVipSafety", CHATTER_GUARDING_VIP_SAFETY, MAX_CHATTER_REPEAT }, { "Chatter_PlantingC4", CHATTER_PLANTING_BOMB, 10.0f }, { "Chatter_InCombat", CHATTER_IN_COMBAT, MAX_CHATTER_REPEAT }, { "Chatter_SeeksEnemy", CHATTER_SEEK_ENEMY, MAX_CHATTER_REPEAT }, { "Chatter_Nothing", CHATTER_NOTHING, MAX_CHATTER_REPEAT }, { "Chatter_EnemyDown", CHATTER_ENEMY_DOWN, 10.0f }, { "Chatter_UseHostage", CHATTER_USING_HOSTAGES, MAX_CHATTER_REPEAT }, { "Chatter_WonTheRound", CHATTER_WON_THE_ROUND, MAX_CHATTER_REPEAT }, { "Chatter_QuicklyWonTheRound", CHATTER_QUICK_WON_ROUND, MAX_CHATTER_REPEAT }, { "Chatter_NoEnemiesLeft", CHATTER_NO_ENEMIES_LEFT, MAX_CHATTER_REPEAT }, { "Chatter_FoundBombPlace", CHATTER_FOUND_BOMB_PLACE, 15.0f }, { "Chatter_WhereIsTheBomb", CHATTER_WHERE_IS_THE_BOMB, MAX_CHATTER_REPEAT }, { "Chatter_DefendingBombSite", CHATTER_DEFENDING_BOMBSITE, MAX_CHATTER_REPEAT }, { "Chatter_BarelyDefused", CHATTER_BARELY_DEFUSED, MAX_CHATTER_REPEAT }, { "Chatter_NiceshotCommander", CHATTER_NICESHOT_COMMANDER, MAX_CHATTER_REPEAT }, { "Chatter_ReportingIn", CHATTER_REPORTING_IN, 10.0f }, { "Chatter_SpotTheBomber", CHATTER_SPOT_THE_BOMBER, 4.3f }, { "Chatter_VIPSpotted", CHATTER_VIP_SPOTTED, 5.3f }, { "Chatter_FriendlyFire", CHATTER_FRIENDLY_FIRE, 2.1f }, { "Chatter_GotBlinded", CHATTER_BLINDED, 5.0f }, { "Chatter_GuardDroppedC4", CHATTER_GUARDING_DROPPED_BOMB, 3.0f }, { "Chatter_DefusingC4", CHATTER_DEFUSING_BOMB, 3.0f }, { "Chatter_FoundC4", CHATTER_FOUND_BOMB, 5.5f }, { "Chatter_ScaredEmotion", CHATTER_SCARED_EMOTE, 6.1f }, { "Chatter_HeardEnemy", CHATTER_SCARED_EMOTE, 12.8f }, { "Chatter_SniperWarning", CHATTER_SNIPER_WARNING, 14.3f }, { "Chatter_SniperKilled", CHATTER_SNIPER_KILLED, 12.1f }, { "Chatter_OneEnemyLeft", CHATTER_ONE_ENEMY_LEFT, 12.5f }, { "Chatter_TwoEnemiesLeft", CHATTER_TWO_ENEMIES_LEFT, 12.5f }, { "Chatter_ThreeEnemiesLeft", CHATTER_THREE_ENEMIES_LEFT, 12.5f }, { "Chatter_NiceshotPall", CHATTER_NICESHOT_PALL, 2.0f }, { "Chatter_GoingToGuardHostages", CHATTER_GOING_TO_GUARD_HOSTAGES, 3.0f }, { "Chatter_GoingToGuardDoppedBomb", CHATTER_GOING_TO_GUARD_DROPPED_BOMB, 6.0f }, { "Chatter_OnMyWay", CHATTER_ON_MY_WAY, 1.5f }, { "Chatter_LeadOnSir", CHATTER_LEAD_ON_SIR, 5.0f }, { "Chatter_Pinned_Down", CHATTER_PINNED_DOWN, 5.0f }, { "Chatter_GottaFindTheBomb", CHATTER_GOTTA_FIND_BOMB, 3.0f }, { "Chatter_You_Heard_The_Man", CHATTER_YOU_HEARD_THE_MAN, 3.0f }, { "Chatter_Lost_The_Commander", CHATTER_LOST_COMMANDER, 4.5f }, { "Chatter_NewRound", CHATTER_NEW_ROUND, 3.5f }, { "Chatter_CoverMe", CHATTER_COVER_ME, 3.5f }, { "Chatter_BehindSmoke", CHATTER_BEHIND_SMOKE, 3.5f }, { "Chatter_BombSiteSecured", CHATTER_BOMB_SITE_SECURED, 3.5f }, { "Chatter_GoingToCamp", CHATTER_GOING_TO_CAMP, 25.0f }, { "Chatter_Camp", CHATTER_CAMP, 25.0f }, }; while (fp.gets (lineBuffer, 511)) { if (isCommentLine (lineBuffer)) { continue; } if (strncmp (lineBuffer, "RewritePath", 11) == 0) { extern ConVar yb_chatter_path; yb_chatter_path.set (String (&lineBuffer[12]).trim ().chars ()); } else if (strncmp (lineBuffer, "Event", 5) == 0) { auto items = String (&lineBuffer[6]).split ("="); if (items.length () != 2) { logEntry (true, LL_ERROR, "Error in chatter config file syntax... Please correct all errors."); continue; } for (auto &item : items) { item.trim (); } items[1].trim ("(;)"); for (size_t i = 0; i < cr::arrsize (chatterEventMap); i++) { auto event = &chatterEventMap[i]; if (stricmp (event->str, items[0].chars ()) == 0) { // this does common work of parsing comma-separated chatter line auto sounds = items[1].split (","); for (auto &sound : sounds) { sound.trim ().trim ("\""); float duration = engine.getWaveLen (sound.chars ()); if (duration > 0.0f) { g_chatterFactory[event->code].push ({ sound, event->repeat, duration }); } } sounds.clear (); } } } } fp.close (); } else { yb_communication_type.set (1); logEntry (true, LL_DEFAULT, "Chatter Communication disabled."); } // LOCALIZER INITITALIZATION if (!(g_gameFlags & GAME_LEGACY) && openConfig ("lang.cfg", "Specified language not found", &fp, true)) { if (engine.isDedicated ()) { return; // dedicated server will use only english translation } enum Lang { LANG_ORIGINAL, LANG_TRANSLATED, LANG_UNDEFINED } langState = static_cast (LANG_UNDEFINED); char buffer[MAX_PRINT_BUFFER] = { 0, }; Pair lang; while (fp.gets (lineBuffer, 255)) { if (isCommentLine (lineBuffer)) { continue; } if (strncmp (lineBuffer, "[ORIGINAL]", 10) == 0) { langState = LANG_ORIGINAL; if (buffer[0] != 0) { lang.second = buffer; lang.second.trim (); buffer[0] = 0; } if (!lang.second.empty () && !lang.first.empty ()) { engine.addTranslation (lang.first, lang.second); } } else if (strncmp (lineBuffer, "[TRANSLATED]", 12) == 0) { lang.first = buffer; lang.first.trim (); langState = LANG_TRANSLATED; buffer[0] = 0; } else { switch (langState) { case LANG_ORIGINAL: strncat (buffer, lineBuffer, MAX_PRINT_BUFFER - 1 - strlen (buffer)); break; case LANG_TRANSLATED: strncat (buffer, lineBuffer, MAX_PRINT_BUFFER - 1 - strlen (buffer)); break; case LANG_UNDEFINED: break; } } } fp.close (); } else if (g_gameFlags & GAME_LEGACY) { logEntry (true, LL_DEFAULT, "Multilingual system disabled, due to your Counter-Strike Version!"); } else if (strcmp (yb_language.str (), "en") != 0) { logEntry (true, LL_ERROR, "Couldn't load language configuration"); } // set personality weapon pointers here g_weaponPrefs[PERSONALITY_NORMAL] = reinterpret_cast (&g_normalWeaponPrefs); g_weaponPrefs[PERSONALITY_RUSHER] = reinterpret_cast (&g_rusherWeaponPrefs); g_weaponPrefs[PERSONALITY_CAREFUL] = reinterpret_cast (&g_carefulWeaponPrefs); g_timePerSecondUpdate = 0.0f; } void GameDLLInit (void) { // this function is a one-time call, and appears to be the second function called in the // DLL after GiveFntprsToDll() has been called. Its purpose is to tell the MOD DLL to // initialize the game before the engine actually hooks into it with its video frames and // clients connecting. Note that it is a different step than the *server* initialization. // This one is called once, and only once, when the game process boots up before the first // server is enabled. Here is a good place to do our own game session initialization, and // to register by the engine side the server commands we need to administrate our bots. auto commandHandler = [](void) { if (handleBotCommands (g_hostEntity, isEmptyStr (g_engfuncs.pfnCmd_Argv (1)) ? "help" : g_engfuncs.pfnCmd_Argv (1), g_engfuncs.pfnCmd_Argv (2), g_engfuncs.pfnCmd_Argv (3), g_engfuncs.pfnCmd_Argv (4), g_engfuncs.pfnCmd_Argv (5), g_engfuncs.pfnCmd_Argv (6), g_engfuncs.pfnCmd_Argv (0)) == 0) { engine.print ("Unknown command: %s", g_engfuncs.pfnCmd_Argv (1)); } }; // register server command(s) engine.registerCmd ("yapb", commandHandler); engine.registerCmd ("yb", commandHandler); // set correct version string yb_version.set (format ("%d.%d.%d", PRODUCT_VERSION_DWORD_INTERNAL, buildNumber ())); // execute main config execBotConfigs (true); // register fake metamod command handler if we not! under mm if (!(g_gameFlags & GAME_METAMOD)) { engine.registerCmd ("meta", [](void) { engine.print ("You're launched standalone version of yapb. Metamod is not installed or not enabled!"); }); } // elite price is 1000$ on older versions of cs... if (g_gameFlags & GAME_LEGACY) { for (int i = 0; i < NUM_WEAPONS; i++) { auto &weapon = g_weaponSelect[i]; if (weapon.id == WEAPON_ELITE) { weapon.price = 1000; break; } } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnGameInit (); } void Touch (edict_t *pentTouched, edict_t *pentOther) { // this function is called when two entities' bounding boxes enter in collision. For example, // when a player walks upon a gun, the player entity bounding box collides to the gun entity // bounding box, and the result is that this function is called. It is used by the game for // taking the appropriate action when such an event occurs (in our example, the player who // is walking upon the gun will "pick it up"). Entities that "touch" others are usually // entities having a velocity, as it is assumed that static entities (entities that don't // move) will never touch anything. Hence, in our example, the pentTouched will be the gun // (static entity), whereas the pentOther will be the player (as it is the one moving). When // the two entities both have velocities, for example two players colliding, this function // is called twice, once for each entity moving. if (!engine.isNullEntity (pentTouched) && (pentTouched->v.flags & FL_FAKECLIENT) && pentOther != engine.getStartEntity ()) { Bot *bot = bots.getBot (pentTouched); if (bot != nullptr && pentOther != bot->ent ()) { if (isPlayer (pentOther) && isAlive (pentOther)) { bot->avoidIncomingPlayers (pentOther); } else { bot->processBreakables (pentOther); } } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnTouch (pentTouched, pentOther); } int Spawn (edict_t *ent) { // this function asks the game DLL to spawn (i.e, give a physical existence in the virtual // world, in other words to 'display') the entity pointed to by ent in the game. The // Spawn() function is one of the functions any entity is supposed to have in the game DLL, // and any MOD is supposed to implement one for each of its entities. engine.precache (); if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, 0); } int result = g_functionTable.pfnSpawn (ent); // get result if (ent->v.rendermode == kRenderTransTexture) { ent->v.flags &= ~FL_WORLDBRUSH; // clear the FL_WORLDBRUSH flag out of transparent ents } return result; } void UpdateClientData (const struct edict_s *ent, int sendweapons, struct clientdata_s *cd) { extern ConVar yb_latency_display; if ((g_gameFlags & GAME_SUPPORT_SVC_PINGS) && yb_latency_display.integer () == 2) { bots.sendPingOffsets (const_cast (ent)); } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnUpdateClientData (ent, sendweapons, cd); } int ClientConnect (edict_t *ent, const char *name, const char *addr, char rejectReason[128]) { // this function is called in order to tell the MOD DLL that a client attempts to connect the // game. The entity pointer of this client is ent, the name under which he connects is // pointed to by the pszName pointer, and its IP address string is pointed by the pszAddress // one. Note that this does not mean this client will actually join the game ; he could as // well be refused connection by the server later, because of latency timeout, unavailable // game resources, or whatever reason. In which case the reason why the game DLL (read well, // the game DLL, *NOT* the engine) refuses this player to connect will be printed in the // rejectReason string in all letters. Understand that a client connecting process is done // in three steps. First, the client requests a connection from the server. This is engine // internals. When there are already too many players, the engine will refuse this client to // connect, and the game DLL won't even notice. Second, if the engine sees no problem, the // game DLL is asked. This is where we are. Once the game DLL acknowledges the connection, // the client downloads the resources it needs and synchronizes its local engine with the one // of the server. And then, the third step, which comes *AFTER* ClientConnect (), is when the // client officially enters the game, through the ClientPutInServer () function, later below. // Here we hook this function in order to keep track of the listen server client entity, // because a listen server client always connects with a "loopback" address string. Also we // tell the bot manager to check the bot population, in order to always have one free slot on // the server for incoming clients. // check if this client is the listen server client if (strcmp (addr, "loopback") == 0) { g_hostEntity = ent; // save the edict of the listen server client... } if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, 0); } return g_functionTable.pfnClientConnect (ent, name, addr, rejectReason); } void ClientDisconnect (edict_t *ent) { // this function is called whenever a client is VOLUNTARILY disconnected from the server, // either because the client dropped the connection, or because the server dropped him from // the game (latency timeout). The effect is the freeing of a client slot on the server. Note // that clients and bots disconnected because of a level change NOT NECESSARILY call this // function, because in case of a level change, it's a server shutdown, and not a normal // disconnection. I find that completely stupid, but that's it. Anyway it's time to update // the bots and players counts, and in case the client disconnecting is a bot, to back its // brain(s) up to disk. We also try to notice when a listenserver client disconnects, so as // to reset his entity pointer for safety. There are still a few server frames to go once a // listen server client disconnects, and we don't want to send him any sort of message then. int index = engine.indexOfEntity (ent) - 1; if (index >= 0 && index < MAX_ENGINE_PLAYERS) { auto bot = bots.getBot (index); // check if its a bot if (bot != nullptr && bot->pev == &ent->v) { bot->showChaterIcon (false); bots.destroy (index); } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnClientDisconnect (ent); } void ClientUserInfoChanged (edict_t *ent, char *infobuffer) { // this function is called when a player changes model, or changes team. Occasionally it // enforces rules on these changes (for example, some MODs don't want to allow players to // change their player model). But most commonly, this function is in charge of handling // team changes, recounting the teams population, etc... if (engine.isDedicated () && !isFakeClient (ent)) { const String &key = yb_password_key.str (); const String &password = yb_password.str (); if (!key.empty () && !password.empty ()) { int clientIndex = engine.indexOfEntity (ent) - 1; if (password == g_engfuncs.pfnInfoKeyValue (infobuffer, key.chars ())) { g_clients[clientIndex].flags |= CF_ADMIN; } else { g_clients[clientIndex].flags &= ~CF_ADMIN; } } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnClientUserInfoChanged (ent, infobuffer); } void ClientCommand (edict_t *ent) { // this function is called whenever the client whose player entity is ent issues a client // command. How it works is that clients all have a global string in their client DLL that // stores the command string; if ever that string is filled with characters, the client DLL // sends it to the engine as a command to be executed. When the engine has executed that // command, that string is reset to zero. By the server side, we can access this string // by asking the engine with the CmdArgv(), CmdArgs() and CmdArgc() functions that work just // like executable files argument processing work in C (argc gets the number of arguments, // command included, args returns the whole string, and argv returns the wanted argument // only). Here is a good place to set up either bot debug commands the listen server client // could type in his game console, or real new client commands, but we wouldn't want to do // so as this is just a bot DLL, not a MOD. The purpose is not to add functionality to // clients. Hence it can lack of commenting a bit, since this code is very subject to change. const char *command = g_engfuncs.pfnCmd_Argv (0); const char *arg1 = g_engfuncs.pfnCmd_Argv (1); static int fillServerTeam = 5; static bool fillCommand = false; int issuerPlayerIndex = engine.indexOfEntity (ent) - 1; if (!engine.isBotCmd () && (ent == g_hostEntity || (g_clients[issuerPlayerIndex].flags & CF_ADMIN))) { if (stricmp (command, "yapb") == 0 || stricmp (command, "yb") == 0) { int state = handleBotCommands (ent, isEmptyStr (arg1) ? "help" : arg1, g_engfuncs.pfnCmd_Argv (2), g_engfuncs.pfnCmd_Argv (3), g_engfuncs.pfnCmd_Argv (4), g_engfuncs.pfnCmd_Argv (5), g_engfuncs.pfnCmd_Argv (6), g_engfuncs.pfnCmd_Argv (0)); switch (state) { case 0: engine.clientPrint (ent, "Unknown command: %s", arg1); break; case 2: engine.clientPrint (ent, "Command \"%s\", can only be executed from server console.", arg1); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (stricmp (command, "menuselect") == 0 && !isEmptyStr (arg1) && g_clients[issuerPlayerIndex].menu != BOT_MENU_INVALID) { Client *client = &g_clients[issuerPlayerIndex]; int selection = atoi (arg1); if (client->menu == BOT_MENU_WAYPOINT_TYPE) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: waypoints.push (selection - 1); showMenu (ent, BOT_MENU_WAYPOINT_TYPE); break; case 8: waypoints.push (100); showMenu (ent, BOT_MENU_WAYPOINT_TYPE); break; case 9: waypoints.startLearnJump (); showMenu (ent, BOT_MENU_WAYPOINT_TYPE); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_FLAG) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: waypoints.toggleFlags (FLAG_NOHOSTAGE); showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; case 2: waypoints.toggleFlags (FLAG_TF_ONLY); showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; case 3: waypoints.toggleFlags (FLAG_CF_ONLY); showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; case 4: waypoints.toggleFlags (FLAG_LIFT); showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; case 5: waypoints.toggleFlags (FLAG_SNIPER); showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_MAIN_PAGE1) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: if (g_waypointOn) engine.execCmd ("yapb waypoint off"); else engine.execCmd ("yapb waypoint on"); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE1); break; case 2: g_waypointOn = true; waypoints.cachePoint (); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE1); break; case 3: g_waypointOn = true; showMenu (ent, BOT_MENU_WAYPOINT_PATH); break; case 4: g_waypointOn = true; waypoints.erasePath (); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE1); break; case 5: g_waypointOn = true; showMenu (ent, BOT_MENU_WAYPOINT_TYPE); break; case 6: g_waypointOn = true; waypoints.erase (INVALID_WAYPOINT_INDEX); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE1); break; case 7: g_waypointOn = true; showMenu (ent, BOT_MENU_WAYPOINT_AUTOPATH); break; case 8: g_waypointOn = true; showMenu (ent, BOT_MENU_WAYPOINT_RADIUS); break; case 9: showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_MAIN_PAGE2) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: { int terrPoints = 0; int ctPoints = 0; int goalPoints = 0; int rescuePoints = 0; int campPoints = 0; int sniperPoints = 0; int noHostagePoints = 0; for (int i = 0; i < waypoints.length (); i++) { Path &path = waypoints[i]; if (path.flags & FLAG_TF_ONLY) { terrPoints++; } if (path.flags & FLAG_CF_ONLY) { ctPoints++; } if (path.flags & FLAG_GOAL) { goalPoints++; } if (path.flags & FLAG_RESCUE) { rescuePoints++; } if (path.flags & FLAG_CAMP) { campPoints++; } if (path.flags & FLAG_SNIPER) { sniperPoints++; } if (path.flags & FLAG_NOHOSTAGE) { noHostagePoints++; } } engine.print ("Waypoints: %d - T Points: %d\n" "CT Points: %d - Goal Points: %d\n" "Rescue Points: %d - Camp Points: %d\n" "Block Hostage Points: %d - Sniper Points: %d\n", waypoints.length (), terrPoints, ctPoints, goalPoints, rescuePoints, campPoints, noHostagePoints, sniperPoints); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); } break; case 2: g_waypointOn = true; g_autoWaypoint &= 1; g_autoWaypoint ^= 1; engine.centerPrint ("Auto-Waypoint %s", g_autoWaypoint ? "Enabled" : "Disabled"); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 3: g_waypointOn = true; showMenu (ent, BOT_MENU_WAYPOINT_FLAG); break; case 4: if (waypoints.checkNodes ()) { waypoints.save (); } else { engine.centerPrint ("Waypoint not saved\nThere are errors, see console"); } showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 5: waypoints.save (); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 6: waypoints.load (); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 7: if (waypoints.checkNodes ()) { engine.centerPrint ("Nodes works fine"); } else { engine.centerPrint ("There are errors, see console"); } showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 8: engine.execCmd ("yapb wp on noclip"); showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE2); break; case 9: showMenu (ent, BOT_MENU_WAYPOINT_MAIN_PAGE1); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_RADIUS) { showMenu (ent, BOT_MENU_INVALID); // reset menu display g_waypointOn = true; // turn waypoints on in case const int radiusValue[] = {0, 8, 16, 32, 48, 64, 80, 96, 128}; if (selection >= 1 && selection <= 9) { waypoints.setRadius (radiusValue[selection - 1]); showMenu (ent, BOT_MENU_WAYPOINT_RADIUS); } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_MAIN) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: fillCommand = false; showMenu (ent, BOT_MENU_CONTROL); break; case 2: showMenu (ent, BOT_MENU_FEATURES); break; case 3: fillCommand = true; showMenu (ent, BOT_MENU_TEAM_SELECT); break; case 4: bots.killAllBots (); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; default: showMenu (ent, BOT_MENU_MAIN); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_CONTROL) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: bots.createRandom (true); showMenu (ent, BOT_MENU_CONTROL); break; case 2: showMenu (ent, BOT_MENU_DIFFICULTY); break; case 3: bots.kickRandom (); showMenu (ent, BOT_MENU_CONTROL); break; case 4: bots.kickEveryone (); break; case 5: bots.kickBotByMenu (ent, 1); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_FEATURES) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: showMenu (ent, BOT_MENU_WEAPON_MODE); break; case 2: showMenu (ent, engine.isDedicated () ? BOT_MENU_FEATURES : BOT_MENU_WAYPOINT_MAIN_PAGE1); break; case 3: showMenu (ent, BOT_MENU_PERSONALITY); break; case 4: extern ConVar yb_debug; yb_debug.set (yb_debug.integer () ^ 1); showMenu (ent, BOT_MENU_FEATURES); break; case 5: if (isAlive (ent)) { showMenu (ent, BOT_MENU_COMMANDS); } else { showMenu (ent, BOT_MENU_INVALID); // reset menu display engine.centerPrint ("You're dead, and have no access to this menu"); } break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_COMMANDS) { showMenu (ent, BOT_MENU_INVALID); // reset menu display Bot *bot = nullptr; switch (selection) { case 1: case 2: if (findNearestPlayer (reinterpret_cast (&bot), client->ent, 600.0f, true, true, true)) { if (!bot->m_hasC4 && !bot->hasHostage ()) { if (selection == 1) { bot->startDoubleJump (client->ent); } else { bot->resetDoubleJump (); } } } showMenu (ent, BOT_MENU_COMMANDS); break; case 3: case 4: if (findNearestPlayer (reinterpret_cast (&bot), ent, 600.0f, true, true, true, true, selection == 4 ? false : true)) { bot->dropWeaponForUser (ent, selection == 4 ? false : true); } showMenu (ent, BOT_MENU_COMMANDS); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_AUTOPATH) { showMenu (ent, BOT_MENU_INVALID); // reset menu display const float autoDistanceValue[] = {0.0f, 100.0f, 130.0f, 160.0f, 190.0f, 220.0f, 250.0f}; if (selection >= 1 && selection <= 7) { g_autoPathDistance = autoDistanceValue[selection - 1]; } if (g_autoPathDistance == 0.0f) { engine.centerPrint ("AutoPath disabled"); } else { engine.centerPrint ("AutoPath maximum distance set to %.2f", g_autoPathDistance); } showMenu (ent, BOT_MENU_WAYPOINT_AUTOPATH); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WAYPOINT_PATH) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: waypoints.pathCreate (CONNECTION_OUTGOING); showMenu (ent, BOT_MENU_WAYPOINT_PATH); break; case 2: waypoints.pathCreate (CONNECTION_INCOMING); showMenu (ent, BOT_MENU_WAYPOINT_PATH); break; case 3: waypoints.pathCreate (CONNECTION_BOTHWAYS); showMenu (ent, BOT_MENU_WAYPOINT_PATH); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_DIFFICULTY) { showMenu (ent, BOT_MENU_INVALID); // reset menu display client->menu = BOT_MENU_PERSONALITY; switch (selection) { case 1: g_storeAddbotVars[0] = 0; break; case 2: g_storeAddbotVars[0] = 1; break; case 3: g_storeAddbotVars[0] = 2; break; case 4: g_storeAddbotVars[0] = 3; break; case 5: g_storeAddbotVars[0] = 4; break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } showMenu (ent, BOT_MENU_PERSONALITY); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_TEAM_SELECT && fillCommand) { showMenu (ent, BOT_MENU_INVALID); // reset menu display if (selection < 3) { extern ConVar mp_limitteams; extern ConVar mp_autoteambalance; // turn off cvars if specified team mp_limitteams.set (0); mp_autoteambalance.set (0); } switch (selection) { case 1: case 2: case 5: fillServerTeam = selection; showMenu (ent, BOT_MENU_DIFFICULTY); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_PERSONALITY && fillCommand) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: bots.serverFill (fillServerTeam, selection - 2, g_storeAddbotVars[0]); showMenu (ent, BOT_MENU_INVALID); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_TEAM_SELECT) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 5: g_storeAddbotVars[1] = selection; if (selection == 5) { g_storeAddbotVars[2] = 5; bots.addbot ("", g_storeAddbotVars[0], g_storeAddbotVars[3], g_storeAddbotVars[1], g_storeAddbotVars[2], true); } else { if (selection == 1) { showMenu (ent, BOT_MENU_TERRORIST_SELECT); } else { showMenu (ent, BOT_MENU_CT_SELECT); } } break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_PERSONALITY) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: g_storeAddbotVars[3] = selection - 2; showMenu (ent, BOT_MENU_TEAM_SELECT); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_TERRORIST_SELECT || client->menu == BOT_MENU_CT_SELECT) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: g_storeAddbotVars[2] = selection; bots.addbot ("", g_storeAddbotVars[0], g_storeAddbotVars[3], g_storeAddbotVars[1], g_storeAddbotVars[2], true); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_WEAPON_MODE) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: bots.setWeaponMode (selection); showMenu (ent, BOT_MENU_WEAPON_MODE); break; case 10: showMenu (ent, BOT_MENU_INVALID); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_KICK_PAGE_1) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: bots.kickBot (selection - 1); bots.kickBotByMenu (ent, 1); break; case 9: bots.kickBotByMenu (ent, 2); break; case 10: showMenu (ent, BOT_MENU_CONTROL); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_KICK_PAGE_2) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: bots.kickBot (selection + 8 - 1); bots.kickBotByMenu (ent, 2); break; case 9: bots.kickBotByMenu (ent, 3); break; case 10: bots.kickBotByMenu (ent, 1); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_KICK_PAGE_3) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: bots.kickBot (selection + 16 - 1); bots.kickBotByMenu (ent, 3); break; case 9: bots.kickBotByMenu (ent, 4); break; case 10: bots.kickBotByMenu (ent, 2); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } else if (client->menu == BOT_MENU_KICK_PAGE_4) { showMenu (ent, BOT_MENU_INVALID); // reset menu display switch (selection) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: bots.kickBot (selection + 24 - 1); bots.kickBotByMenu (ent, 4); break; case 10: bots.kickBotByMenu (ent, 3); break; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } } } if (!engine.isBotCmd () && (stricmp (command, "say") == 0 || stricmp (command, "say_team") == 0)) { Bot *bot = nullptr; if (strcmp (arg1, "dropme") == 0 || strcmp (arg1, "dropc4") == 0) { if (findNearestPlayer (reinterpret_cast (&bot), ent, 300.0f, true, true, true)) { bot->dropWeaponForUser (ent, isEmptyStr (strstr (arg1, "c4")) ? false : true); } return; } bool alive = isAlive (ent); int team = -1; if (strcmp (command, "say_team") == 0) { team = engine.getTeam (ent); } for (int i = 0; i < engine.maxClients (); i++) { const Client &client = g_clients[i]; if (!(client.flags & CF_USED) || (team != -1 && team != client.team) || alive != isAlive (client.ent)) { continue; } Bot *target = bots.getBot (i); if (target != nullptr) { target->m_sayTextBuffer.entityIndex = engine.indexOfEntity (ent); if (isEmptyStr (g_engfuncs.pfnCmd_Args ())) { continue; } target->m_sayTextBuffer.sayText = g_engfuncs.pfnCmd_Args (); target->m_sayTextBuffer.timeNextChat = engine.timebase () + target->m_sayTextBuffer.chatDelay; } } } int clientIndex = engine.indexOfEntity (ent) - 1; const Client &radioTarget = g_clients[clientIndex]; // check if this player alive, and issue something if ((radioTarget.flags & CF_ALIVE) && g_radioSelect[clientIndex] != 0 && strncmp (command, "menuselect", 10) == 0) { int radioCommand = atoi (arg1); if (radioCommand != 0) { radioCommand += 10 * (g_radioSelect[clientIndex] - 1); if (radioCommand != RADIO_AFFIRMATIVE && radioCommand != RADIO_NEGATIVE && radioCommand != RADIO_REPORTING_IN) { for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); // validate bot if (bot != nullptr && bot->m_team == radioTarget.team && ent != bot->ent () && bot->m_radioOrder == 0) { bot->m_radioOrder = radioCommand; bot->m_radioEntity = ent; } } } g_lastRadioTime[radioTarget.team] = engine.timebase (); } g_radioSelect[clientIndex] = 0; } else if (strncmp (command, "radio", 5) == 0) { g_radioSelect[clientIndex] = atoi (&command[5]); } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnClientCommand (ent); } void ServerActivate (edict_t *pentEdictList, int edictCount, int clientMax) { // this function is called when the server has fully loaded and is about to manifest itself // on the network as such. Since a mapchange is actually a server shutdown followed by a // restart, this function is also called when a new map is being loaded. Hence it's the // perfect place for doing initialization stuff for our bots, such as reading the BSP data, // loading the bot profiles, and drawing the world map (ie, filling the navigation hashtable). // Once this function has been called, the server can be considered as "running". cleanupGarbage (); execBotConfigs (false); // initialize all config files // do a level initialization engine.levelInitialize (); // do level initialization stuff here... waypoints.init (); waypoints.load (); // execute main config execBotConfigs (true); if (File::exists (format ("%s/maps/%s_yapb.cfg", engine.getModName (), engine.getMapName ()))) { engine.execCmd ("exec maps/%s_yapb.cfg", engine.getMapName ()); engine.print ("Executing Map-Specific config file"); } bots.initQuota (); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnServerActivate (pentEdictList, edictCount, clientMax); waypoints.rebuildVisibility (); } void ServerDeactivate (void) { // this function is called when the server is shutting down. A particular note about map // changes: changing the map means shutting down the server and starting a new one. Of course // this process is transparent to the user, but either in single player when the hero reaches // a new level and in multiplayer when it's time for a map change, be aware that what happens // is that the server actually shuts down and restarts with a new map. Hence we can use this // function to free and deinit anything which is map-specific, for example we free the memory // space we m'allocated for our BSP data, since a new map means new BSP data to interpret. In // any case, when the new map will be booting, ServerActivate() will be called, so we'll do // the loading of new bots and the new BSP data parsing there. // save collected experience on shutdown waypoints.saveExperience (); waypoints.saveVisibility (); // destroy global killer entity bots.destroyKillerEntity (); // set state to unprecached engine.setUnprecached (); // xash is not kicking fakeclients on changelevel if (g_gameFlags & GAME_XASH_ENGINE) { bots.kickEveryone (true, false); bots.destroy (); } cleanupGarbage (); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnServerDeactivate (); } void StartFrame (void) { // this function starts a video frame. It is called once per video frame by the engine. If // you run Half-Life at 90 fps, this function will then be called 90 times per second. By // placing a hook on it, we have a good place to do things that should be done continuously // during the game, for example making the bots think (yes, because no Think() function exists // for the bots by the MOD side, remember). Also here we have control on the bot population, // for example if a new player joins the server, we should disconnect a bot, and if the // player population decreases, we should fill the server with other bots. // run periodic update of bot states bots.frame (); // record some stats of all players on the server for (int i = 0; i < engine.maxClients (); i++) { edict_t *player = engine.entityOfIndex (i + 1); Client &client = g_clients[i]; if (!engine.isNullEntity (player) && (player->v.flags & FL_CLIENT)) { client.ent = player; client.flags |= CF_USED; if (isAlive (player)) { client.flags |= CF_ALIVE; } else { client.flags &= ~CF_ALIVE; } if (client.flags & CF_ALIVE) { // keep the clipping mode enabled, or it can be turned off after new round has started if (g_hostEntity == player && g_editNoclip) { g_hostEntity->v.movetype = MOVETYPE_NOCLIP; } client.origin = player->v.origin; simulateSoundUpdates (i); } } else { client.flags &= ~(CF_USED | CF_ALIVE); client.ent = nullptr; } } if (g_waypointOn && !engine.isDedicated () && !engine.isNullEntity (g_hostEntity)) { waypoints.frame (); } bots.updateDeathMsgState (false); if (g_timePerSecondUpdate < engine.timebase ()) { checkWelcome (); for (int i = 0; i < engine.maxClients (); i++) { edict_t *player = engine.entityOfIndex (i + 1); // code below is executed only on dedicated server if (engine.isDedicated () && !engine.isNullEntity (player) && (player->v.flags & FL_CLIENT) && !(player->v.flags & FL_FAKECLIENT)) { Client &client = g_clients[i]; if (client.flags & CF_ADMIN) { if (isEmptyStr (yb_password_key.str ()) && isEmptyStr (yb_password.str ())) { client.flags &= ~CF_ADMIN; } else if (!!strcmp (yb_password.str (), g_engfuncs.pfnInfoKeyValue (g_engfuncs.pfnGetInfoKeyBuffer (client.ent), const_cast (yb_password_key.str ())))) { client.flags &= ~CF_ADMIN; engine.print ("Player %s had lost remote access to yapb.", STRING (player->v.netname)); } } else if (!(client.flags & CF_ADMIN) && !isEmptyStr (yb_password_key.str ()) && !isEmptyStr (yb_password.str ())) { if (strcmp (yb_password.str (), g_engfuncs.pfnInfoKeyValue (g_engfuncs.pfnGetInfoKeyBuffer (client.ent), const_cast (yb_password_key.str ()))) == 0) { client.flags |= CF_ADMIN; engine.print ("Player %s had gained full remote access to yapb.", STRING (player->v.netname)); } } } } bots.calculatePingOffsets (); if (g_gameFlags & GAME_METAMOD) { static auto dmActive = g_engfuncs.pfnCVarGetPointer ("csdm_active"); static auto freeForAll = g_engfuncs.pfnCVarGetPointer ("mp_freeforall"); if (dmActive && freeForAll) { if (dmActive->value > 0.0f) { g_gameFlags |= GAME_CSDM; } else if (g_gameFlags & GAME_CSDM) { g_gameFlags &= ~GAME_CSDM; } if (freeForAll->value > 0.0f) { g_gameFlags |= GAME_CSDM_FFA; } else if (g_gameFlags & GAME_CSDM_FFA) { g_gameFlags &= ~GAME_CSDM_FFA; } } } g_timePerSecondUpdate = engine.timebase () + 1.0f; } if (bots.getBotCount () > 0) { // keep track of grenades on map bots.updateActiveGrenade (); // keep track of intresting entities bots.updateIntrestingEntities (); } // keep bot number up to date bots.maintainQuota (); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_functionTable.pfnStartFrame (); // **** AI EXECUTION STARTS **** bots.framePeriodic (); // **** AI EXECUTION FINISH **** } void StartFrame_Post (void) { // this function starts a video frame. It is called once per video frame by the engine. If // you run Half-Life at 90 fps, this function will then be called 90 times per second. By // placing a hook on it, we have a good place to do things that should be done continuously // during the game, for example making the bots think (yes, because no Think() function exists // for the bots by the MOD side, remember). Post version called only by metamod. // **** AI EXECUTION STARTS **** bots.framePeriodic (); // **** AI EXECUTION FINISH **** RETURN_META (MRES_IGNORED); } int Spawn_Post (edict_t *ent) { // this function asks the game DLL to spawn (i.e, give a physical existence in the virtual // world, in other words to 'display') the entity pointed to by ent in the game. The // Spawn() function is one of the functions any entity is supposed to have in the game DLL, // and any MOD is supposed to implement one for each of its entities. Post version called // only by metamod. // solves the bots unable to see through certain types of glass bug. if (ent->v.rendermode == kRenderTransTexture) { ent->v.flags &= ~FL_WORLDBRUSH; // clear the FL_WORLDBRUSH flag out of transparent ents } RETURN_META_VALUE (MRES_IGNORED, 0); } void ServerActivate_Post (edict_t *, int, int) { // this function is called when the server has fully loaded and is about to manifest itself // on the network as such. Since a mapchange is actually a server shutdown followed by a // restart, this function is also called when a new map is being loaded. Hence it's the // perfect place for doing initialization stuff for our bots, such as reading the BSP data, // loading the bot profiles, and drawing the world map (ie, filling the navigation hashtable). // Once this function has been called, the server can be considered as "running". Post version // called only by metamod. waypoints.rebuildVisibility (); RETURN_META (MRES_IGNORED); } void pfnChangeLevel (char *s1, char *s2) { // the purpose of this function is to ask the engine to shutdown the server and restart a // new one running the map whose name is s1. It is used ONLY IN SINGLE PLAYER MODE and is // transparent to the user, because it saves the player state and equipment and restores it // back in the new level. The "changelevel trigger point" in the old level is linked to the // new level's spawn point using the s2 string, which is formatted as follows: "trigger_name // to spawnpoint_name", without spaces (for example, "tr_1atotr_2lm" would tell the engine // the player has reached the trigger point "tr_1a" and has to spawn in the next level on the // spawn point named "tr_2lm". // save collected experience on map change waypoints.saveExperience (); waypoints.saveVisibility (); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnChangeLevel (s1, s2); } edict_t *pfnFindEntityByString (edict_t *edictStartSearchAfter, const char *field, const char *value) { // round starts in counter-strike 1.5 if ((g_gameFlags & GAME_LEGACY) && strcmp (value, "info_map_parameters") == 0) { initRound (); } if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, 0); } return g_engfuncs.pfnFindEntityByString (edictStartSearchAfter, field, value); } void pfnEmitSound (edict_t *entity, int channel, const char *sample, float volume, float attenuation, int flags, int pitch) { // this function tells the engine that the entity pointed to by "entity", is emitting a sound // which fileName is "sample", at level "channel" (CHAN_VOICE, etc...), with "volume" as // loudness multiplicator (normal volume VOL_NORM is 1.0), with a pitch of "pitch" (normal // pitch PITCH_NORM is 100.0), and that this sound has to be attenuated by distance in air // according to the value of "attenuation" (normal attenuation ATTN_NORM is 0.8 ; ATTN_NONE // means no attenuation with distance). Optionally flags "fFlags" can be passed, which I don't // know the heck of the purpose. After we tell the engine to emit the sound, we have to call // SoundAttachToThreat() to bring the sound to the ears of the bots. Since bots have no client DLL // to handle this for them, such a job has to be done manually. attachSoundsToClients (entity, sample, volume); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnEmitSound (entity, channel, sample, volume, attenuation, flags, pitch); } void pfnClientCommand (edict_t *ent, char const *format, ...) { // this function forces the client whose player entity is ent to issue a client command. // How it works is that clients all have a g_xgv global string in their client DLL that // stores the command string; if ever that string is filled with characters, the client DLL // sends it to the engine as a command to be executed. When the engine has executed that // command, this g_xgv string is reset to zero. Here is somehow a curious implementation of // ClientCommand: the engine sets the command it wants the client to issue in his g_xgv, then // the client DLL sends it back to the engine, the engine receives it then executes the // command therein. Don't ask me why we need all this complicated crap. Anyhow since bots have // no client DLL, be certain never to call this function upon a bot entity, else it will just // make the server crash. Since hordes of uncautious, not to say stupid, programmers don't // even imagine some players on their servers could be bots, this check is performed less than // sometimes actually by their side, that's why we strongly recommend to check it here too. In // case it's a bot asking for a client command, we handle it like we do for bot commands va_list ap; char buffer[MAX_PRINT_BUFFER]; va_start (ap, format); _vsnprintf (buffer, cr::bufsize (buffer), format, ap); va_end (ap); if (ent && (ent->v.flags & (FL_FAKECLIENT | FL_DORMANT))) { if (bots.getBot (ent)) { engine.execBotCmd (ent, buffer); } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); // prevent bots to be forced to issue client commands } return; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnClientCommand (ent, buffer); } void pfnMessageBegin (int msgDest, int msgType, const float *origin, edict_t *ed) { // this function called each time a message is about to sent. // store the message type in our own variables, since the GET_USER_MSG_ID () will just do a lot of strcmp()'s... if ((g_gameFlags & GAME_METAMOD) && engine.getMessageId (NETMSG_MONEY) == -1) { engine.setMessageId (NETMSG_VGUI, GET_USER_MSG_ID (PLID, "VGUIMenu", nullptr)); engine.setMessageId (NETMSG_SHOWMENU, GET_USER_MSG_ID (PLID, "ShowMenu", nullptr)); engine.setMessageId (NETMSG_WEAPONLIST, GET_USER_MSG_ID (PLID, "WeaponList", nullptr)); engine.setMessageId (NETMSG_CURWEAPON, GET_USER_MSG_ID (PLID, "CurWeapon", nullptr)); engine.setMessageId (NETMSG_AMMOX, GET_USER_MSG_ID (PLID, "AmmoX", nullptr)); engine.setMessageId (NETMSG_AMMOPICKUP, GET_USER_MSG_ID (PLID, "AmmoPickup", nullptr)); engine.setMessageId (NETMSG_DAMAGE, GET_USER_MSG_ID (PLID, "Damage", nullptr)); engine.setMessageId (NETMSG_MONEY, GET_USER_MSG_ID (PLID, "Money", nullptr)); engine.setMessageId (NETMSG_STATUSICON, GET_USER_MSG_ID (PLID, "StatusIcon", nullptr)); engine.setMessageId (NETMSG_DEATH, GET_USER_MSG_ID (PLID, "DeathMsg", nullptr)); engine.setMessageId (NETMSG_SCREENFADE, GET_USER_MSG_ID (PLID, "ScreenFade", nullptr)); engine.setMessageId (NETMSG_HLTV, GET_USER_MSG_ID (PLID, "HLTV", nullptr)); engine.setMessageId (NETMSG_TEXTMSG, GET_USER_MSG_ID (PLID, "TextMsg", nullptr)); engine.setMessageId (NETMSG_TEAMINFO, GET_USER_MSG_ID (PLID, "TeamInfo", nullptr)); engine.setMessageId (NETMSG_BARTIME, GET_USER_MSG_ID (PLID, "BarTime", nullptr)); engine.setMessageId (NETMSG_SENDAUDIO, GET_USER_MSG_ID (PLID, "SendAudio", nullptr)); engine.setMessageId (NETMSG_SAYTEXT, GET_USER_MSG_ID (PLID, "SayText", nullptr)); if (g_gameFlags & GAME_SUPPORT_BOT_VOICE) { engine.setMessageId (NETMSG_BOTVOICE, GET_USER_MSG_ID (PLID, "BotVoice", nullptr)); } } engine.resetMessages (); if ((!(g_gameFlags & GAME_LEGACY) || (g_gameFlags & GAME_XASH_ENGINE)) && msgDest == MSG_SPEC && msgType == engine.getMessageId (NETMSG_HLTV)) { engine.setCurrentMessageId (NETMSG_HLTV); } engine.captureMessage (msgType, NETMSG_WEAPONLIST); if (!engine.isNullEntity (ed)) { int index = bots.index (ed); // is this message for a bot? if (index != -1 && !(ed->v.flags & FL_DORMANT)) { engine.setCurrentMessageOwner (index); // message handling is done in usermsg.cpp engine.captureMessage (msgType, NETMSG_VGUI); engine.captureMessage (msgType, NETMSG_CURWEAPON); engine.captureMessage (msgType, NETMSG_AMMOX); engine.captureMessage (msgType, NETMSG_AMMOPICKUP); engine.captureMessage (msgType, NETMSG_DAMAGE); engine.captureMessage (msgType, NETMSG_MONEY); engine.captureMessage (msgType, NETMSG_STATUSICON); engine.captureMessage (msgType, NETMSG_SCREENFADE); engine.captureMessage (msgType, NETMSG_BARTIME); engine.captureMessage (msgType, NETMSG_TEXTMSG); engine.captureMessage (msgType, NETMSG_SHOWMENU); } } else if (msgDest == MSG_ALL) { engine.captureMessage (msgType, NETMSG_TEAMINFO); engine.captureMessage (msgType, NETMSG_DEATH); engine.captureMessage (msgType, NETMSG_TEXTMSG); if (msgType == SVC_INTERMISSION) { for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot != nullptr) { bot->m_notKilled = false; } } } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnMessageBegin (msgDest, msgType, origin, ed); } void pfnMessageEnd (void) { engine.resetMessages (); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnMessageEnd (); // send latency fix bots.sendDeathMsgFix (); } void pfnMessageEnd_Post (void) { // send latency fix bots.sendDeathMsgFix (); RETURN_META (MRES_IGNORED); } void pfnWriteByte (int value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteByte (value); } void pfnWriteChar (int value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteChar (value); } void pfnWriteShort (int value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteShort (value); } void pfnWriteLong (int value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteLong (value); } void pfnWriteAngle (float value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteAngle (value); } void pfnWriteCoord (float value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteCoord (value); } void pfnWriteString (const char *sz) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)sz); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteString (sz); } void pfnWriteEntity (int value) { // if this message is for a bot, call the client message function... engine.processMessages ((void *)&value); if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnWriteEntity (value); } int pfnCmd_Argc (void) { // this function returns the number of arguments the current client command string has. Since // bots have no client DLL and we may want a bot to execute a client command, we had to // implement a g_xgv string in the bot DLL for holding the bots' commands, and also keep // track of the argument count. Hence this hook not to let the engine ask an unexistent client // DLL for a command we are holding here. Of course, real clients commands are still retrieved // the normal way, by asking the engine. // is this a bot issuing that client command? if (engine.isBotCmd ()) { if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_SUPERCEDE, engine.botArgc ()); } return engine.botArgc (); // if so, then return the argument count we know } if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, 0); } return g_engfuncs.pfnCmd_Argc (); // ask the engine how many arguments there are } const char *pfnCmd_Args (void) { // this function returns a pointer to the whole current client command string. Since bots // have no client DLL and we may want a bot to execute a client command, we had to implement // a g_xgv string in the bot DLL for holding the bots' commands, and also keep track of the // argument count. Hence this hook not to let the engine ask an unexistent client DLL for a // command we are holding here. Of course, real clients commands are still retrieved the // normal way, by asking the engine. // is this a bot issuing that client command? if (engine.isBotCmd ()) { if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_SUPERCEDE, engine.botArgs ()); } return engine.botArgs (); // else return the whole bot client command string we know } if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, nullptr); } return g_engfuncs.pfnCmd_Args (); // ask the client command string to the engine } const char *pfnCmd_Argv (int argc) { // this function returns a pointer to a certain argument of the current client command. Since // bots have no client DLL and we may want a bot to execute a client command, we had to // implement a g_xgv string in the bot DLL for holding the bots' commands, and also keep // track of the argument count. Hence this hook not to let the engine ask an unexistent client // DLL for a command we are holding here. Of course, real clients commands are still retrieved // the normal way, by asking the engine. // is this a bot issuing that client command? if (engine.isBotCmd ()) { if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_SUPERCEDE, engine.botArgv (argc)); } return engine.botArgv (argc); // if so, then return the wanted argument we know } if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, nullptr); } return g_engfuncs.pfnCmd_Argv (argc); // ask the argument number "argc" to the engine } void pfnClientPrintf (edict_t *ent, PRINT_TYPE printType, const char *message) { // this function prints the text message string pointed to by message by the client side of // the client entity pointed to by ent, in a manner depending of printType (print_console, // print_center or print_chat). Be certain never to try to feed a bot with this function, // as it will crash your server. Why would you, anyway ? bots have no client DLL as far as // we know, right ? But since stupidity rules this world, we do a preventive check :) if (isFakeClient (ent)) { if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_SUPERCEDE); } return; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnClientPrintf (ent, printType, message); } void pfnSetClientMaxspeed (const edict_t *ent, float newMaxspeed) { Bot *bot = bots.getBot (const_cast (ent)); // check wether it's not a bot if (bot != nullptr) { bot->pev->maxspeed = newMaxspeed; } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnSetClientMaxspeed (ent, newMaxspeed); } int pfnRegUserMsg (const char *name, int size) { // this function registers a "user message" by the engine side. User messages are network // messages the game DLL asks the engine to send to clients. Since many MODs have completely // different client features (Counter-Strike has a radar and a timer, for example), network // messages just can't be the same for every MOD. Hence here the MOD DLL tells the engine, // "Hey, you have to know that I use a network message whose name is pszName and it is size // packets long". The engine books it, and returns the ID number under which he recorded that // custom message. Thus every time the MOD DLL will be wanting to send a message named pszName // using pfnMessageBegin (), it will know what message ID number to send, and the engine will // know what to do, only for non-metamod version if (g_gameFlags & GAME_METAMOD) { RETURN_META_VALUE (MRES_IGNORED, 0); } int message = g_engfuncs.pfnRegUserMsg (name, size); if (strcmp (name, "VGUIMenu") == 0) { engine.setMessageId (NETMSG_VGUI, message); } else if (strcmp (name, "ShowMenu") == 0) { engine.setMessageId (NETMSG_SHOWMENU, message); } else if (strcmp (name, "WeaponList") == 0) { engine.setMessageId (NETMSG_WEAPONLIST, message); } else if (strcmp (name, "CurWeapon") == 0) { engine.setMessageId (NETMSG_CURWEAPON, message); } else if (strcmp (name, "AmmoX") == 0) { engine.setMessageId (NETMSG_AMMOX, message); } else if (strcmp (name, "AmmoPickup") == 0) { engine.setMessageId (NETMSG_AMMOPICKUP, message); } else if (strcmp (name, "Damage") == 0) { engine.setMessageId (NETMSG_DAMAGE, message); } else if (strcmp (name, "Money") == 0) { engine.setMessageId (NETMSG_MONEY, message); } else if (strcmp (name, "StatusIcon") == 0) { engine.setMessageId (NETMSG_STATUSICON, message); } else if (strcmp (name, "DeathMsg") == 0) { engine.setMessageId (NETMSG_DEATH, message); } else if (strcmp (name, "ScreenFade") == 0) { engine.setMessageId (NETMSG_SCREENFADE, message); } else if (strcmp (name, "HLTV") == 0) { engine.setMessageId (NETMSG_HLTV, message); } else if (strcmp (name, "TextMsg") == 0) { engine.setMessageId (NETMSG_TEXTMSG, message); } else if (strcmp (name, "TeamInfo") == 0) { engine.setMessageId (NETMSG_TEAMINFO, message); } else if (strcmp (name, "BarTime") == 0) { engine.setMessageId (NETMSG_BARTIME, message); } else if (strcmp (name, "SendAudio") == 0) { engine.setMessageId (NETMSG_SENDAUDIO, message); } else if (strcmp (name, "SayText") == 0) { engine.setMessageId (NETMSG_SAYTEXT, message); } else if (strcmp (name, "BotVoice") == 0) { engine.setMessageId (NETMSG_BOTVOICE, message); } return message; } void pfnAlertMessage (ALERT_TYPE alertType, char *format, ...) { va_list ap; char buffer[MAX_PRINT_BUFFER]; va_start (ap, format); vsnprintf (buffer, cr::bufsize (buffer), format, ap); va_end (ap); if ((g_mapFlags & MAP_DE) && g_bombPlanted && strstr (buffer, "_Defuse_") != nullptr) { // notify all terrorists that CT is starting bomb defusing for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot != nullptr && bot->m_team == TEAM_TERRORIST && bot->m_notKilled) { bot->clearSearchNodes (); bot->m_position = waypoints.getBombPos (); bot->startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, INVALID_WAYPOINT_INDEX, 0.0f, true); } } } if (g_gameFlags & GAME_METAMOD) { RETURN_META (MRES_IGNORED); } g_engfuncs.pfnAlertMessage (alertType, buffer); } typedef void (*entity_func_t) (entvars_t *); gamedll_funcs_t gameDLLFunc; SHARED_LIBRARAY_EXPORT int GetEntityAPI2 (gamefuncs_t *functionTable, int *) { // this function is called right after FuncPointers_t() by the engine in the game DLL (or // what it BELIEVES to be the game DLL), in order to copy the list of MOD functions that can // be called by the engine, into a memory block pointed to by the functionTable pointer // that is passed into this function (explanation comes straight from botman). This allows // the Half-Life engine to call these MOD DLL functions when it needs to spawn an entity, // connect or disconnect a player, call Think() functions, Touch() functions, or Use() // functions, etc. The bot DLL passes its OWN list of these functions back to the Half-Life // engine, and then calls the MOD DLL's version of GetEntityAPI to get the REAL gamedll // functions this time (to use in the bot code). memset (functionTable, 0, sizeof (gamefuncs_t)); if (!(g_gameFlags & GAME_METAMOD)) { auto api_GetEntityAPI = g_gameLib->resolve ("GetEntityAPI"); // pass other DLLs engine callbacks to function table... if (api_GetEntityAPI (&g_functionTable, INTERFACE_VERSION) == 0) { logEntry (true, LL_FATAL, "GetEntityAPI2: ERROR - Not Initialized."); return FALSE; // error initializing function table!!! } gameDLLFunc.dllapi_table = &g_functionTable; gpGamedllFuncs = &gameDLLFunc; memcpy (functionTable, &g_functionTable, sizeof (gamefuncs_t)); } functionTable->pfnGameInit = GameDLLInit; functionTable->pfnSpawn = Spawn; functionTable->pfnTouch = Touch; functionTable->pfnClientConnect = ClientConnect; functionTable->pfnClientDisconnect = ClientDisconnect; functionTable->pfnClientUserInfoChanged = ClientUserInfoChanged; functionTable->pfnClientCommand = ClientCommand; functionTable->pfnServerActivate = ServerActivate; functionTable->pfnServerDeactivate = ServerDeactivate; functionTable->pfnStartFrame = StartFrame; functionTable->pfnUpdateClientData = UpdateClientData; return TRUE; } SHARED_LIBRARAY_EXPORT int GetEntityAPI2_Post (gamefuncs_t *functionTable, int *) { // this function is called right after FuncPointers_t() by the engine in the game DLL (or // what it BELIEVES to be the game DLL), in order to copy the list of MOD functions that can // be called by the engine, into a memory block pointed to by the functionTable pointer // that is passed into this function (explanation comes straight from botman). This allows // the Half-Life engine to call these MOD DLL functions when it needs to spawn an entity, // connect or disconnect a player, call Think() functions, Touch() functions, or Use() // functions, etc. The bot DLL passes its OWN list of these functions back to the Half-Life // engine, and then calls the MOD DLL's version of GetEntityAPI to get the REAL gamedll // functions this time (to use in the bot code). Post version, called only by metamod. memset (functionTable, 0, sizeof (gamefuncs_t)); functionTable->pfnSpawn = Spawn_Post; functionTable->pfnStartFrame = StartFrame_Post; functionTable->pfnServerActivate = ServerActivate_Post; return TRUE; } SHARED_LIBRARAY_EXPORT int GetNewDLLFunctions (newgamefuncs_t *functionTable, int *interfaceVersion) { // it appears that an extra function table has been added in the engine to gamedll interface // since the date where the first enginefuncs table standard was frozen. These ones are // facultative and we don't hook them, but since some MODs might be featuring it, we have to // pass them too, else the DLL interfacing wouldn't be complete and the game possibly wouldn't // run properly. auto api_GetNewDLLFunctions = g_gameLib->resolve ("GetNewDLLFunctions"); if (api_GetNewDLLFunctions == nullptr) { return FALSE; } if (!api_GetNewDLLFunctions (functionTable, interfaceVersion)) { logEntry (true, LL_FATAL, "GetNewDLLFunctions: ERROR - Not Initialized."); return FALSE; } gameDLLFunc.newapi_table = functionTable; return TRUE; } SHARED_LIBRARAY_EXPORT int GetEngineFunctions (enginefuncs_t *functionTable, int *) { if (g_gameFlags & GAME_METAMOD) { memset (functionTable, 0, sizeof (enginefuncs_t)); } functionTable->pfnChangeLevel = pfnChangeLevel; functionTable->pfnFindEntityByString = pfnFindEntityByString; functionTable->pfnEmitSound = pfnEmitSound; functionTable->pfnClientCommand = pfnClientCommand; functionTable->pfnMessageBegin = pfnMessageBegin; functionTable->pfnMessageEnd = pfnMessageEnd; functionTable->pfnWriteByte = pfnWriteByte; functionTable->pfnWriteChar = pfnWriteChar; functionTable->pfnWriteShort = pfnWriteShort; functionTable->pfnWriteLong = pfnWriteLong; functionTable->pfnWriteAngle = pfnWriteAngle; functionTable->pfnWriteCoord = pfnWriteCoord; functionTable->pfnWriteString = pfnWriteString; functionTable->pfnWriteEntity = pfnWriteEntity; functionTable->pfnRegUserMsg = pfnRegUserMsg; functionTable->pfnClientPrintf = pfnClientPrintf; functionTable->pfnCmd_Args = pfnCmd_Args; functionTable->pfnCmd_Argv = pfnCmd_Argv; functionTable->pfnCmd_Argc = pfnCmd_Argc; functionTable->pfnSetClientMaxspeed = pfnSetClientMaxspeed; functionTable->pfnAlertMessage = pfnAlertMessage; return TRUE; } SHARED_LIBRARAY_EXPORT int GetEngineFunctions_Post (enginefuncs_t *functionTable, int *) { memset (functionTable, 0, sizeof (enginefuncs_t)); functionTable->pfnMessageEnd = pfnMessageEnd_Post; return TRUE; } SHARED_LIBRARAY_EXPORT int Server_GetBlendingInterface (int version, void **ppinterface, void *pstudio, float (*rotationmatrix)[3][4], float (*bonetransform)[128][3][4]) { // this function synchronizes the studio model animation blending interface (i.e, what parts // of the body move, which bones, which hitboxes and how) between the server and the game DLL. // some MODs can be using a different hitbox scheme than the standard one. auto api_GetBlendingInterface = g_gameLib->resolve ("Server_GetBlendingInterface"); if (api_GetBlendingInterface == nullptr) { return FALSE; } return api_GetBlendingInterface (version, ppinterface, pstudio, rotationmatrix, bonetransform); } SHARED_LIBRARAY_EXPORT int Meta_Query (char *, plugin_info_t **pPlugInfo, mutil_funcs_t *pMetaUtilFuncs) { // this function is the first function ever called by metamod in the plugin DLL. Its purpose // is for metamod to retrieve basic information about the plugin, such as its meta-interface // version, for ensuring compatibility with the current version of the running metamod. gpMetaUtilFuncs = pMetaUtilFuncs; *pPlugInfo = &Plugin_info; return TRUE; // tell metamod this plugin looks safe } SHARED_LIBRARAY_EXPORT int Meta_Attach (PLUG_LOADTIME, metamod_funcs_t *functionTable, meta_globals_t *pMGlobals, gamedll_funcs_t *pGamedllFuncs) { // this function is called when metamod attempts to load the plugin. Since it's the place // where we can tell if the plugin will be allowed to run or not, we wait until here to make // our initialization stuff, like registering CVARs and dedicated server commands. // metamod engine & dllapi function tables static metamod_funcs_t metamodFunctionTable = { nullptr, // pfnGetEntityAPI () nullptr, // pfnGetEntityAPI_Post () GetEntityAPI2, // pfnGetEntityAPI2 () GetEntityAPI2_Post, // pfnGetEntityAPI2_Post () nullptr, // pfnGetNewDLLFunctions () nullptr, // pfnGetNewDLLFunctions_Post () GetEngineFunctions, // pfnGetEngineFunctions () GetEngineFunctions_Post, // pfnGetEngineFunctions_Post () }; // keep track of the pointers to engine function tables metamod gives us gpMetaGlobals = pMGlobals; memcpy (functionTable, &metamodFunctionTable, sizeof (metamod_funcs_t)); gpGamedllFuncs = pGamedllFuncs; return TRUE; // returning true enables metamod to attach this plugin } SHARED_LIBRARAY_EXPORT int Meta_Detach (PLUG_LOADTIME, PL_UNLOAD_REASON) { // this function is called when metamod unloads the plugin. A basic check is made in order // to prevent unloading the plugin if its processing should not be interrupted. bots.kickEveryone (true); // kick all bots off this server cleanupGarbage (); return TRUE; } SHARED_LIBRARAY_EXPORT void Meta_Init (void) { // this function is called by metamod, before any other interface functions. Purpose of this // function to give plugin a chance to determine is plugin running under metamod or not. g_gameFlags |= GAME_METAMOD; } Library *LoadCSBinary (void) { const char *modname = engine.getModName (); if (!modname) return nullptr; #if defined(PLATFORM_WIN32) const char *libs[] = {"mp.dll", "cs.dll"}; #elif defined(PLATFORM_LINUX) const char *libs[] = {"cs.so", "cs_i386.so"}; #elif defined(PLATFORM_OSX) const char *libs[] = {"cs.dylib"}; #endif // search the libraries inside game dlls directory for (size_t i = 0; i < cr::arrsize (libs); i++) { auto *path = format ("%s/dlls/%s", modname, libs[i]); // if we can't read file, skip it if (!File::exists (path)) { continue; } // special case, czero is always detected first, as it's has custom directory if (strcmp (modname, "czero") == 0) { g_gameFlags |= (GAME_CZERO | GAME_SUPPORT_BOT_VOICE | GAME_SUPPORT_SVC_PINGS); if (g_gameFlags & GAME_METAMOD) { return nullptr; } return new Library (path); } else { Library *game = new Library (path); // try to load gamedll if (!game->isValid ()) { logEntry (true, LL_FATAL | LL_IGNORE, "Unable to load gamedll \"%s\". Exiting... (gamedir: %s)", libs[i], modname); delete game; return nullptr; } // detect if we're running modern game auto entity = game->resolve ("weapon_famas"); // detect xash engine if (g_engfuncs.pfnCVarGetPointer ("build") != nullptr) { g_gameFlags |= (GAME_LEGACY | GAME_XASH_ENGINE); if (entity != nullptr) { g_gameFlags |= GAME_SUPPORT_BOT_VOICE; } if (g_gameFlags & GAME_METAMOD) { delete game; return nullptr; } return game; } if (entity != nullptr) { g_gameFlags |= (GAME_CSTRIKE16 | GAME_SUPPORT_BOT_VOICE | GAME_SUPPORT_SVC_PINGS); } else { g_gameFlags |= GAME_LEGACY; } if (g_gameFlags & GAME_METAMOD) { delete game; return nullptr; } return game; } } return nullptr; } DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *functionTable, globalvars_t *pGlobals) { // this is the very first function that is called in the game DLL by the engine. Its purpose // is to set the functions interfacing up, by exchanging the functionTable function list // along with a pointer to the engine's global variables structure pGlobals, with the game // DLL. We can there decide if we want to load the normal game DLL just behind our bot DLL, // or any other game DLL that is present, such as Will Day's metamod. Also, since there is // a known bug on Win32 platforms that prevent hook DLLs (such as our bot DLL) to be used in // single player games (because they don't export all the stuff they should), we may need to // build our own array of exported symbols from the actual game DLL in order to use it as // such if necessary. Nothing really bot-related is done in this function. The actual bot // initialization stuff will be done later, when we'll be certain to have a multilayer game. // get the engine functions from the engine... memcpy (&g_engfuncs, functionTable, sizeof (enginefuncs_t)); g_pGlobals = pGlobals; // register our cvars engine.pushRegStackToEngine (); // ensure we're have all needed directories { const char *mod = engine.getModName (); // create the needed paths File::pathCreate (const_cast (format ("%s/addons/yapb/conf/lang", mod))); File::pathCreate (const_cast (format ("%s/addons/yapb/data/learned", mod))); } #ifdef PLATFORM_ANDROID g_gameFlags |= (GAME_XASH_ENGINE | GAME_MOBILITY | GAME_SUPPORT_BOT_VOICE); if (g_gameFlags & GAME_METAMOD) { return; // we should stop the attempt for loading the real gamedll, since metamod handle this for us } extern ConVar yb_difficulty; yb_difficulty.set (2); #ifdef LOAD_HARDFP const char *serverDLL = "libserver_hardfp.so"; #else const char *serverDLL = "libserver.so"; #endif char gameDLLName[256]; snprintf (gameDLLName, cr::bufsize (gameDLLName), "%s/%s", getenv ("XASH3D_GAMELIBDIR"), serverDLL); g_gameLib = new Library (gameDLLName); if (!g_gameLib->isValid ()) { logEntry (true, LL_FATAL | LL_IGNORE, "Unable to load gamedll \"%s\". Exiting... (gamedir: %s)", gameDLLName, engine.getModName ()); delete g_gameLib; } #else g_gameLib = LoadCSBinary (); { if (!g_gameLib && !(g_gameFlags & GAME_METAMOD)) { logEntry (true, LL_FATAL | LL_IGNORE, "Mod that you has started, not supported by this bot (gamedir: %s)", engine.getModName ()); return; } // print game detection info String gameVersionStr; if (g_gameFlags & GAME_LEGACY) { gameVersionStr.assign ("Legacy"); } else if (g_gameFlags & GAME_CZERO) { gameVersionStr.assign ("Condition Zero"); } else if (g_gameFlags & GAME_CSTRIKE16) { gameVersionStr.assign ("v1.6"); } if (g_gameFlags & GAME_XASH_ENGINE) { gameVersionStr.append (" @ Xash3D Engine"); if (g_gameFlags & GAME_MOBILITY) { gameVersionStr.append (" Mobile"); } gameVersionStr.replace ("Legacy", "1.6 Limited"); } if (g_gameFlags & GAME_SUPPORT_BOT_VOICE) { gameVersionStr.append (" (BV)"); } if (g_gameFlags & GAME_SUPPORT_SVC_PINGS) { gameVersionStr.append (" (SVC)"); } engine.print ("[YAPB] Bot v%s.0.%d Loaded. Game detected as Counter-Strike: %s", PRODUCT_VERSION, buildNumber(), gameVersionStr.chars ()); if (g_gameFlags & GAME_METAMOD) { return; } } #endif auto api_GiveFnptrsToDll = g_gameLib->resolve ("GiveFnptrsToDll"); assert (api_GiveFnptrsToDll != nullptr); GetEngineFunctions (functionTable, nullptr); // give the engine functions to the other DLL... api_GiveFnptrsToDll (functionTable, pGlobals); } DLL_ENTRYPOINT { // dynamic library entry point, can be used for uninitialization stuff. NOT for initializing // anything because if you ever attempt to wander outside the scope of this function on a // DLL attach, LoadLibrary() will simply fail. And you can't do I/Os here either. // dynamic library detaching ?? if (DLL_DETACHING) { cleanupGarbage (); // free everything that's freeable delete g_gameLib; // if dynamic link library of mod is load, free it } DLL_RETENTRY; // the return data type is OS specific too } void helper_LinkEntity (entity_func_t &addr, const char *name, entvars_t *pev) { if (addr == nullptr) { addr = g_gameLib->resolve (name); } if (addr == nullptr) { return; } addr (pev); } #define LINK_ENTITY(entityName) \ SHARED_LIBRARAY_EXPORT void entityName (entvars_t *pev) { \ static entity_func_t addr; \ helper_LinkEntity (addr, #entityName, pev); \ } // entities in counter-strike... LINK_ENTITY (DelayedUse) LINK_ENTITY (ambient_generic) LINK_ENTITY (ammo_338magnum) LINK_ENTITY (ammo_357sig) LINK_ENTITY (ammo_45acp) LINK_ENTITY (ammo_50ae) LINK_ENTITY (ammo_556nato) LINK_ENTITY (ammo_556natobox) LINK_ENTITY (ammo_57mm) LINK_ENTITY (ammo_762nato) LINK_ENTITY (ammo_9mm) LINK_ENTITY (ammo_buckshot) LINK_ENTITY (armoury_entity) LINK_ENTITY (beam) LINK_ENTITY (bodyque) LINK_ENTITY (button_target) LINK_ENTITY (cycler) LINK_ENTITY (cycler_prdroid) LINK_ENTITY (cycler_sprite) LINK_ENTITY (cycler_weapon) LINK_ENTITY (cycler_wreckage) LINK_ENTITY (env_beam) LINK_ENTITY (env_beverage) LINK_ENTITY (env_blood) LINK_ENTITY (env_bombglow) LINK_ENTITY (env_bubbles) LINK_ENTITY (env_debris) LINK_ENTITY (env_explosion) LINK_ENTITY (env_fade) LINK_ENTITY (env_funnel) LINK_ENTITY (env_global) LINK_ENTITY (env_glow) LINK_ENTITY (env_laser) LINK_ENTITY (env_lightning) LINK_ENTITY (env_message) LINK_ENTITY (env_rain) LINK_ENTITY (env_render) LINK_ENTITY (env_shake) LINK_ENTITY (env_shooter) LINK_ENTITY (env_snow) LINK_ENTITY (env_sound) LINK_ENTITY (env_spark) LINK_ENTITY (env_sprite) LINK_ENTITY (fireanddie) LINK_ENTITY (func_bomb_target) LINK_ENTITY (func_breakable) LINK_ENTITY (func_button) LINK_ENTITY (func_buyzone) LINK_ENTITY (func_conveyor) LINK_ENTITY (func_door) LINK_ENTITY (func_door_rotating) LINK_ENTITY (func_escapezone) LINK_ENTITY (func_friction) LINK_ENTITY (func_grencatch) LINK_ENTITY (func_guntarget) LINK_ENTITY (func_healthcharger) LINK_ENTITY (func_hostage_rescue) LINK_ENTITY (func_illusionary) LINK_ENTITY (func_ladder) LINK_ENTITY (func_monsterclip) LINK_ENTITY (func_mortar_field) LINK_ENTITY (func_pendulum) LINK_ENTITY (func_plat) LINK_ENTITY (func_platrot) LINK_ENTITY (func_pushable) LINK_ENTITY (func_rain) LINK_ENTITY (func_recharge) LINK_ENTITY (func_rot_button) LINK_ENTITY (func_rotating) LINK_ENTITY (func_snow) LINK_ENTITY (func_tank) LINK_ENTITY (func_tankcontrols) LINK_ENTITY (func_tanklaser) LINK_ENTITY (func_tankmortar) LINK_ENTITY (func_tankrocket) LINK_ENTITY (func_trackautochange) LINK_ENTITY (func_trackchange) LINK_ENTITY (func_tracktrain) LINK_ENTITY (func_train) LINK_ENTITY (func_traincontrols) LINK_ENTITY (func_vehicle) LINK_ENTITY (func_vehiclecontrols) LINK_ENTITY (func_vip_safetyzone) LINK_ENTITY (func_wall) LINK_ENTITY (func_wall_toggle) LINK_ENTITY (func_water) LINK_ENTITY (func_weaponcheck) LINK_ENTITY (game_counter) LINK_ENTITY (game_counter_set) LINK_ENTITY (game_end) LINK_ENTITY (game_player_equip) LINK_ENTITY (game_player_hurt) LINK_ENTITY (game_player_team) LINK_ENTITY (game_score) LINK_ENTITY (game_team_master) LINK_ENTITY (game_team_set) LINK_ENTITY (game_text) LINK_ENTITY (game_zone_player) LINK_ENTITY (gibshooter) LINK_ENTITY (grenade) LINK_ENTITY (hostage_entity) LINK_ENTITY (info_bomb_target) LINK_ENTITY (info_hostage_rescue) LINK_ENTITY (info_intermission) LINK_ENTITY (info_landmark) LINK_ENTITY (info_map_parameters) LINK_ENTITY (info_null) LINK_ENTITY (info_player_deathmatch) LINK_ENTITY (info_player_start) LINK_ENTITY (info_target) LINK_ENTITY (info_teleport_destination) LINK_ENTITY (info_vip_start) LINK_ENTITY (infodecal) LINK_ENTITY (item_airtank) LINK_ENTITY (item_antidote) LINK_ENTITY (item_assaultsuit) LINK_ENTITY (item_battery) LINK_ENTITY (item_healthkit) LINK_ENTITY (item_kevlar) LINK_ENTITY (item_longjump) LINK_ENTITY (item_security) LINK_ENTITY (item_sodacan) LINK_ENTITY (item_suit) LINK_ENTITY (item_thighpack) LINK_ENTITY (light) LINK_ENTITY (light_environment) LINK_ENTITY (light_spot) LINK_ENTITY (momentary_door) LINK_ENTITY (momentary_rot_button) LINK_ENTITY (monster_hevsuit_dead) LINK_ENTITY (monster_mortar) LINK_ENTITY (monster_scientist) LINK_ENTITY (multi_manager) LINK_ENTITY (multisource) LINK_ENTITY (path_corner) LINK_ENTITY (path_track) LINK_ENTITY (player) LINK_ENTITY (player_loadsaved) LINK_ENTITY (player_weaponstrip) LINK_ENTITY (soundent) LINK_ENTITY (spark_shower) LINK_ENTITY (speaker) LINK_ENTITY (target_cdaudio) LINK_ENTITY (test_effect) LINK_ENTITY (trigger) LINK_ENTITY (trigger_auto) LINK_ENTITY (trigger_autosave) LINK_ENTITY (trigger_camera) LINK_ENTITY (trigger_cdaudio) LINK_ENTITY (trigger_changelevel) LINK_ENTITY (trigger_changetarget) LINK_ENTITY (trigger_counter) LINK_ENTITY (trigger_endsection) LINK_ENTITY (trigger_gravity) LINK_ENTITY (trigger_hurt) LINK_ENTITY (trigger_monsterjump) LINK_ENTITY (trigger_multiple) LINK_ENTITY (trigger_once) LINK_ENTITY (trigger_push) LINK_ENTITY (trigger_relay) LINK_ENTITY (trigger_teleport) LINK_ENTITY (trigger_transition) LINK_ENTITY (weapon_ak47) LINK_ENTITY (weapon_aug) LINK_ENTITY (weapon_awp) LINK_ENTITY (weapon_c4) LINK_ENTITY (weapon_deagle) LINK_ENTITY (weapon_elite) LINK_ENTITY (weapon_famas) LINK_ENTITY (weapon_fiveseven) LINK_ENTITY (weapon_flashbang) LINK_ENTITY (weapon_g3sg1) LINK_ENTITY (weapon_galil) LINK_ENTITY (weapon_glock18) LINK_ENTITY (weapon_hegrenade) LINK_ENTITY (weapon_knife) LINK_ENTITY (weapon_m249) LINK_ENTITY (weapon_m3) LINK_ENTITY (weapon_m4a1) LINK_ENTITY (weapon_mac10) LINK_ENTITY (weapon_mp5navy) LINK_ENTITY (weapon_p228) LINK_ENTITY (weapon_p90) LINK_ENTITY (weapon_scout) LINK_ENTITY (weapon_sg550) LINK_ENTITY (weapon_sg552) LINK_ENTITY (weapon_shield) LINK_ENTITY (weapon_shieldgun) LINK_ENTITY (weapon_smokegrenade) LINK_ENTITY (weapon_tmp) LINK_ENTITY (weapon_ump45) LINK_ENTITY (weapon_usp) LINK_ENTITY (weapon_xm1014) LINK_ENTITY (weaponbox) LINK_ENTITY (world_items) LINK_ENTITY (worldspawn)