diff --git a/AMBuildScript b/AMBuildScript index 1f902a6e..abe002b1 100644 --- a/AMBuildScript +++ b/AMBuildScript @@ -92,7 +92,7 @@ class MMSPluginConfig(object): if builder.options.targets: target_archs = builder.options.targets.split(',') else: - target_archs = ['x86', 'x86_64'] + target_archs = ['x86_64'] for arch in target_archs: try: diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj index cd8c2ac9..9d9f8776 100644 --- a/CS2Fixes.vcxproj +++ b/CS2Fixes.vcxproj @@ -279,6 +279,7 @@ + diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters index e4c1f32d..2d425015 100644 --- a/CS2Fixes.vcxproj.filters +++ b/CS2Fixes.vcxproj.filters @@ -364,5 +364,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/PackageScript b/PackageScript index bd8cb922..82844781 100644 --- a/PackageScript +++ b/PackageScript @@ -59,7 +59,7 @@ mapcfg_folder = builder.AddFolder(os.path.join('cfg', MMSPlugin.plugin_name, 'ma gamedata_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'gamedata')) builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'admins.cfg.example'), configs_folder) builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'discordbots.cfg.example'), configs_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'maplist.cfg.example'), configs_folder) +builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'maplist.jsonc.example'), configs_folder) builder.AddCopy(os.path.join(builder.sourcePath, 'cfg', MMSPlugin.plugin_name, 'cs2fixes.cfg'), cfg_folder) builder.AddCopy(os.path.join(builder.sourcePath, 'cfg', MMSPlugin.plugin_name, 'maps', 'de_somemap.cfg'), mapcfg_folder) builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'zr', 'playerclass.jsonc.example'), zr_folder) diff --git a/cfg/cs2fixes/cs2fixes.cfg b/cfg/cs2fixes/cs2fixes.cfg index 8be5d885..ed07458a 100644 --- a/cfg/cs2fixes/cs2fixes.cfg +++ b/cfg/cs2fixes/cs2fixes.cfg @@ -78,8 +78,9 @@ cs2f_rtv_success_ratio 0.6 // Ratio needed to pass RTV cs2f_rtv_endround 0 // Whether to immediately end the round when RTV succeeds // Map vote settings -cs2f_vote_maps_cooldown 10 // Default number of maps to wait until a map can be voted / nominated again i.e. cooldown. -cs2f_vote_max_nominations 10 // Number of nominations to include per vote, out of a maximum of 10. +cs2f_vote_maps_cooldown 6.0 // Default number of hours until a map can be played again i.e. cooldown +cs2f_vote_max_nominations 10 // Number of nominations to include per vote, out of a maximum of 10 +cs2f_vote_max_maps 10 // Number of total maps to include per vote, including nominations, out of a maximum of 10 // User preferences settings cs2f_user_prefs_api "" // User Preferences REST API endpoint diff --git a/configs/maplist.cfg.example b/configs/maplist.cfg.example deleted file mode 100644 index 5df4d395..00000000 --- a/configs/maplist.cfg.example +++ /dev/null @@ -1,29 +0,0 @@ -"Maplist" -{ - "de_dust2" - { - "enabled" "1" - } - "ze_my_first_ze_map" - { - "workshop_id" "123" - "enabled" "1" - "min_players" "30" - "cooldown" "2" - } - "ze_my_second_ze_map" - { - "workshop_id" "456" - "enabled" "1" - "min_players" "5" - "max_players" "10" - "cooldown" "3" - } - "ze_my_third_ze_map" - { - "workshop_id" "789" - "enabled" "1" - "max_players" "20" - "cooldown" "1" - } -} \ No newline at end of file diff --git a/configs/maplist.jsonc.example b/configs/maplist.jsonc.example new file mode 100644 index 00000000..9786c30b --- /dev/null +++ b/configs/maplist.jsonc.example @@ -0,0 +1,52 @@ +{ + "Groups": + { + // If any map in a group gets played, all maps in that group will receive the group cooldown + // The group cooldown applies immediately, but only starts ticking down *after* the trigger map ends + "MyFirstGroup": + { + "enabled": true, + "cooldown": 4 // Cooldown in hours + }, + "MySecondGroup": + { + "enabled": true, + "cooldown": 0.5 + }, + "MyThirdGroup": + { + // Omitting the cooldown option will fall back to cs2f_vote_maps_cooldown cvar + "enabled": true + } + }, + "Maps": + { + "de_dust2": // Map name should always match .vpk name to avoid unintended behaviour + { + "enabled": true + }, + "ze_my_first_ze_map": + { + "enabled": true, + "workshop_id": 123, // Workshop ID must be specified for workshop maps + "min_players": 20, // Minimum players required to nominate or appear in vote + "cooldown": 2 // Custom cooldown in hours, can override default map cooldown + }, + "ze_my_second_ze_map": + { + "enabled": true, + "workshop_id": 456, + "min_players": 5, // Maximum players where map can be nominated or appear in vote + "max_players": 10, + "cooldown": 3, + "groups": [ "MyFirstGroup" ] // Any other maps in this group will receive the group cooldown when this map is played + }, + "ze_my_third_ze_map": + { + "enabled": true, + "workshop_id": 789, + "max_players": 20, + "groups": [ "MyFirstGroup", "MySecondGroup" ] // A map can be in multiple groups + } + } +} \ No newline at end of file diff --git a/src/adminsystem.cpp b/src/adminsystem.cpp index e9882eb6..55e953bc 100644 --- a/src/adminsystem.cpp +++ b/src/adminsystem.cpp @@ -38,7 +38,7 @@ extern IVEngineServer2* g_pEngineServer2; extern CGameEntitySystem* g_pEntitySystem; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; CAdminSystem* g_pAdminSystem = nullptr; @@ -113,10 +113,10 @@ void PrintMultiAdminAction(ETargetType nType, const char* pszAdminName, const ch CON_COMMAND_F(c_reload_admins, "Reload admin config", FCVAR_SPONLY | FCVAR_LINKED_CONCOMMAND) { - if (!g_pAdminSystem->LoadAdmins()) + if (!g_pAdminSystem->LoadAdmins() || !GetGlobals()) return; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -131,10 +131,10 @@ CON_COMMAND_F(c_reload_admins, "Reload admin config", FCVAR_SPONLY | FCVAR_LINKE CON_COMMAND_F(c_reload_infractions, "Reload infractions file", FCVAR_SPONLY | FCVAR_LINKED_CONCOMMAND) { - if (!g_pAdminSystem->LoadInfractions()) + if (!g_pAdminSystem->LoadInfractions() || !GetGlobals()) return; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -577,76 +577,6 @@ CON_COMMAND_CHAT_FLAGS(entfirecontroller, " [parameter] - Fire out ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Input successful on %i player controllers.", iFoundEnts); } -CON_COMMAND_CHAT_FLAGS(map, " - Change map", ADMFLAG_CHANGEMAP) -{ - if (args.ArgC() < 2) - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !map "); - return; - } - - std::string sMapName = args[1]; - - for (int i = 0; sMapName[i]; i++) - { - // Injection prevention, because we may pass user input to ServerCommand - if (sMapName[i] == ';' || sMapName[i] == '|') - return; - - sMapName[i] = tolower(sMapName[i]); - } - - const char* pszMapName = sMapName.c_str(); - - if (!g_pEngineServer2->IsMapValid(pszMapName)) - { - std::string sCommand; - std::vector foundIndexes = g_pMapVoteSystem->GetMapIndexesFromSubstring(pszMapName); - - // Check if input is numeric (workshop ID) - // Not safe to expose to all admins until crashing on failed workshop addon downloads is fixed - if ((!player || player->GetZEPlayer()->IsAdminFlagSet(ADMFLAG_RCON)) && V_StringToUint64(pszMapName, 0, NULL, NULL, PARSING_FLAG_SKIP_WARNING) != 0) - { - sCommand = "host_workshop_map " + sMapName; - } - else if (g_bVoteManagerEnable && foundIndexes.size() > 0) - { - if (foundIndexes.size() > 1) - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Multiple maps matched \x06%s\x01, try being more specific:", pszMapName); - - for (int i = 0; i < foundIndexes.size() && i < 5; i++) - ClientPrint(player, HUD_PRINTTALK, "- %s", g_pMapVoteSystem->GetMapName(foundIndexes[i])); - - return; - } - - sCommand = "host_workshop_map " + std::to_string(g_pMapVoteSystem->GetMapWorkshopId(foundIndexes[0])); - } - else - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Failed to find a map matching %s.", pszMapName); - return; - } - - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Changing map to %s...", pszMapName); - - new CTimer(5.0f, false, true, [sCommand]() { - g_pEngineServer2->ServerCommand(sCommand.c_str()); - return -1.0f; - }); - - return; - } - - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Changing map to %s...", pszMapName); - - new CTimer(5.0f, false, true, [sMapName]() { - g_pEngineServer2->ChangeLevel(sMapName.c_str(), nullptr); - return -1.0f; - }); -} - CON_COMMAND_CHAT_FLAGS(hsay, " - Say something as a hud hint", ADMFLAG_CHAT) { if (args.ArgC() < 2) @@ -685,8 +615,7 @@ CON_COMMAND_CHAT_FLAGS(extend, " - Extend current map (negative value r int iExtendTime = V_StringToInt32(args[1], 0); - // Call the votemanager extend function so the extend vote can be checked - ExtendMap(iExtendTime); + g_pVoteManager->ExtendMap(iExtendTime); const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; @@ -698,6 +627,9 @@ CON_COMMAND_CHAT_FLAGS(extend, " - Extend current map (negative value r CON_COMMAND_CHAT_FLAGS(pm, " - Private message a player. This will also show to all online admins", ADMFLAG_GENERIC) { + if (!GetGlobals()) + return; + if (args.ArgC() < 3) { ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: /pm "); @@ -737,7 +669,7 @@ CON_COMMAND_CHAT_FLAGS(pm, " - Private message a player. This wi return; } - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -755,9 +687,12 @@ CON_COMMAND_CHAT_FLAGS(pm, " - Private message a player. This wi CON_COMMAND_CHAT_FLAGS(who, "- List the flags of all online players", ADMFLAG_GENERIC) { + if (!GetGlobals()) + return; + std::vector> rgNameSlotID; - for (size_t i = 0; i < gpGlobals->maxClients; i++) + for (size_t i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* ccsPly = CCSPlayerController::FromSlot(i); @@ -961,7 +896,8 @@ CON_COMMAND_CHAT_FLAGS(listdc, "- List recently disconnected players and their S CON_COMMAND_CHAT_FLAGS(endround, "- Immediately ends the round, client-side variant of endround", ADMFLAG_RCON) { - g_pGameRules->TerminateRound(0.0f, CSRoundEndReason::Draw); + if (g_pGameRules) + g_pGameRules->TerminateRound(0.0f, CSRoundEndReason::Draw); } CON_COMMAND_CHAT_FLAGS(money, " - Set a player's amount of money", ADMFLAG_CHEATS) diff --git a/src/buttonwatch.cpp b/src/buttonwatch.cpp index 7c6c5213..6cd6664c 100644 --- a/src/buttonwatch.cpp +++ b/src/buttonwatch.cpp @@ -96,7 +96,7 @@ bool IsButtonWatchEnabled() std::map mapRecentEnts; void ButtonWatch(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay) { - if (!IsButtonWatchEnabled() || V_stricmp(pThis->m_pDesc->m_pName, "OnPressed") || !pActivator || !((CBaseEntity*)pActivator)->IsPawn() || !pCaller || mapRecentEnts.contains(pCaller->GetEntityIndex().Get())) + if (!IsButtonWatchEnabled() || !GetGlobals() || V_stricmp(pThis->m_pDesc->m_pName, "OnPressed") || !pActivator || !((CBaseEntity*)pActivator)->IsPawn() || !pCaller || mapRecentEnts.contains(pCaller->GetEntityIndex().Get())) return; CCSPlayerController* ccsPlayer = CCSPlayerController::FromPawn(static_cast(pActivator)); @@ -112,7 +112,7 @@ void ButtonWatch(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEnt std::string strButton = std::to_string(pCaller->GetEntityIndex().Get()) + " " + std::string(((CBaseEntity*)pCaller)->GetName()); - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* ccsPlayer = CCSPlayerController::FromSlot(i); if (!ccsPlayer) diff --git a/src/cs2_sdk/entity/ccsweaponbase.h b/src/cs2_sdk/entity/ccsweaponbase.h index 91e03c9b..32f1378a 100644 --- a/src/cs2_sdk/entity/ccsweaponbase.h +++ b/src/cs2_sdk/entity/ccsweaponbase.h @@ -21,7 +21,7 @@ #include "cbaseentity.h" -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); enum gear_slot_t : uint32_t { @@ -97,8 +97,11 @@ class CBasePlayerWeapon : public CEconEntity void Disarm() { - m_nNextPrimaryAttackTick(MAX(m_nNextPrimaryAttackTick(), gpGlobals->tickcount + 24)); - m_nNextSecondaryAttackTick(MAX(m_nNextSecondaryAttackTick(), gpGlobals->tickcount + 24)); + if (!GetGlobals()) + return; + + m_nNextPrimaryAttackTick(MAX(m_nNextPrimaryAttackTick(), GetGlobals()->tickcount + 24)); + m_nNextSecondaryAttackTick(MAX(m_nNextSecondaryAttackTick(), GetGlobals()->tickcount + 24)); } }; diff --git a/src/cs2_sdk/entity/cgamerules.h b/src/cs2_sdk/entity/cgamerules.h index 6b491038..127ba020 100644 --- a/src/cs2_sdk/entity/cgamerules.h +++ b/src/cs2_sdk/entity/cgamerules.h @@ -65,6 +65,7 @@ class CCSGameRules : public CGameRules SCHEMA_FIELD(GameTime_t, m_fRoundStartTime) SCHEMA_FIELD(GameTime_t, m_flRestartRoundTime) SCHEMA_FIELD_POINTER(int, m_nEndMatchMapGroupVoteOptions) + SCHEMA_FIELD_POINTER(int, m_nEndMatchMapGroupVoteTypes) SCHEMA_FIELD(int, m_nEndMatchMapVoteWinner) SCHEMA_FIELD(int, m_iRoundTime) SCHEMA_FIELD(bool, m_bFreezePeriod) diff --git a/src/cs2_sdk/schema.cpp b/src/cs2_sdk/schema.cpp index efcb343a..18d42009 100644 --- a/src/cs2_sdk/schema.cpp +++ b/src/cs2_sdk/schema.cpp @@ -27,7 +27,7 @@ #include "tier0/memdbgon.h" -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); using SchemaKeyValueMap_t = CUtlMap; using SchemaTableMap_t = CUtlMap; diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index 93c42adb..f884e441 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -124,10 +124,8 @@ CS2Fixes g_CS2Fixes; IGameEventSystem* g_gameEventSystem = nullptr; IGameEventManager2* g_gameEventManager = nullptr; -INetworkGameServer* g_pNetworkGameServer = nullptr; CGameEntitySystem* g_pEntitySystem = nullptr; CEntityListener* g_pEntityListener = nullptr; -CGlobalVars* gpGlobals = nullptr; CPlayerManager* g_playerManager = nullptr; IVEngineServer2* g_pEngineServer2 = nullptr; CGameConfig* g_GameConfig = nullptr; @@ -148,6 +146,18 @@ CGameEntitySystem* GameEntitySystem() return *reinterpret_cast((uintptr_t)(g_pGameResourceServiceServer) + offset); } +// Will return null between map end & new map startup, null check if necessary! +INetworkGameServer* GetNetworkGameServer() +{ + return g_pNetworkServerService->GetIGameServer(); +} + +// Will return null between map end & new map startup, null check if necessary! +CGlobalVars* GetGlobals() +{ + return g_pEngineServer2->GetServerGlobals(); +} + PLUGIN_EXPOSE(CS2Fixes, g_CS2Fixes); bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late) { @@ -311,17 +321,16 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool RegisterEventListeners(); g_pEntitySystem = GameEntitySystem(); g_pEntitySystem->AddListenerEntity(g_pEntityListener); - g_pNetworkGameServer = g_pNetworkServerService->GetIGameServer(); - gpGlobals = g_pEngineServer2->GetServerGlobals(); } g_pAdminSystem = new CAdminSystem(); g_playerManager = new CPlayerManager(late); g_pDiscordBotManager = new CDiscordBotManager(); - g_pZRPlayerClassManager = new CZRPlayerClassManager(); g_pMapVoteSystem = new CMapVoteSystem(); + g_pVoteManager = new CVoteManager(); g_pUserPreferencesSystem = new CUserPreferencesSystem(); g_pUserPreferencesStorage = new CUserPreferencesREST(); + g_pZRPlayerClassManager = new CZRPlayerClassManager(); g_pZRWeaponConfig = new ZRWeaponConfig(); g_pZRHitgroupConfig = new ZRHitgroupConfig(); g_pEntityListener = new CEntityListener(); @@ -384,6 +393,9 @@ bool CS2Fixes::Unload(char* error, size_t maxlen) if (g_iGoToIntermissionId != -1) SH_REMOVE_HOOK_ID(g_iGoToIntermissionId); + if (g_iCGamePlayerEquipUseId != -1) + SH_REMOVE_HOOK_ID(g_iCGamePlayerEquipUseId); + ConVar_Unregister(); g_CommandList.Purge(); @@ -393,17 +405,29 @@ bool CS2Fixes::Unload(char* error, size_t maxlen) RemoveTimers(); UnregisterEventListeners(); - if (g_playerManager) - delete g_playerManager; + if (g_GameConfig) + delete g_GameConfig; if (g_pAdminSystem) delete g_pAdminSystem; + if (g_playerManager) + delete g_playerManager; + if (g_pDiscordBotManager) delete g_pDiscordBotManager; - if (g_GameConfig) - delete g_GameConfig; + if (g_pMapVoteSystem) + delete g_pMapVoteSystem; + + if (g_pVoteManager) + delete g_pVoteManager; + + if (g_pUserPreferencesSystem) + delete g_pUserPreferencesSystem; + + if (g_pUserPreferencesStorage) + delete g_pUserPreferencesStorage; if (g_pZRPlayerClassManager) delete g_pZRPlayerClassManager; @@ -414,12 +438,6 @@ bool CS2Fixes::Unload(char* error, size_t maxlen) if (g_pZRHitgroupConfig) delete g_pZRHitgroupConfig; - if (g_pUserPreferencesSystem) - delete g_pUserPreferencesSystem; - - if (g_pUserPreferencesStorage) - delete g_pUserPreferencesStorage; - if (g_pEntityListener) delete g_pEntityListener; @@ -429,9 +447,6 @@ bool CS2Fixes::Unload(char* error, size_t maxlen) if (g_pPanoramaVoteHandler) delete g_pPanoramaVoteHandler; - if (g_iCGamePlayerEquipUseId != -1) - SH_REMOVE_HOOK_ID(g_iCGamePlayerEquipUseId); - return true; } @@ -484,14 +499,14 @@ void CS2Fixes::Hook_DispatchConCommand(ConCommandHandle cmdHandle, const CComman if (pController) ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "You are flooding the server!"); } - else if (bAdminChat) // Admin chat can be sent by anyone but only seen by admins, use flood protection here too + else if (bAdminChat && GetGlobals()) // Admin chat can be sent by anyone but only seen by admins, use flood protection here too { // HACK: At this point, we can safely modify the arg buffer as it won't be passed anywhere else // The string here is originally ("@foo bar"), trim it to be (foo bar) char* pszMessage = (char*)(args.ArgS() + 2); pszMessage[V_strlen(pszMessage) - 1] = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -528,10 +543,8 @@ void CS2Fixes::Hook_DispatchConCommand(ConCommandHandle cmdHandle, const CComman void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession* pSession, const char* pszMapName) { - g_pNetworkGameServer = g_pNetworkServerService->GetIGameServer(); g_pEntitySystem = GameEntitySystem(); g_pEntitySystem->AddListenerEntity(g_pEntityListener); - gpGlobals = g_pEngineServer2->GetServerGlobals(); Message("Hook_StartupServer: %s\n", pszMapName); @@ -543,7 +556,7 @@ void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISou RegisterEventListeners(); g_pPanoramaVoteHandler->Reset(); - VoteManager_Init(); + g_pVoteManager->VoteManager_Init(); g_pIdleSystem->Reset(); } @@ -649,11 +662,11 @@ void CS2Fixes::AllPluginsLoaded() CUtlVector* GetClientList() { - if (!g_pNetworkGameServer) + if (!GetNetworkGameServer()) return nullptr; static int offset = g_GameConfig->GetOffset("CNetworkGameServer_ClientList"); - return (CUtlVector*)(&g_pNetworkGameServer[offset]); + return (CUtlVector*)(&GetNetworkGameServer()[offset]); } CServerSideClient* GetClientBySlot(CPlayerSlot slot) @@ -670,6 +683,9 @@ void FullUpdateAllClients() { auto pClients = GetClientList(); + if (!pClients) + return; + FOR_EACH_VEC(*pClients, i) (*pClients)[i]->ForceFullUpdate(); } @@ -785,10 +801,13 @@ void CS2Fixes::Hook_GameFramePost(bool simulating, bool bFirstTick, bool bLastTi VPROF_BUDGET("CS2Fixes::Hook_GameFramePost", "CS2FixesPerFrame"); + if (!GetGlobals()) + return; + if (simulating && g_bHasTicked) - g_flUniversalTime += gpGlobals->curtime - g_flLastTickedTime; + g_flUniversalTime += GetGlobals()->curtime - g_flLastTickedTime; - g_flLastTickedTime = gpGlobals->curtime; + g_flLastTickedTime = GetGlobals()->curtime; g_bHasTicked = true; for (int i = g_timers.Tail(); i != g_timers.InvalidIndex();) @@ -819,7 +838,7 @@ void CS2Fixes::Hook_GameFramePost(bool simulating, bool bFirstTick, bool bLastTi if (g_bEnableZR) CZRRegenTimer::Tick(); - EntityHandler_OnGameFramePost(simulating, gpGlobals->tickcount); + EntityHandler_OnGameFramePost(simulating, GetGlobals()->tickcount); } extern bool g_bFlashLightTransmitOthers; @@ -827,7 +846,7 @@ extern bool g_bFlashLightTransmitOthers; void CS2Fixes::Hook_CheckTransmit(CCheckTransmitInfo** ppInfoList, int infoCount, CBitVec<16384>& unionTransmitEdicts, const Entity2Networkable_t** pNetworkables, const uint16* pEntityIndicies, int nEntities, bool bEnablePVSBits) { - if (!g_pEntitySystem) + if (!g_pEntitySystem || !GetGlobals()) return; VPROF("CS2Fixes::Hook_CheckTransmit"); @@ -851,7 +870,7 @@ void CS2Fixes::Hook_CheckTransmit(CCheckTransmitInfo** ppInfoList, int infoCount if (!pSelfZEPlayer) continue; - for (int j = 0; j < gpGlobals->maxClients; j++) + for (int j = 0; j < GetGlobals()->maxClients; j++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(j); // Always transmit to themselves @@ -891,13 +910,7 @@ void CS2Fixes::Hook_CheckTransmit(CCheckTransmitInfo** ppInfoList, int infoCount void CS2Fixes::Hook_ApplyGameSettings(KeyValues* pKV) { - if (!pKV->FindKey("launchoptions")) - return; - - if (pKV->FindKey("launchoptions")->FindKey("customgamemode")) - g_pMapVoteSystem->SetCurrentWorkshopMap(pKV->FindKey("launchoptions")->GetUint64("customgamemode")); - else if (pKV->FindKey("launchoptions")->FindKey("levelname")) - g_pMapVoteSystem->SetCurrentMap(pKV->FindKey("launchoptions")->GetString("levelname")); + g_pMapVoteSystem->ApplyGameSettings(pKV); } void CS2Fixes::Hook_CreateWorkshopMapGroup(const char* name, const CUtlStringList& mapList) @@ -972,7 +985,8 @@ void CS2Fixes::Hook_PhysicsTouchShuffle(CUtlVector* pList, bool u // [Kxnrl] // seems it sorted by flags? - std::srand(gpGlobals->tickcount); + if (GetGlobals()) + std::srand(GetGlobals()->tickcount); // Fisher-Yates shuffle @@ -1010,7 +1024,7 @@ void CS2Fixes::Hook_CheckMovingGround(double frametime) CCSPlayer_MovementServices* pMove = META_IFACEPTR(CCSPlayer_MovementServices); CCSPlayerPawn* pPawn = pMove->GetPawn(); - if (!pPawn) + if (!pPawn || !GetGlobals()) RETURN_META(MRES_IGNORED); CCSPlayerController* pController = pPawn->GetOriginalController(); @@ -1024,10 +1038,10 @@ void CS2Fixes::Hook_CheckMovingGround(double frametime) // The point of doing this is to avoid running the function (and applying/resetting basevelocity) multiple times per tick // This can happen when the client or server lags - if (aPlayerTicks[iSlot] == gpGlobals->tickcount) + if (aPlayerTicks[iSlot] == GetGlobals()->tickcount) RETURN_META(MRES_SUPERCEDE); - aPlayerTicks[iSlot] = gpGlobals->tickcount; + aPlayerTicks[iSlot] = GetGlobals()->tickcount; RETURN_META(MRES_IGNORED); } @@ -1064,10 +1078,12 @@ void CS2Fixes::OnLevelInit(char const* pMapName, ZR_OnLevelInit(); } -// Potentially might not work void CS2Fixes::OnLevelShutdown() { Message("OnLevelShutdown()\n"); + + if (g_bVoteManagerEnable) + g_pMapVoteSystem->OnLevelShutdown(); } bool CS2Fixes::Pause(char* error, size_t maxlen) diff --git a/src/customio.cpp b/src/customio.cpp index 7aa5b330..e5e7e166 100644 --- a/src/customio.cpp +++ b/src/customio.cpp @@ -31,7 +31,7 @@ #include #include -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); struct AddOutputKey_t { @@ -414,13 +414,16 @@ FAKE_FLOAT_CVAR(cs2f_burn_interval, "The interval between burn damage ticks", g_ bool IgnitePawn(CCSPlayerPawn* pPawn, float flDuration, CBaseEntity* pInflictor, CBaseEntity* pAttacker, CBaseEntity* pAbility, DamageTypes_t nDamageType) { + if (!GetGlobals()) + return false; + auto pParticleEnt = reinterpret_cast(pPawn->m_hEffectEntity().Get()); // This guy is already burning, don't ignite again if (pParticleEnt) { // Override the end time instead of just adding to it so players who get a ton of ignite inputs don't burn forever - pParticleEnt->m_flDissolveStartTime = gpGlobals->curtime + flDuration; + pParticleEnt->m_flDissolveStartTime = GetGlobals()->curtime + flDuration; return true; } @@ -431,7 +434,7 @@ bool IgnitePawn(CCSPlayerPawn* pPawn, float flDuration, CBaseEntity* pInflictor, pParticleEnt->m_bStartActive(true); pParticleEnt->m_iszEffectName(g_sBurnParticle.c_str()); pParticleEnt->m_hControlPointEnts[0] = pPawn; - pParticleEnt->m_flDissolveStartTime = gpGlobals->curtime + flDuration; // Store the end time in the particle itself so we can increment if needed + pParticleEnt->m_flDissolveStartTime = GetGlobals()->curtime + flDuration; // Store the end time in the particle itself so we can increment if needed pParticleEnt->Teleport(&vecOrigin, nullptr, nullptr); pParticleEnt->DispatchSpawn(); @@ -448,7 +451,7 @@ bool IgnitePawn(CCSPlayerPawn* pPawn, float flDuration, CBaseEntity* pInflictor, new CTimer(0.f, false, false, [hPawn, hInflictor, hAttacker, hAbility, nDamageType]() { CCSPlayerPawn* pPawn = hPawn.Get(); - if (!pPawn) + if (!pPawn || !GetGlobals()) return -1.f; const auto pParticleEnt = reinterpret_cast(pPawn->m_hEffectEntity().Get()); @@ -463,7 +466,7 @@ bool IgnitePawn(CCSPlayerPawn* pPawn, float flDuration, CBaseEntity* pInflictor, return -1.f; } - if (pParticleEnt->m_flDissolveStartTime() <= gpGlobals->curtime || !pPawn->IsAlive()) + if (pParticleEnt->m_flDissolveStartTime() <= GetGlobals()->curtime || !pPawn->IsAlive()) { pParticleEnt->AcceptInput("Stop"); UTIL_AddEntityIOEvent(pParticleEnt, "Kill"); // Kill on the next frame diff --git a/src/detours.cpp b/src/detours.cpp index 5370f9a5..83506baa 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -53,7 +53,7 @@ #include "tier0/memdbgon.h" -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CGameEntitySystem* g_pEntitySystem; extern IGameEventManager2* g_gameEventManager; extern CCSGameRules* g_pGameRules; @@ -201,15 +201,15 @@ void FASTCALL Detour_TriggerPush_Touch(CTriggerPush* pPush, CBaseEntity* pOther) pOther->Teleport(&origin, nullptr, nullptr); } - if (g_bLogPushes) + if (g_bLogPushes && GetGlobals()) { Vector vecEntBaseVelocity = pOther->m_vecBaseVelocity; Vector vecOrigPush = vecAbsDir * pPush->m_flSpeed(); Message("Pushing entity %i | frame = %i | tick = %i | entity basevelocity %s = %.2f %.2f %.2f | original push velocity = %.2f %.2f %.2f | final push velocity = %.2f %.2f %.2f\n", pOther->GetEntityIndex(), - gpGlobals->framecount, - gpGlobals->tickcount, + GetGlobals()->framecount, + GetGlobals()->tickcount, (flags & FL_BASEVELOCITY) ? "WITH FLAG" : "", vecEntBaseVelocity.x, vecEntBaseVelocity.y, vecEntBaseVelocity.z, vecOrigPush.x, vecOrigPush.y, vecOrigPush.z, @@ -235,6 +235,9 @@ void SayChatMessageWithTimer(IRecipientFilter& filter, const char* pText, CCSPla { VPROF("SayChatMessageWithTimer"); + if (!GetGlobals() || !g_pGameRules) + return; + char buf[256]; // Filter console message - remove non-alphanumeric chars and convert to lowercase @@ -303,7 +306,7 @@ void SayChatMessageWithTimer(IRecipientFilter& filter, const char* pText, CCSPla } } - float fCurrentRoundClock = g_pGameRules->m_iRoundTime - (gpGlobals->curtime - g_pGameRules->m_fRoundStartTime.Get().GetTime()); + float fCurrentRoundClock = g_pGameRules->m_iRoundTime - (GetGlobals()->curtime - g_pGameRules->m_fRoundStartTime.Get().GetTime()); // Only display trigger time if the timer is greater than 4 seconds, and time expires within the round if ((uiTriggerTimerLength > 4) && (fCurrentRoundClock > uiTriggerTimerLength)) @@ -480,7 +483,7 @@ void FASTCALL Detour_ProcessMovement(CCSPlayer_MovementServices* pThis, void* pM { CCSPlayerPawn* pPawn = pThis->GetPawn(); - if (!pPawn->IsAlive()) + if (!pPawn->IsAlive() || !GetGlobals()) return ProcessMovement(pThis, pMove); CCSPlayerController* pController = pPawn->GetOriginalController(); @@ -495,13 +498,13 @@ void FASTCALL Detour_ProcessMovement(CCSPlayer_MovementServices* pThis, void* pM // Yes, this is what source1 does to scale player speed // Scale frametime during the entire movement processing step and revert right after - float flStoreFrametime = gpGlobals->frametime; + float flStoreFrametime = GetGlobals()->frametime; - gpGlobals->frametime *= flSpeedMod; + GetGlobals()->frametime *= flSpeedMod; ProcessMovement(pThis, pMove); - gpGlobals->frametime = flStoreFrametime; + GetGlobals()->frametime = flStoreFrametime; } static bool g_bDisableSubtick = false; @@ -560,8 +563,12 @@ void FASTCALL Detour_CGamePlayerEquip_InputTriggerForActivatedPlayer(CGamePlayer CServerSideClient* FASTCALL Detour_GetFreeClient(int64_t unk1, const __m128i* unk2, unsigned int unk3, int64_t unk4, char unk5, void* unk6) { + // Not sure if this function can even be called in this state, but if it is, we can't do shit anyways + if (!GetClientList() || !GetGlobals()) + return nullptr; + // Check if there is still unused slots, this should never break so just fall back to original behaviour for ease (we don't have a CServerSideClient constructor) - if (gpGlobals->maxClients != GetClientList()->Count()) + if (GetGlobals()->maxClients != GetClientList()->Count()) return GetFreeClient(unk1, unk2, unk3, unk4, unk5, unk6); // Phantom client fix diff --git a/src/entities.cpp b/src/entities.cpp index e377627c..33dcc10c 100644 --- a/src/entities.cpp +++ b/src/entities.cpp @@ -511,10 +511,10 @@ namespace CPointViewControlHandler { const auto key = pEntity->GetHandle().ToInt(); const auto it = s_repository.find(key); - if (it == s_repository.end()) + if (it == s_repository.end() || !GetGlobals()) return false; - for (auto i = 0; i < gpGlobals->maxClients; i++) + for (auto i = 0; i < GetGlobals()->maxClients; i++) { const auto pController = CCSPlayerController::FromSlot(i); if (!pController || !pController->IsConnected() || pController->IsBot() || pController->m_bIsHLTV()) diff --git a/src/events.cpp b/src/events.cpp index 6e85afb9..01980bf2 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -25,7 +25,9 @@ #include "entity/cbaseplayercontroller.h" #include "entity/cgamerules.h" #include "eventlistener.h" +#include "idlemanager.h" #include "leader.h" +#include "map_votes.h" #include "networkstringtabledefs.h" #include "panoramavote.h" #include "recipientfilters.h" @@ -37,7 +39,7 @@ extern IGameEventManager2* g_gameEventManager; extern IServerGameClients* g_pSource2GameClients; extern CGameEntitySystem* g_pEntitySystem; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; extern IVEngineServer2* g_pEngineServer2; @@ -261,10 +263,10 @@ GAME_EVENT_F(round_start) if (g_bFullAllTalk) g_pEngineServer2->ServerCommand("sv_full_alltalk 1"); - if (!g_bEnableTopDefender) + if (!g_bEnableTopDefender || !GetGlobals()) return; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -280,29 +282,14 @@ GAME_EVENT_F(round_start) GAME_EVENT_F(round_end) { if (g_bVoteManagerEnable) - { - ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_timelimit")); - - // CONVAR_TODO - // HACK: values is actually the cvar value itself, hence this ugly cast. - float flTimelimit = *(float*)&cvar->values; - - int iTimeleft = (int)((g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - gpGlobals->curtime); + g_pVoteManager->OnRoundEnd(); - // check for end of last round - if (iTimeleft <= 0) - { - g_RTVState = ERTVState::POST_LAST_ROUND_END; - g_ExtendState = EExtendState::POST_LAST_ROUND_END; - } - } - - if (!g_bEnableTopDefender) + if (!g_bEnableTopDefender || !GetGlobals()) return; CUtlVector sortedPlayers; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -365,4 +352,12 @@ GAME_EVENT_F(bullet_impact) GAME_EVENT_F(vote_cast) { g_pPanoramaVoteHandler->VoteCast(pEvent); +} + +GAME_EVENT_F(cs_win_panel_match) +{ + g_pIdleSystem->PauseIdleChecks(); + + if (!g_pMapVoteSystem->IsVoteOngoing()) + g_pMapVoteSystem->StartVote(); } \ No newline at end of file diff --git a/src/gamesystem.cpp b/src/gamesystem.cpp index 55935778..e0580124 100644 --- a/src/gamesystem.cpp +++ b/src/gamesystem.cpp @@ -22,6 +22,7 @@ #include "adminsystem.h" #include "common.h" #include "entities.h" +#include "entity/cgamerules.h" #include "gameconfig.h" #include "idlemanager.h" #include "leader.h" @@ -31,8 +32,9 @@ #include "tier0/memdbgon.h" -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CGameConfig* g_GameConfig; +extern CCSGameRules* g_pGameRules; CBaseGameSystemFactory** CBaseGameSystemFactory::sm_pFirst = nullptr; @@ -91,7 +93,9 @@ GS_EVENT_MEMBER(CGameSystem, ServerPreEntityThink) VPROF_BUDGET("CGameSystem::ServerPreEntityThink", "CS2FixesPerFrame") g_playerManager->FlashLightThink(); g_pIdleSystem->UpdateIdleTimes(); - EntityHandler_OnGameFramePre(gpGlobals->m_bInSimulation, gpGlobals->tickcount); + + if (GetGlobals()) + EntityHandler_OnGameFramePre(GetGlobals()->m_bInSimulation, GetGlobals()->tickcount); } // Called every frame after entities think @@ -100,3 +104,8 @@ GS_EVENT_MEMBER(CGameSystem, ServerPostEntityThink) VPROF_BUDGET("CGameSystem::ServerPostEntityThink", "CS2FixesPerFrame") g_playerManager->UpdatePlayerStates(); } + +GS_EVENT_MEMBER(CGameSystem, GameShutdown) +{ + g_pGameRules = nullptr; +} diff --git a/src/gamesystem.h b/src/gamesystem.h index 0a56f33b..9f472b7e 100644 --- a/src/gamesystem.h +++ b/src/gamesystem.h @@ -30,6 +30,7 @@ class CGameSystem : public CBaseGameSystem GS_EVENT(BuildGameSessionManifest); GS_EVENT(ServerPreEntityThink); GS_EVENT(ServerPostEntityThink); + GS_EVENT(GameShutdown); void Shutdown() override { diff --git a/src/idlemanager.cpp b/src/idlemanager.cpp index b30eb233..c565f68a 100644 --- a/src/idlemanager.cpp +++ b/src/idlemanager.cpp @@ -22,7 +22,7 @@ #include extern IVEngineServer2* g_pEngineServer2; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CPlayerManager* g_playerManager; CIdleSystem* g_pIdleSystem = nullptr; @@ -37,11 +37,11 @@ FAKE_BOOL_CVAR(cs2f_idle_kick_admins, "Whether to kick idle players with ADMFLAG void CIdleSystem::CheckForIdleClients() { - if (m_bPaused || g_fIdleKickTime <= 0.0f) + if (m_bPaused || g_fIdleKickTime <= 0.0f || !GetGlobals()) return; int iClientNum = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* zPlayer = g_playerManager->GetPlayer(i); @@ -51,7 +51,7 @@ void CIdleSystem::CheckForIdleClients() iClientNum++; } - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* zPlayer = g_playerManager->GetPlayer(i); @@ -88,12 +88,12 @@ void CIdleSystem::CheckForIdleClients() // Logged inputs and time for the logged inputs are updated every time this function is run. void CIdleSystem::UpdateIdleTimes() { - if (g_fIdleKickTime <= 0.0f) + if (g_fIdleKickTime <= 0.0f || !GetGlobals()) return; VPROF("CIdleSystem::UpdateIdleTimes"); - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -129,7 +129,10 @@ void CIdleSystem::Reset() { m_bPaused = false; - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); diff --git a/src/leader.cpp b/src/leader.cpp index c2583781..bd34f0bf 100644 --- a/src/leader.cpp +++ b/src/leader.cpp @@ -28,7 +28,7 @@ extern IVEngineServer2* g_pEngineServer2; extern CGameEntitySystem* g_pEntitySystem; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern IGameEventManager2* g_gameEventManager; // All colors MUST have 255 alpha @@ -178,10 +178,13 @@ std::pair GetLeaders() std::pair GetCount(int iType) { + if (!GetGlobals()) + return std::make_pair(0, ""); + int iCount = 0; std::string strPlayerNames = ""; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(CPlayerSlot(i)); if (!pPlayer) @@ -376,7 +379,13 @@ void Leader_PostEventAbstract_Source1LegacyGameEvent(const uint64* clients, cons void Leader_OnRoundStart(IGameEvent* pEvent) { - for (int i = 0; i < gpGlobals->maxClients; i++) + g_bPingWithLeader = true; + g_iMarkerCount = 0; + + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pLeader = CCSPlayerController::FromSlot((CPlayerSlot)i); if (!pLeader) @@ -394,9 +403,6 @@ void Leader_OnRoundStart(IGameEvent* pEvent) else Leader_ApplyLeaderVisuals(pawnLeader); } - - g_bPingWithLeader = true; - g_iMarkerCount = 0; } // revisit this later with a TempEnt implementation @@ -503,7 +509,7 @@ CON_COMMAND_CHAT(glows, "- List all active player glows") CON_COMMAND_CHAT(vl, " - Vote for a player to become a leader") { - if (!g_bEnableLeader) + if (!g_bEnableLeader || !GetGlobals()) return; if (!player) @@ -518,7 +524,7 @@ CON_COMMAND_CHAT(vl, " - Vote for a player to become a leader") return; } - if (gpGlobals->curtime < 60.0f) + if (GetGlobals()->curtime < 60.0f) { ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Leader voting is not open yet."); return; @@ -540,9 +546,9 @@ CON_COMMAND_CHAT(vl, " - Vote for a player to become a leader") if (!pPlayer) return; - if (pPlayer->GetLeaderVoteTime() + 30.0f > gpGlobals->curtime) + if (pPlayer->GetLeaderVoteTime() + 30.0f > GetGlobals()->curtime) { - int iRemainingTime = (int)(pPlayer->GetLeaderVoteTime() + 30.0f - gpGlobals->curtime); + int iRemainingTime = (int)(pPlayer->GetLeaderVoteTime() + 30.0f - GetGlobals()->curtime); ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Wait %i seconds before you can !vl again.", iRemainingTime); return; } @@ -571,7 +577,7 @@ CON_COMMAND_CHAT(vl, " - Vote for a player to become a leader") int iLeaderVoteCount = pPlayerTarget->GetLeaderVoteCount(); int iNeededLeaderVoteCount = (int)(g_playerManager->GetOnlinePlayerCount(false) * g_flLeaderVoteRatio) + 1; - pPlayer->SetLeaderVoteTime(gpGlobals->curtime); + pPlayer->SetLeaderVoteTime(GetGlobals()->curtime); if (iLeaderVoteCount + 1 >= iNeededLeaderVoteCount) { diff --git a/src/map_votes.cpp b/src/map_votes.cpp index b29020e7..2ae60d10 100644 --- a/src/map_votes.cpp +++ b/src/map_votes.cpp @@ -23,22 +23,25 @@ #include "ctimer.h" #include "entity/cgamerules.h" #include "eventlistener.h" -#include "idlemanager.h" +#include "iserver.h" #include "playermanager.h" #include "steam/steam_gameserver.h" #include "strtools.h" #include "utlstring.h" #include "utlvector.h" +#undef snprintf +#include "vendor/nlohmann/json.hpp" #include "votemanager.h" +#include #include +#include #include -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; extern IVEngineServer2* g_pEngineServer2; extern CSteamGameServerAPIContext g_steamAPI; extern IGameTypes* g_pGameTypes; -extern CIdleSystem* g_pIdleSystem; CMapVoteSystem* g_pMapVoteSystem = nullptr; @@ -53,7 +56,11 @@ CON_COMMAND_CHAT_FLAGS(reload_map_list, "- Reload map list, also reloads current return; } - g_pMapVoteSystem->LoadMapList(); + if (!g_pMapVoteSystem->LoadMapList() || !V_strcmp(g_pMapVoteSystem->GetCurrentMapName(), "MISSING_MAP")) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Failed to reload map list!"); + return; + } // A CUtlStringList param is also expected, but we build it in our CreateWorkshopMapGroup pre-hook anyways CALL_VIRTUAL(void, g_GameConfig->GetOffset("IGameTypes_CreateWorkshopMapGroup"), g_pGameTypes, "workshop"); @@ -63,197 +70,136 @@ CON_COMMAND_CHAT_FLAGS(reload_map_list, "- Reload map list, also reloads current if (g_pMapVoteSystem->GetCurrentWorkshopMap() != 0) V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "host_workshop_map %llu", g_pMapVoteSystem->GetCurrentWorkshopMap()); - else if (g_pMapVoteSystem->GetCurrentMap()[0] != '\0') - V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "map %s", g_pMapVoteSystem->GetCurrentMap()); + else + V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "map %s", g_pMapVoteSystem->GetCurrentMapName()); g_pEngineServer2->ServerCommand(sChangeMapCmd); ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Map list reloaded!"); } -CON_COMMAND_F(cs2f_vote_maps_cooldown, "Default number of maps to wait until a map can be voted / nominated again i.e. cooldown.", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) +CON_COMMAND_F(cs2f_vote_maps_cooldown, "Default number of hours until a map can be played again i.e. cooldown", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) { - if (!g_pMapVoteSystem) - { - Message("The map vote subsystem is not enabled.\n"); - return; - } + float fCurrentCooldown = g_pMapVoteSystem->GetDefaultMapCooldown(); if (args.ArgC() < 2) - Message("%s %d\n", args[0], g_pMapVoteSystem->GetDefaultMapCooldown()); + Msg("%s %f\n", args[0], fCurrentCooldown); else - { - int iCurrentCooldown = g_pMapVoteSystem->GetDefaultMapCooldown(); - g_pMapVoteSystem->SetDefaultMapCooldown(V_StringToInt32(args[1], iCurrentCooldown)); - } + g_pMapVoteSystem->SetDefaultMapCooldown(V_StringToFloat32(args[1], fCurrentCooldown)); } -CON_COMMAND_F(cs2f_vote_max_nominations, "Number of nominations to include per vote, out of a maximum of 10.", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) +CON_COMMAND_F(cs2f_vote_max_nominations, "Number of nominations to include per vote, out of a maximum of 10", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) { - if (!g_pMapVoteSystem) + int iMaxNominatedMaps = g_pMapVoteSystem->GetMaxNominatedMaps(); + + if (args.ArgC() < 2) + Msg("%s %d\n", args[0], iMaxNominatedMaps); + else { - Message("The map vote subsystem is not enabled.\n"); - return; + int iValue = V_StringToInt32(args[1], iMaxNominatedMaps); + + if (iValue < 0 || iValue > 10) + Msg("Value must be between 0-10!\n"); + else + g_pMapVoteSystem->SetMaxNominatedMaps(iValue); } +} + +CON_COMMAND_F(cs2f_vote_max_maps, "Number of total maps to include per vote, including nominations, out of a maximum of 10", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) +{ + int iMaxVoteMaps = g_pMapVoteSystem->GetMaxVoteMaps(); if (args.ArgC() < 2) - Message("%s %d\n", args[0], g_pMapVoteSystem->GetMaxNominatedMaps()); + Msg("%s %d\n", args[0], iMaxVoteMaps); else { - int iMaxNominatedMaps = g_pMapVoteSystem->GetMaxNominatedMaps(); - g_pMapVoteSystem->SetMaxNominatedMaps(V_StringToInt32(args[1], iMaxNominatedMaps)); + int iValue = V_StringToInt32(args[1], iMaxVoteMaps); + + if (iValue < 2 || iValue > 10) + Msg("Value must be between 2-10!\n"); + else + g_pMapVoteSystem->SetMaxVoteMaps(iValue); } } -// TODO: workshop id support for rcon admins? -CON_COMMAND_CHAT_FLAGS(setnextmap, "[mapname] - Force next map (empty to clear forced next map)", ADMFLAG_CHANGEMAP) +CON_COMMAND_CHAT_FLAGS(map, " - Change map", ADMFLAG_CHANGEMAP) { if (!g_bVoteManagerEnable) return; - int iPreviousNextMap = g_pMapVoteSystem->GetForcedNextMap(); - std::pair> response = g_pMapVoteSystem->ForceNextMap(args.ArgC() < 2 ? "" : args[1]); - - if (response.first == 0 && iPreviousNextMap == response.second[0]) + if (args.ArgC() < 2) { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "\x06%s\x01 is already the next map!", g_pMapVoteSystem->GetMapName(iPreviousNextMap)); + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !map "); return; } - switch (response.first) + std::string sMapInput = g_pMapVoteSystem->StringToLower(args[1]); + + for (int i = 0; sMapInput[i]; i++) { - case -1: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Failed to find a map matching \x06%s\x01.", args[1]); - break; - case -3: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "There is no next map to reset!"); - break; - case -4: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Multiple maps matched \x06%s\x01, try being more specific:", args[1]); + // Injection prevention, because we may pass user input to ServerCommand + if (sMapInput[i] == ';' || sMapInput[i] == '|') + return; + } - for (int i = 0; i < response.second.size() && i < 5; i++) - ClientPrint(player, HUD_PRINTTALK, "- %s", g_pMapVoteSystem->GetMapName(response.second[i])); + const char* pszMapInput = sMapInput.c_str(); - break; - } -} + if (g_pEngineServer2->IsMapValid(pszMapInput)) + { + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Changing map to \x06%s\x01...", pszMapInput); -static int __cdecl OrderStringsLexicographically(const MapIndexPair* a, const MapIndexPair* b) -{ - return V_strcasecmp(a->name, b->name); -} + new CTimer(5.0f, false, true, [sMapInput]() { + g_pEngineServer2->ChangeLevel(sMapInput.c_str(), nullptr); + return -1.0f; + }); -CON_COMMAND_CHAT(nominate, "[mapname] - Nominate a map (empty to clear nomination or list all maps)") -{ - if (!g_bVoteManagerEnable || !player) return; + } - std::pair> response = g_pMapVoteSystem->AddMapNomination(player->GetPlayerSlot(), args.ArgC() < 2 ? "" : args[1]); - ZEPlayer* pPlayer = g_playerManager->GetPlayer(player->GetPlayerSlot()); - - if (!pPlayer) - return; - - switch (response.first) - { - case NominationReturnCodes::VOTE_STARTED: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Nominations are currently disabled because the vote has already started."); - break; - case NominationReturnCodes::MAP_NOT_FOUND: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because no map matched.", args[1]); - break; - case NominationReturnCodes::MAP_DISABLED: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's disabled.", g_pMapVoteSystem->GetMapName(response.second[0])); - break; - case NominationReturnCodes::MAP_CURRENT: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's already the current map!", g_pMapVoteSystem->GetMapName(response.second[0])); - break; - case NominationReturnCodes::MAP_COOLDOWN: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's on a %i map cooldown.", g_pMapVoteSystem->GetMapName(response.second[0]), g_pMapVoteSystem->GetCooldownMap(response.second[0])); - break; - case NominationReturnCodes::MAP_MINPLAYERS: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it needs %i more players.", g_pMapVoteSystem->GetMapName(response.second[0]), g_pMapVoteSystem->GetMapMinPlayers(response.second[0]) - g_playerManager->GetOnlinePlayerCount(false)); - break; - case NominationReturnCodes::MAP_MAXPLAYERS: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it needs %i less players.", g_pMapVoteSystem->GetMapName(response.second[0]), g_playerManager->GetOnlinePlayerCount(false) - g_pMapVoteSystem->GetMapMaxPlayers(response.second[0])); - break; - case NominationReturnCodes::NOMINATION_DISABLED: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Nominations are currently disabled."); - break; - case NominationReturnCodes::NOMINATION_RESET: - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Your nomination was reset."); - g_pMapVoteSystem->ClearPlayerInfo(player->GetPlayerSlot()); - break; - case NominationReturnCodes::MAP_MULTIPLE: - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Multiple maps matched \x06%s\x01, try being more specific:", args[1]); + std::string sCommand; + std::string sMapName; + uint64 iMap = g_pMapVoteSystem->HandlePlayerMapLookup(player, pszMapInput, true); - for (int i = 0; i < response.second.size() && i < 5; i++) - ClientPrint(player, HUD_PRINTTALK, "- %s", g_pMapVoteSystem->GetMapName(response.second[i])); + if (iMap == -1) + return; - break; - } - case NominationReturnCodes::NOMINATION_RESET_FAILED: - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The list of all maps will be shown in console."); - ClientPrint(player, HUD_PRINTCONSOLE, "The list of all maps is:"); - CUtlVector vecMapNames; + if (iMap > g_pMapVoteSystem->GetMapListSize()) + { + sCommand = "host_workshop_map " + std::to_string(iMap); + sMapName = std::to_string(iMap); + } + else + { + uint64 workshopId = g_pMapVoteSystem->GetMapWorkshopId(iMap); + sMapName = g_pMapVoteSystem->GetMapName(iMap); - for (int i = 0; i < g_pMapVoteSystem->GetMapListSize(); i++) - { - if (!g_pMapVoteSystem->GetMapEnabledStatus(i)) - continue; + if (workshopId == 0) + sCommand = "map " + sMapName; + else + sCommand = "host_workshop_map " + std::to_string(workshopId); + } - MapIndexPair map; - map.name = g_pMapVoteSystem->GetMapName(i); - map.index = i; - vecMapNames.AddToTail(map); - } + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Changing map to \x06%s\x01...", sMapName.c_str()); - vecMapNames.Sort(OrderStringsLexicographically); + new CTimer(5.0f, false, true, [sCommand]() { + g_pEngineServer2->ServerCommand(sCommand.c_str()); + return -1.0f; + }); +} - FOR_EACH_VEC(vecMapNames, i) - { - const char* name = vecMapNames[i].name; - int mapIndex = vecMapNames[i].index; - int cooldown = g_pMapVoteSystem->GetCooldownMap(mapIndex); - int minPlayers = g_pMapVoteSystem->GetMapMinPlayers(mapIndex); - int maxPlayers = g_pMapVoteSystem->GetMapMaxPlayers(mapIndex); - int playerCount = g_playerManager->GetOnlinePlayerCount(false); - - if (cooldown > 0) - ClientPrint(player, HUD_PRINTCONSOLE, "- %s - Cooldown: %d", name, cooldown); - else if (mapIndex == g_pMapVoteSystem->GetCurrentMapIndex()) - ClientPrint(player, HUD_PRINTCONSOLE, "- %s - Current Map", name); - else if (playerCount < minPlayers) - ClientPrint(player, HUD_PRINTCONSOLE, "- %s - +%d Players", name, minPlayers - playerCount); - else if (playerCount > maxPlayers) - ClientPrint(player, HUD_PRINTCONSOLE, "- %s - -%d Players", name, playerCount - maxPlayers); - else - ClientPrint(player, HUD_PRINTCONSOLE, "- %s", name); - } +CON_COMMAND_CHAT_FLAGS(setnextmap, "[name/id] - Force next map (empty to clear forced next map)", ADMFLAG_CHANGEMAP) +{ + if (!g_bVoteManagerEnable) + return; - break; - } - case NominationReturnCodes::MAP_NOMINATED: - { - if (pPlayer->GetNominateTime() + 60.0f > gpGlobals->curtime) - { - int iRemainingTime = (int)(pPlayer->GetNominateTime() + 60.0f - gpGlobals->curtime); - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Wait %i seconds before you can nominate again.", iRemainingTime); - return; - } - else - { - const char* sPlayerName = player->GetPlayerName(); - const char* sMapName = g_pMapVoteSystem->GetMapName(response.second[0]); - int iNumNominations = g_pMapVoteSystem->GetTotalNominations(response.second[0]); - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01was nominated by %s. It now has %d nominations.", sMapName, sPlayerName, iNumNominations); - pPlayer->SetNominateTime(gpGlobals->curtime); - } + g_pMapVoteSystem->ForceNextMap(player, args.ArgC() < 2 ? "" : args[1]); +} - break; - } - } +CON_COMMAND_CHAT(nominate, "[mapname] - Nominate a map (empty to clear nomination or list all maps)") +{ + if (!g_bVoteManagerEnable || !player) + return; + + g_pMapVoteSystem->AttemptNomination(player, args.ArgC() < 2 ? "" : args[1]); } CON_COMMAND_CHAT(nomlist, "- List the list of nominations") @@ -261,15 +207,24 @@ CON_COMMAND_CHAT(nomlist, "- List the list of nominations") if (!g_bVoteManagerEnable) return; - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Current nominations:"); - for (int i = 0; i < g_pMapVoteSystem->GetMapListSize(); i++) + if (g_pMapVoteSystem->GetForcedNextMap() != -1) { - if (!g_pMapVoteSystem->IsMapIndexEnabled(i)) continue; - int iNumNominations = g_pMapVoteSystem->GetTotalNominations(i); - if (iNumNominations == 0) continue; - const char* sMapName = g_pMapVoteSystem->GetMapName(i); - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "- %s (%d times)\n", sMapName, iNumNominations); + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Nominations are disabled because the next map has been forced to \x06%s\x01.", g_pMapVoteSystem->GetForcedNextMapName().c_str()); + return; } + + std::unordered_map mapNominatedMaps = g_pMapVoteSystem->GetNominatedMaps(); + + if (mapNominatedMaps.size() == 0) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "No maps have been nominated yet!"); + return; + } + + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Current nominations:"); + + for (auto pair : mapNominatedMaps) + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "- %s (%d times)\n", g_pMapVoteSystem->GetMapName(pair.first), pair.second); } CON_COMMAND_CHAT(mapcooldowns, "- List the maps currently in cooldown") @@ -277,49 +232,61 @@ CON_COMMAND_CHAT(mapcooldowns, "- List the maps currently in cooldown") if (!g_bVoteManagerEnable) return; - int iMapCount = g_pMapVoteSystem->GetMapListSize(); - std::vector> vecCooldowns; - - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The list of maps in cooldown will be shown in console."); - ClientPrint(player, HUD_PRINTCONSOLE, "The list of maps in cooldown is:"); + // Use a new vector, because we want to sort the command output + std::vector> vecCooldowns; - for (int iMapIndex = 0; iMapIndex < iMapCount; iMapIndex++) + for (std::shared_ptr pCooldown : g_pMapVoteSystem->GetMapCooldowns()) { - int iCooldown = g_pMapVoteSystem->GetCooldownMap(iMapIndex); + // Only print maps that are added to maplist.cfg + if (pCooldown->IsOnCooldown() && g_pMapVoteSystem->GetMapIndexFromString(pCooldown->GetMapName()) != -1) + vecCooldowns.push_back(std::make_pair(pCooldown->GetMapName(), pCooldown->GetCurrentCooldown())); + } - if (iCooldown > 0 && g_pMapVoteSystem->GetMapEnabledStatus(iMapIndex)) - vecCooldowns.push_back(std::make_pair(g_pMapVoteSystem->GetMapName(iMapIndex), iCooldown)); + if (vecCooldowns.size() == 0) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "There are no maps on cooldown!"); + return; } - std::sort(vecCooldowns.begin(), vecCooldowns.end(), [](auto& left, auto& right) { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The list of maps in cooldown will be shown in console."); + ClientPrint(player, HUD_PRINTCONSOLE, "The list of maps in cooldown is:"); + + std::sort(vecCooldowns.begin(), vecCooldowns.end(), [](auto left, auto right) { return left.second < right.second; }); for (auto pair : vecCooldowns) - ClientPrint(player, HUD_PRINTCONSOLE, "- %s (%d maps remaining)", pair.first.c_str(), pair.second); + ClientPrint(player, HUD_PRINTCONSOLE, "- %s (%s)", pair.first.c_str(), g_pMapVoteSystem->GetMapCooldownText(pair.first.c_str(), true).c_str()); } -GAME_EVENT_F(cs_win_panel_match) +CON_COMMAND_CHAT(nextmap, "- Check the next map if it was forced") { - if (g_bVoteManagerEnable && !g_pMapVoteSystem->IsVoteOngoing()) - g_pMapVoteSystem->StartVote(); + if (!g_bVoteManagerEnable) + return; + + if (g_pMapVoteSystem->GetForcedNextMap() == -1) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Next map is pending vote, no map has been forced."); + return; + } + + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Next map is \x06%s\x01.", g_pMapVoteSystem->GetForcedNextMapName().c_str()); } -GAME_EVENT_F(endmatch_mapvote_selecting_map) +CON_COMMAND_CHAT(maplist, "- List the maps in the server") { - if (g_bVoteManagerEnable) - g_pMapVoteSystem->FinishVote(); + g_pMapVoteSystem->PrintMapList(player); } bool CMapVoteSystem::IsMapIndexEnabled(int iMapIndex) { - if (iMapIndex >= m_vecMapList.Count() || iMapIndex < 0) return false; - if (GetCooldownMap(iMapIndex) > 0 || GetCurrentMapIndex() == iMapIndex) return false; - if (!m_vecMapList[iMapIndex].IsEnabled()) return false; + if (iMapIndex >= GetMapListSize() || iMapIndex < 0) return false; + if (GetMapCooldown(iMapIndex)->IsOnCooldown() || GetCurrentMapIndex() == iMapIndex) return false; + if (!m_vecMapList[iMapIndex]->IsEnabled()) return false; int iOnlinePlayers = g_playerManager->GetOnlinePlayerCount(false); - bool bMeetsMaxPlayers = iOnlinePlayers <= m_vecMapList[iMapIndex].GetMaxPlayers(); - bool bMeetsMinPlayers = iOnlinePlayers >= m_vecMapList[iMapIndex].GetMinPlayers(); + bool bMeetsMaxPlayers = iOnlinePlayers <= GetMapMaxPlayers(iMapIndex); + bool bMeetsMinPlayers = iOnlinePlayers >= GetMapMinPlayers(iMapIndex); return bMeetsMaxPlayers && bMeetsMinPlayers; } @@ -330,101 +297,135 @@ void CMapVoteSystem::OnLevelInit(const char* pMapName) m_bIsVoteOngoing = false; m_bIntermissionStarted = false; - m_iForcedNextMapIndex = -1; + m_iForcedNextMap = -1; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < MAXPLAYERS; i++) ClearPlayerInfo(i); // Delay one tick to override any .cfg's new CTimer(0.02f, false, true, []() { g_pEngineServer2->ServerCommand("mp_match_end_changelevel 0"); + g_pEngineServer2->ServerCommand("mp_endmatch_votenextmap 1"); return -1.0f; }); - - SetCurrentMapIndex(GetMapIndexFromString(pMapName)); } void CMapVoteSystem::StartVote() { + if (!g_pGameRules) + return; + m_bIsVoteOngoing = true; - g_pIdleSystem->PauseIdleChecks(); + // Select random maps that meet requirements to appear + std::vector vecPossibleMaps; + for (int i = 0; i < GetMapListSize(); i++) + if (IsMapIndexEnabled(i)) + vecPossibleMaps.push_back(i); - // Reset the player vote counts as the vote just started - for (int i = 0; i < gpGlobals->maxClients; i++) - m_arrPlayerVotes[i] = -1; + m_iVoteSize = std::min((int)vecPossibleMaps.size(), GetMaxVoteMaps()); + bool bAbort = false; + // CONVAR_TODO + ConVar* pVoteCvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_endmatch_votenextmap")); + // HACK: values is actually the cvar value itself, hence this ugly cast. + bool bVoteEnabled = *(bool*)&pVoteCvar->values; - // If we are forcing a map, just set all vote options to that map - if (m_iForcedNextMapIndex != -1) + if (!bVoteEnabled) + { + m_bIsVoteOngoing = false; + bAbort = true; + } + else if (m_iForcedNextMap != -1) { - for (int i = 0; i < 10; i++) - g_pGameRules->m_nEndMatchMapGroupVoteOptions[i] = m_iForcedNextMapIndex; - new CTimer(6.0f, false, true, []() { g_pMapVoteSystem->FinishVote(); return -1.0f; }); - return; + bAbort = true; + } + else if (m_iVoteSize < 2) + { + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Not enough maps available for map vote, aborting! Please have an admin loosen map limits."); + Message("Not enough maps available for map vote, aborting!\n"); + g_pEngineServer2->ServerCommand("mp_match_end_changelevel 1"); // Allow game to auto-switch map again + m_bIsVoteOngoing = false; + bAbort = true; } - // Seed the randomness for the event - m_iRandomWinnerShift = rand(); - - // Select random maps not in cooldown, not disabled, and not nominated - CUtlVector vecPossibleMaps; - CUtlVector vecIncludedMaps; - GetNominatedMapsForVote(vecIncludedMaps); - for (int i = 0; i < m_vecMapList.Count(); i++) + if (bAbort) { - if (!IsMapIndexEnabled(i)) continue; - if (vecIncludedMaps.HasElement(i)) continue; - vecPossibleMaps.AddToTail(i); + // Disable the map vote + for (int i = 0; i < 10; i++) + { + g_pGameRules->m_nEndMatchMapGroupVoteTypes[i] = -1; + g_pGameRules->m_nEndMatchMapGroupVoteOptions[i] = -1; + } + + return; } + // We're checking this later, so we can always disable the map vote if mp_endmatch_votenextmap is disabled + if (!g_bVoteManagerEnable) + return; + + // Reset the player vote counts as the vote just started + for (int i = 0; i < MAXPLAYERS; i++) + m_arrPlayerVotes[i] = -1; + // Print all available maps out to console - FOR_EACH_VEC(vecPossibleMaps, i) + for (int i = 0; i < vecPossibleMaps.size(); i++) { int iPossibleMapIndex = vecPossibleMaps[i]; - Message("The %d-th possible map index %d is %s\n", i, iPossibleMapIndex, m_vecMapList[iPossibleMapIndex].GetName()); + Message("The %d-th possible map index %d is %s\n", i, iPossibleMapIndex, GetMapName(iPossibleMapIndex)); } - // Set the maps in the vote: merge nominated and possible maps, then randomly sort - int iNumMapsInVote = vecPossibleMaps.Count() + vecIncludedMaps.Count(); - if (iNumMapsInVote >= 10) iNumMapsInVote = 10; - while (vecIncludedMaps.Count() < iNumMapsInVote && vecPossibleMaps.Count() > 0) + // Seed the randomness for the event + m_iRandomWinnerShift = rand(); + + // Set the maps in the vote: merge nominated and random possible maps + std::vector vecIncludedMaps = GetNominatedMapsForVote(); + + while (vecIncludedMaps.size() < m_iVoteSize && vecPossibleMaps.size() > 0) { - int iMapToAdd = vecPossibleMaps[rand() % vecPossibleMaps.Count()]; - vecIncludedMaps.AddToTail(iMapToAdd); - vecPossibleMaps.FindAndRemove(iMapToAdd); + int iMapToAdd = vecPossibleMaps[rand() % vecPossibleMaps.size()]; + + // Do we need to add this map? It may have already been included via nomination + if (std::find(vecIncludedMaps.begin(), vecIncludedMaps.end(), iMapToAdd) == vecIncludedMaps.end()) + vecIncludedMaps.push_back(iMapToAdd); + + std::erase_if(vecPossibleMaps, [iMapToAdd](int i) { return i == iMapToAdd; }); } // Randomly sort the chosen maps for (int i = 0; i < 10; i++) { - if (i < iNumMapsInVote) + if (i < m_iVoteSize) { - int iMapToAdd = vecIncludedMaps[rand() % vecIncludedMaps.Count()]; + int iMapToAdd = vecIncludedMaps[rand() % vecIncludedMaps.size()]; + g_pGameRules->m_nEndMatchMapGroupVoteTypes[i] = 0; g_pGameRules->m_nEndMatchMapGroupVoteOptions[i] = iMapToAdd; - vecIncludedMaps.FindAndRemove(iMapToAdd); + std::erase_if(vecIncludedMaps, [iMapToAdd](int i) { return i == iMapToAdd; }); } else { + g_pGameRules->m_nEndMatchMapGroupVoteTypes[i] = -1; g_pGameRules->m_nEndMatchMapGroupVoteOptions[i] = -1; } } // Print the maps chosen in the vote to console - for (int i = 0; i < iNumMapsInVote; i++) + for (int i = 0; i < m_iVoteSize; i++) { int iMapIndex = g_pGameRules->m_nEndMatchMapGroupVoteOptions[i]; - Message("The %d-th chosen map index %d is %s\n", i, iMapIndex, m_vecMapList[iMapIndex].GetName()); + Message("The %d-th chosen map index %d is %s\n", i, iMapIndex, GetMapName(iMapIndex)); } // Start the end-of-vote timer to finish the vote - ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_endmatch_votenextleveltime")); - float flVoteTime = *(float*)&cvar->values; + // CONVAR_TODO + ConVar* pVoteTimeCvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_endmatch_votenextleveltime")); + float flVoteTime = *(float*)&pVoteTimeCvar->values; new CTimer(flVoteTime, false, true, []() { g_pMapVoteSystem->FinishVote(); return -1.0; @@ -434,7 +435,11 @@ void CMapVoteSystem::StartVote() int CMapVoteSystem::GetTotalNominations(int iMapIndex) { int iNumNominations = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + + if (!GetGlobals()) + return iNumNominations; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { auto pController = CCSPlayerController::FromSlot(i); if (pController && pController->IsConnected() && m_arrPlayerNominations[i] == iMapIndex) @@ -445,7 +450,8 @@ int CMapVoteSystem::GetTotalNominations(int iMapIndex) void CMapVoteSystem::FinishVote() { - if (!m_bIsVoteOngoing) return; + if (!m_bIsVoteOngoing || !g_pGameRules) + return; // Clean up the ongoing voting state and variables m_bIsVoteOngoing = false; @@ -453,26 +459,31 @@ void CMapVoteSystem::FinishVote() // Get the winning map bool bIsNextMapVoted = UpdateWinningMap(); int iNextMapVoteIndex = WinningMapIndex(); + bool bIsNextMapForced = m_iForcedNextMap != -1; + char buffer[256]; + uint64 iWinningMap; // Map index OR possibly workshop ID if next map was forced - // If we are forcing the map, show different text - bool bIsNextMapForced = m_iForcedNextMapIndex != -1; if (bIsNextMapForced) { - iNextMapVoteIndex = 0; - g_pGameRules->m_nEndMatchMapGroupVoteOptions[0] = m_iForcedNextMapIndex; - g_pGameRules->m_nEndMatchMapVoteWinner = iNextMapVoteIndex; + iWinningMap = m_iForcedNextMap; } + else + { + if (iNextMapVoteIndex == -1) + { + Panic("Failed to count map votes, file a bug\n"); + iNextMapVoteIndex = 0; + } - // Print out the winning map - if (iNextMapVoteIndex < 0) iNextMapVoteIndex = -1; - g_pGameRules->m_nEndMatchMapVoteWinner = iNextMapVoteIndex; - int iWinningMap = g_pGameRules->m_nEndMatchMapGroupVoteOptions[iNextMapVoteIndex]; - char buffer[256]; + g_pGameRules->m_nEndMatchMapVoteWinner = iNextMapVoteIndex; + iWinningMap = g_pGameRules->m_nEndMatchMapGroupVoteOptions[iNextMapVoteIndex]; + } - if (bIsNextMapVoted) + // Print out the map we're changing to + if (bIsNextMapForced) + V_snprintf(buffer, sizeof(buffer), "The vote was overriden. \x06%s\x01 will be the next map!\n", GetForcedNextMapName().c_str()); + else if (bIsNextMapVoted) V_snprintf(buffer, sizeof(buffer), "The vote has ended. \x06%s\x01 will be the next map!\n", GetMapName(iWinningMap)); - else if (bIsNextMapForced) - V_snprintf(buffer, sizeof(buffer), "The vote was overriden. \x06%s\x01 will be the next map!\n", GetMapName(iWinningMap)); else V_snprintf(buffer, sizeof(buffer), "No map was chosen. \x06%s\x01 will be the next map!\n", GetMapName(iWinningMap)); @@ -480,36 +491,28 @@ void CMapVoteSystem::FinishVote() Message(buffer); // Print vote result information: how many votes did each map get? - int arrMapVotes[10] = {0}; - Message("Map vote result --- total votes per map:\n"); - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!bIsNextMapForced && GetGlobals()) { - auto pController = CCSPlayerController::FromSlot(i); - int iPlayerVotedIndex = m_arrPlayerVotes[i]; - if (pController && pController->IsConnected() && iPlayerVotedIndex >= 0) - arrMapVotes[iPlayerVotedIndex]++; - } - for (int i = 0; i < 10; i++) - { - int iMapIndex = g_pGameRules->m_nEndMatchMapGroupVoteOptions[i]; - const char* sIsWinner = (i == iNextMapVoteIndex) ? "(WINNER)" : ""; - Message("- %s got %d votes\n", GetMapName(iMapIndex), arrMapVotes[i]); + int arrMapVotes[10] = {0}; + Message("Map vote result --- total votes per map:\n"); + for (int i = 0; i < GetGlobals()->maxClients; i++) + { + auto pController = CCSPlayerController::FromSlot(i); + int iPlayerVotedIndex = m_arrPlayerVotes[i]; + if (pController && pController->IsConnected() && iPlayerVotedIndex >= 0) + arrMapVotes[iPlayerVotedIndex]++; + } + for (int i = 0; i < m_iVoteSize; i++) + { + int iMapIndex = g_pGameRules->m_nEndMatchMapGroupVoteOptions[i]; + Message("- %s got %d votes\n", GetMapName(iMapIndex), arrMapVotes[i]); + } } - // Put the map on cooldown as we transition to the next map if map index is valid, also decrease cooldown remaining for others - // Map index will be invalid for any map not added to maplist.cfg - DecrementAllMapCooldowns(); - - int iMapIndex = GetCurrentMapIndex(); - if (iMapIndex >= 0 && iMapIndex < GetMapListSize()) - PutMapOnCooldown(iMapIndex); - - WriteMapCooldownsToFile(); - // Wait a second and force-change the map new CTimer(1.0, false, true, [iWinningMap]() { char sChangeMapCmd[128]; - uint64 workshopId = g_pMapVoteSystem->GetMapWorkshopId(iWinningMap); + uint64 workshopId = iWinningMap > g_pMapVoteSystem->GetMapListSize() ? iWinningMap : g_pMapVoteSystem->GetMapWorkshopId(iWinningMap); if (workshopId == 0) V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "map %s", g_pMapVoteSystem->GetMapName(iWinningMap)); @@ -524,12 +527,12 @@ void CMapVoteSystem::FinishVote() bool CMapVoteSystem::RegisterPlayerVote(CPlayerSlot iPlayerSlot, int iVoteOption) { CCSPlayerController* pController = CCSPlayerController::FromSlot(iPlayerSlot); - if (!pController || !m_bIsVoteOngoing) return false; - if (iVoteOption < 0 || iVoteOption >= 10) return false; + if (!pController || !m_bIsVoteOngoing || !g_pGameRules) return false; + if (iVoteOption < 0 || iVoteOption >= m_iVoteSize) return false; // Filter out votes on invalid maps int iMapIndexToVote = g_pGameRules->m_nEndMatchMapGroupVoteOptions[iVoteOption]; - if (iMapIndexToVote < 0 || iMapIndexToVote >= m_vecMapList.Count()) return false; + if (iMapIndexToVote < 0 || iMapIndexToVote >= GetMapListSize()) return false; // Set the vote for the player int iSlot = pController->GetPlayerSlot(); @@ -549,7 +552,7 @@ bool CMapVoteSystem::RegisterPlayerVote(CPlayerSlot iPlayerSlot, int iVoteOption bool CMapVoteSystem::UpdateWinningMap() { int iWinningMapIndex = WinningMapIndex(); - if (iWinningMapIndex >= 0) + if (iWinningMapIndex >= 0 && g_pGameRules) { g_pGameRules->m_nEndMatchMapVoteWinner = iWinningMapIndex; return true; @@ -559,9 +562,12 @@ bool CMapVoteSystem::UpdateWinningMap() int CMapVoteSystem::WinningMapIndex() { + if (!GetGlobals()) + return -1; + // Count the votes of every player int arrMapVotes[10] = {0}; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { auto pController = CCSPlayerController::FromSlot(i); if (pController && pController->IsConnected() && m_arrPlayerVotes[i] >= 0) @@ -570,18 +576,18 @@ int CMapVoteSystem::WinningMapIndex() // Identify the max. number of votes int iMaxVotes = 0; - for (int i = 0; i < 10; i++) + for (int i = 0; i < m_iVoteSize; i++) iMaxVotes = (arrMapVotes[i] > iMaxVotes) ? arrMapVotes[i] : iMaxVotes; // Identify how many maps are tied with the max number of votes int iMapsWithMaxVotes = 0; - for (int i = 0; i < 10; i++) + for (int i = 0; i < m_iVoteSize; i++) if (arrMapVotes[i] == iMaxVotes) iMapsWithMaxVotes++; // Break ties: 'random' map with the most votes int iWinningMapTieBreak = m_iRandomWinnerShift % iMapsWithMaxVotes; int iWinningMapCount = 0; - for (int i = 0; i < 10; i++) + for (int i = 0; i < m_iVoteSize; i++) { if (arrMapVotes[i] == iMaxVotes) { @@ -592,54 +598,145 @@ int CMapVoteSystem::WinningMapIndex() return -1; } -void CMapVoteSystem::GetNominatedMapsForVote(CUtlVector& vecChosenNominatedMaps) +std::unordered_map CMapVoteSystem::GetNominatedMaps() { - int iNumDistinctMaps = 0; - CUtlVector vecAvailableNominatedMaps; - for (int i = 0; i < gpGlobals->maxClients; i++) + std::unordered_map mapNominatedMaps; + + if (!GetGlobals()) + return mapNominatedMaps; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { + CCSPlayerController* pController = CCSPlayerController::FromSlot(i); int iNominatedMapIndex = m_arrPlayerNominations[i]; // Introduce nominated map indexes and count the total number - if (iNominatedMapIndex != -1) + if (iNominatedMapIndex != -1 && pController && pController->IsConnected() && IsMapIndexEnabled(iNominatedMapIndex)) + ++mapNominatedMaps[iNominatedMapIndex]; + } + + return mapNominatedMaps; +} + +std::vector CMapVoteSystem::GetNominatedMapsForVote() +{ + std::unordered_map mapOriginalNominatedMaps = GetNominatedMaps(); // Original nominations map + std::unordered_map mapAvailableNominatedMaps(mapOriginalNominatedMaps); // A copy of the map that we can remove from without worry + std::vector vecTiedNominations; // Nominations with tied nom counts + std::vector vecChosenNominatedMaps; // Final vector of chosen nominations + int iMapsToIncludeInNominate = std::min({(int)mapOriginalNominatedMaps.size(), GetMaxNominatedMaps(), GetMaxVoteMaps()}); + int iMostNominations; + auto rng = std::default_random_engine{std::random_device{}()}; + + // Select top maps by number of nominations + while (vecChosenNominatedMaps.size() < iMapsToIncludeInNominate) + { + if (vecTiedNominations.size() == 0) { - if (!vecAvailableNominatedMaps.HasElement(iNominatedMapIndex)) - iNumDistinctMaps++; - vecAvailableNominatedMaps.AddToTail(iNominatedMapIndex); + // Find highest nomination count + iMostNominations = std::max_element( + mapAvailableNominatedMaps.begin(), mapAvailableNominatedMaps.end(), + [](const std::pair& p1, const std::pair& p2) { + return p1.second < p2.second; + }) + ->second; + + // Copy the most nominated maps to a new vector + for (auto pair : mapAvailableNominatedMaps) + if (pair.second == iMostNominations) + vecTiedNominations.push_back(pair.first); + + // Randomize the vector order + std::ranges::shuffle(vecTiedNominations, rng); } + + // Pick map from front of vector, and remove from both sources + vecChosenNominatedMaps.push_back(vecTiedNominations.front()); + mapAvailableNominatedMaps.erase(vecTiedNominations.front()); + vecTiedNominations.erase(vecTiedNominations.begin()); } - // Randomly select maps out of the set of nominated maps - // weighting by number of nominations, and returning a random order - int iMapsToIncludeInNominate = (iNumDistinctMaps < m_iMaxNominatedMaps) ? iNumDistinctMaps : m_iMaxNominatedMaps; - while (vecChosenNominatedMaps.Count() < iMapsToIncludeInNominate) + if (!GetGlobals()) + return vecChosenNominatedMaps; + + // Notify nomination owners about the state of their nominations + for (int i = 0; i < GetGlobals()->maxClients; i++) { - int iMapToAdd = vecAvailableNominatedMaps[rand() % vecAvailableNominatedMaps.Count()]; - vecChosenNominatedMaps.AddToTail(iMapToAdd); - while (vecAvailableNominatedMaps.HasElement(iMapToAdd)) - vecAvailableNominatedMaps.FindAndRemove(iMapToAdd); + int iNominatedMapIndex = m_arrPlayerNominations[i]; + CCSPlayerController* pController = CCSPlayerController::FromSlot(i); + + if (!pController || !pController->IsConnected()) + continue; + + // Ignore unset nominations (negative index) + if (iNominatedMapIndex < 0) + continue; + + int iNominations = mapOriginalNominatedMaps[iNominatedMapIndex]; + // At this point, iMostNominations represents nomination count of last map to make the map vote + int iNominationsNeeded = iMostNominations - iNominations; + + // Bad RNG, needed 1 more for guaranteed selection then + if (iNominationsNeeded == 0) + iNominationsNeeded = 1; + + if (std::find(vecChosenNominatedMaps.begin(), vecChosenNominatedMaps.end(), iNominatedMapIndex) != vecChosenNominatedMaps.end()) + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Your \x06%s\x01 nomination made it to the map vote with \x06%i nomination%s\x01.", GetMapName(iNominatedMapIndex), iNominations, iNominations > 1 ? "s" : ""); + else + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Your \x06%s\x01 nomination failed to make the map vote, it needed \x06%i more nomination%s\x01 for a total of \x06%i nominations\x01.", + GetMapName(iNominatedMapIndex), iNominationsNeeded, iNominationsNeeded > 1 ? "s" : "", iNominations + iNominationsNeeded); } + + return vecChosenNominatedMaps; } std::vector CMapVoteSystem::GetMapIndexesFromSubstring(const char* sMapSubstring) { std::vector vecMaps; - FOR_EACH_VEC(m_vecMapList, i) - { - if (V_stristr(m_vecMapList[i].GetName(), sMapSubstring)) + for (int i = 0; i < GetMapListSize(); i++) + if (V_stristr(GetMapName(i), sMapSubstring)) vecMaps.push_back(i); - } return vecMaps; } -int CMapVoteSystem::GetMapIndexFromString(const char* sMapString) +uint64 CMapVoteSystem::HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAllowWorkshopID) { - FOR_EACH_VEC(m_vecMapList, i) + if (bAllowWorkshopID) { - if (!V_strcasecmp(m_vecMapList[i].GetName(), sMapString)) - return i; + uint64 iWorkshopID = V_StringToUint64(sMapSubstring, 0, NULL, NULL, PARSING_FLAG_SKIP_WARNING); + + // Check if input is numeric (workshop ID) + // Not safe to expose to all admins until crashing on failed workshop addon downloads is fixed + if ((!pController || pController->GetZEPlayer()->IsAdminFlagSet(ADMFLAG_RCON)) && iWorkshopID != 0) + { + // Try to get a head start on downloading the map if needed + g_steamAPI.SteamUGC()->DownloadItem(iWorkshopID, false); + + return iWorkshopID; + } + } + + std::vector foundIndexes = GetMapIndexesFromSubstring(sMapSubstring); + + if (foundIndexes.size() > 0) + { + if (foundIndexes.size() > 1) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Multiple maps matched \x06%s\x01, try being more specific:", sMapSubstring); + + for (int i = 0; i < foundIndexes.size() && i < 5; i++) + ClientPrint(pController, HUD_PRINTTALK, "- %s", GetMapName(foundIndexes[i])); + } + else + { + return foundIndexes[0]; + } + } + else + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Failed to find a map matching \x06%s\x01.", sMapSubstring); } return -1; @@ -654,102 +751,179 @@ void CMapVoteSystem::ClearPlayerInfo(int iSlot) m_arrPlayerVotes[iSlot] = -1; } -std::pair> CMapVoteSystem::AddMapNomination(CPlayerSlot iPlayerSlot, const char* sMapSubstring) +int CMapVoteSystem::GetMapIndexFromString(const char* pszMapString) +{ + for (int i = 0; i < GetMapListSize(); i++) + if (!V_strcasecmp(GetMapName(i), pszMapString)) + return i; + + return -1; +} + +std::shared_ptr CMapVoteSystem::GetGroupFromString(const char* pszName) { - if (m_bIsVoteOngoing) return std::make_pair(NominationReturnCodes::VOTE_STARTED, std::vector()); - if (m_iForcedNextMapIndex != -1 || m_iMaxNominatedMaps == 0) return std::make_pair(NominationReturnCodes::NOMINATION_DISABLED, std::vector()); + for (int i = 0; i < m_vecGroups.size(); i++) + if (!V_strcmp(m_vecGroups[i]->GetName(), pszName)) + return m_vecGroups[i]; - int iSlot = iPlayerSlot.Get(); + return nullptr; +} + +void CMapVoteSystem::AttemptNomination(CCSPlayerController* pController, const char* sMapSubstring) +{ + int iSlot = pController->GetPlayerSlot(); + ZEPlayer* pPlayer = g_playerManager->GetPlayer(iSlot); + + if (!pPlayer || !GetGlobals()) + return; + + if (GetMaxNominatedMaps() == 0) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Nominations are currently disabled."); + return; + } + + if (GetForcedNextMap() != -1) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Nominations are disabled because the next map has been forced to \x06%s\x01.", GetForcedNextMapName().c_str()); + return; + } + + if (IsVoteOngoing()) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Nominations are disabled because the vote has already started."); + return; + } if (sMapSubstring[0] == '\0') { - // If we are resetting the nomination, return NOMINATION_RESET if (m_arrPlayerNominations[iSlot] != -1) { - m_arrPlayerNominations[iSlot] = -1; - return std::make_pair(NominationReturnCodes::NOMINATION_RESET, std::vector()); + ClearPlayerInfo(iSlot); + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Your nomination was reset."); } else { - return std::make_pair(NominationReturnCodes::NOMINATION_RESET_FAILED, std::vector()); + PrintMapList(pController); } - } - - // We are not reseting the nomination: is the map found? is it valid? - std::vector foundIndexes = GetMapIndexesFromSubstring(sMapSubstring); - int iOnlinePlayers = g_playerManager->GetOnlinePlayerCount(false); - if (foundIndexes.size() == 0) - return std::make_pair(NominationReturnCodes::MAP_NOT_FOUND, std::vector()); + return; + } - if (foundIndexes.size() > 1) - return std::make_pair(NominationReturnCodes::MAP_MULTIPLE, foundIndexes); + int iFoundIndex = HandlePlayerMapLookup(pController, sMapSubstring); + int iPlayerCount = g_playerManager->GetOnlinePlayerCount(false); - int iFoundIndex = foundIndexes[0]; + if (iFoundIndex == -1) + return; if (!GetMapEnabledStatus(iFoundIndex)) - return std::make_pair(NominationReturnCodes::MAP_DISABLED, foundIndexes); + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's disabled.", GetMapName(iFoundIndex)); + return; + } if (GetCurrentMapIndex() == iFoundIndex) - return std::make_pair(NominationReturnCodes::MAP_CURRENT, foundIndexes); + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's already the current map!", GetMapName(iFoundIndex)); + return; + } - if (GetCooldownMap(iFoundIndex) > 0) - return std::make_pair(NominationReturnCodes::MAP_COOLDOWN, foundIndexes); + if (GetMapCooldown(iFoundIndex)->IsOnCooldown()) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it's on a %s cooldown.", GetMapName(iFoundIndex), GetMapCooldownText(iFoundIndex, false).c_str()); + return; + } - if (iOnlinePlayers < m_vecMapList[iFoundIndex].GetMinPlayers()) - return std::make_pair(NominationReturnCodes::MAP_MINPLAYERS, foundIndexes); + if (iPlayerCount < GetMapMinPlayers(iFoundIndex)) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it needs %i more players.", GetMapName(iFoundIndex), GetMapMinPlayers(iFoundIndex) - iPlayerCount); + return; + } - if (iOnlinePlayers > m_vecMapList[iFoundIndex].GetMaxPlayers()) - return std::make_pair(NominationReturnCodes::MAP_MAXPLAYERS, foundIndexes); + if (iPlayerCount > GetMapMaxPlayers(iFoundIndex)) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Cannot nominate \x06%s\x01 because it needs %i less players.", GetMapName(iFoundIndex), iPlayerCount - GetMapMaxPlayers(iFoundIndex)); + return; + } + + if (pPlayer->GetNominateTime() + 60.0f > GetGlobals()->curtime) + { + int iRemainingTime = (int)(pPlayer->GetNominateTime() + 60.0f - GetGlobals()->curtime); + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Wait %i seconds before you can nominate again.", iRemainingTime); + return; + } m_arrPlayerNominations[iSlot] = iFoundIndex; - return std::make_pair(NominationReturnCodes::MAP_NOMINATED, foundIndexes); + int iNominations = GetTotalNominations(iFoundIndex); + + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01was nominated by %s. It now has %d nomination%s.", GetMapName(iFoundIndex), pController->GetPlayerName(), iNominations, iNominations > 1 ? "s" : ""); + pPlayer->SetNominateTime(GetGlobals()->curtime); +} + +void CMapVoteSystem::PrintMapList(CCSPlayerController* pController) +{ + std::vector> vecSortedMaps; + int iPlayerCount = g_playerManager->GetOnlinePlayerCount(false); + + for (int i = 0; i < GetMapListSize(); i++) + if (GetMapEnabledStatus(i)) + vecSortedMaps.push_back(std::make_pair(i, GetMapName(i))); + + std::sort(vecSortedMaps.begin(), vecSortedMaps.end(), [](auto left, auto right) { + return V_strcasecmp(right.second.c_str(), left.second.c_str()) > 0; + }); + + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "The list of all maps will be shown in console."); + ClientPrint(pController, HUD_PRINTCONSOLE, "The list of all maps is:"); + + for (std::pair pair : vecSortedMaps) + { + int mapIndex = pair.first; + const char* name = pair.second.c_str(); + int minPlayers = GetMapMinPlayers(mapIndex); + int maxPlayers = GetMapMaxPlayers(mapIndex); + + if (mapIndex == GetCurrentMapIndex()) + ClientPrint(pController, HUD_PRINTCONSOLE, "- %s - Current Map", name); + else if (GetMapCooldown(mapIndex)->IsOnCooldown()) + ClientPrint(pController, HUD_PRINTCONSOLE, "- %s - Cooldown: %s", name, GetMapCooldownText(mapIndex, true).c_str()); + else if (iPlayerCount < minPlayers) + ClientPrint(pController, HUD_PRINTCONSOLE, "- %s - +%d Players", name, minPlayers - iPlayerCount); + else if (iPlayerCount > maxPlayers) + ClientPrint(pController, HUD_PRINTCONSOLE, "- %s - -%d Players", name, iPlayerCount - maxPlayers); + else + ClientPrint(pController, HUD_PRINTCONSOLE, "- %s", name); + } } -std::pair> CMapVoteSystem::ForceNextMap(const char* sMapSubstring) +void CMapVoteSystem::ForceNextMap(CCSPlayerController* pController, const char* sMapSubstring) { if (sMapSubstring[0] == '\0') { - if (m_iForcedNextMapIndex != -1) + if (GetForcedNextMap() == -1) { - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01is no longer the forced next map.\n", m_vecMapList[m_iForcedNextMapIndex].GetName()); - m_iForcedNextMapIndex = -1; - return std::make_pair(-2, std::vector()); + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "There is no next map to reset!"); + } + else + { + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01is no longer the forced next map.\n", GetForcedNextMapName().c_str()); + m_iForcedNextMap = -1; } - return std::make_pair(-3, std::vector()); + return; } - std::vector foundIndexes = GetMapIndexesFromSubstring(sMapSubstring); - - if (foundIndexes.size() == 0) - return std::make_pair(-1, foundIndexes); - - if (foundIndexes.size() > 1) - return std::make_pair(-4, foundIndexes); + uint64 iFoundMap = HandlePlayerMapLookup(pController, sMapSubstring, true); - int iFoundIndex = foundIndexes[0]; - - if (m_iForcedNextMapIndex == iFoundIndex) - return std::make_pair(0, foundIndexes); + if (GetForcedNextMap() == iFoundMap) + { + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "\x06%s\x01 is already the next map!", GetForcedNextMapName().c_str()); + return; + } // When found, print the map and store the forced map - m_iForcedNextMapIndex = iFoundIndex; - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01has been forced as the next map.\n", m_vecMapList[iFoundIndex].GetName()); - return std::make_pair(0, foundIndexes); -} - -static int __cdecl OrderMapsByWorkshopId(const CMapInfo* a, const CMapInfo* b) -{ - int valueA = a->GetWorkshopId(); - int valueB = b->GetWorkshopId(); - - if (valueA < valueB) - return -1; - else if (valueA == valueB) - return 0; - else - return 1; + m_iForcedNextMap = iFoundMap; + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01has been forced as the next map.\n", GetForcedNextMapName().c_str()); } void CMapVoteSystem::PrintDownloadProgress() @@ -800,15 +974,31 @@ bool CMapVoteSystem::LoadMapList() { // This is called when the Steam API is init'd, now is the time to register this m_CallbackDownloadItemResult.Register(this, &CMapVoteSystem::OnMapDownloaded); + m_vecMapList.clear(); + m_vecGroups.clear(); + m_vecCooldowns.clear(); - m_vecMapList.Purge(); - KeyValues* pKV = new KeyValues("maplist"); - KeyValues::AutoDelete autoDelete(pKV); + const char* pszJsonPath = "addons/cs2fixes/configs/maplist.jsonc"; + char szPath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszJsonPath); + std::ifstream jsonFile(szPath); - const char* pszPath = "addons/cs2fixes/configs/maplist.cfg"; - if (!pKV->LoadFromFile(g_pFullFileSystem, pszPath)) + if (!jsonFile.is_open()) { - Panic("Failed to load %s\n", pszPath); + if (!ConvertMapListKVToJSON()) + { + Panic("Failed to open %s and convert KV1 maplist.cfg to JSON format, map list not loaded!\n", pszJsonPath); + return false; + } + + jsonFile.open(szPath); + } + + ordered_json jsonMaps = ordered_json::parse(jsonFile, nullptr, false, true); + + if (jsonMaps.is_discarded()) + { + Panic("Failed parsing JSON from %s, map list not loaded!\n", pszJsonPath); return false; } @@ -819,44 +1009,56 @@ bool CMapVoteSystem::LoadMapList() if (!pKVcooldowns->LoadFromFile(g_pFullFileSystem, pszCooldownFilePath)) Message("Failed to load cooldown file at %s - resetting all cooldowns to 0\n", pszCooldownFilePath); - // KV1 has some funny behaviour with capitalization, to ensure consistency we can't directly lookup case-sensitive key names - std::unordered_map mapCooldowns; - for (KeyValues* pKey = pKVcooldowns->GetFirstSubKey(); pKey; pKey = pKey->GetNextKey()) { - std::string sMapName = pKey->GetName(); - int iCooldown = pKey->GetInt(); + time_t timeCooldown = pKey->GetUint64(); - for (int i = 0; sMapName[i]; i++) - sMapName[i] = tolower(sMapName[i]); + if (timeCooldown > std::time(0)) + { + std::shared_ptr pCooldown = std::make_shared(pKey->GetName()); - mapCooldowns[sMapName] = iCooldown; + pCooldown->SetTimeCooldown(timeCooldown); + m_vecCooldowns.push_back(pCooldown); + } } - for (KeyValues* pKey = pKV->GetFirstSubKey(); pKey; pKey = pKey->GetNextKey()) + for (auto& [sSection, jsonSection] : jsonMaps.items()) { - const char* pszName = pKey->GetName(); - std::string sName = pszName; + for (auto& [sEntry, jsonEntry] : jsonSection.items()) + { + if (sSection == "Groups") + { + m_vecGroups.push_back(std::make_shared(sEntry, jsonEntry.value("enabled", true), jsonEntry.value("cooldown", 0.0f))); + } + else if (sSection == "Maps") + { + // Seems like uint64 needs special handling + uint64 iWorkshopId = 0; + + if (jsonEntry.contains("workshop_id")) + iWorkshopId = jsonEntry["workshop_id"].get(); - for (int i = 0; sName[i]; i++) - sName[i] = tolower(sName[i]); + bool bIsEnabled = jsonEntry.value("enabled", true); + int iMinPlayers = jsonEntry.value("min_players", 0); + int iMaxPlayers = jsonEntry.value("max_players", 64); + float fCooldown = jsonEntry.value("cooldown", 0.0f); + std::vector vecGroups; - uint64 iWorkshopId = pKey->GetUint64("workshop_id"); - bool bIsEnabled = pKey->GetBool("enabled", true); - int iMinPlayers = pKey->GetInt("min_players", 0); - int iMaxPlayers = pKey->GetInt("max_players", 64); - int iBaseCooldown = pKey->GetInt("cooldown", m_iDefaultMapCooldown); - int iCurrentCooldown = mapCooldowns[sName]; + if (jsonEntry.contains("groups") && jsonEntry["groups"].size() > 0) + for (auto& [key, group] : jsonEntry["groups"].items()) + vecGroups.push_back(group); - if (iWorkshopId != 0) - QueueMapDownload(iWorkshopId); + if (iWorkshopId != 0) + QueueMapDownload(iWorkshopId); - // We just append the maps to the map list - m_vecMapList.AddToTail(CMapInfo(pszName, iWorkshopId, bIsEnabled, iMinPlayers, iMaxPlayers, iBaseCooldown, iCurrentCooldown)); + // We just append the maps to the map list + m_vecMapList.push_back(std::make_shared(sEntry, iWorkshopId, bIsEnabled, iMinPlayers, iMaxPlayers, fCooldown, vecGroups)); + } + } } new CTimer(0.f, true, true, []() { - if (g_pMapVoteSystem->m_DownloadQueue.Count() == 0) + if (g_pMapVoteSystem->GetDownloadQueueSize() == 0) return -1.f; g_pMapVoteSystem->PrintDownloadProgress(); @@ -864,18 +1066,22 @@ bool CMapVoteSystem::LoadMapList() return 1.f; }); - // Sort the map list by the workshop id - m_vecMapList.Sort(OrderMapsByWorkshopId); - - // Print all the maps to show the order - FOR_EACH_VEC(m_vecMapList, i) + // Print all the maps + for (int i = 0; i < GetMapListSize(); i++) { - CMapInfo map = m_vecMapList[i]; + std::shared_ptr map = m_vecMapList[i]; + std::string groups = ""; + + for (std::string group : map->GetGroups()) + groups += group + ", "; - if (map.GetWorkshopId() == 0) - ConMsg("Map %d is %s, which is %s. MinPlayers: %d MaxPlayers: %d Cooldown: %d\n", i, map.GetName(), map.IsEnabled() ? "enabled" : "disabled", map.GetMinPlayers(), map.GetMaxPlayers(), map.GetBaseCooldown()); + if (groups != "") + groups.erase(groups.length() - 2); + + if (map->GetWorkshopId() == 0) + ConMsg("Map %d is %s, which is %s. MinPlayers: %d MaxPlayers: %d Custom Cooldown: %.2f Groups: %s\n", i, map->GetName(), map->IsEnabled() ? "enabled" : "disabled", map->GetMinPlayers(), map->GetMaxPlayers(), map->GetCustomCooldown(), groups.c_str()); else - ConMsg("Map %d is %s with workshop id %llu, which is %s. MinPlayers: %d MaxPlayers: %d Cooldown: %d\n", i, map.GetName(), map.GetWorkshopId(), map.IsEnabled() ? "enabled" : "disabled", map.GetMinPlayers(), map.GetMaxPlayers(), map.GetBaseCooldown()); + ConMsg("Map %d is %s with workshop id %llu, which is %s. MinPlayers: %d MaxPlayers: %d Custom Cooldown: %.2f Groups: %s\n", i, map->GetName(), map->GetWorkshopId(), map->IsEnabled() ? "enabled" : "disabled", map->GetMinPlayers(), map->GetMaxPlayers(), map->GetCustomCooldown(), groups.c_str()); } m_bMapListLoaded = true; @@ -907,15 +1113,6 @@ CUtlStringList CMapVoteSystem::CreateWorkshopMapGroup() return mapList; } -void CMapVoteSystem::DecrementAllMapCooldowns() -{ - FOR_EACH_VEC(m_vecMapList, i) - { - CMapInfo* pMap = &m_vecMapList[i]; - pMap->DecrementCooldown(); - } -} - bool CMapVoteSystem::WriteMapCooldownsToFile() { KeyValues* pKV = new KeyValues("cooldowns"); @@ -923,17 +1120,9 @@ bool CMapVoteSystem::WriteMapCooldownsToFile() const char* pszPath = "addons/cs2fixes/data/cooldowns.txt"; - FOR_EACH_VEC(m_vecMapList, i) - { - std::string mapName = m_vecMapList[i].GetName(); - const int mapCooldown = m_vecMapList[i].GetCooldown(); - - for (int i = 0; mapName[i]; i++) - mapName[i] = tolower(mapName[i]); - - if (mapCooldown > 0) - pKV->AddInt(mapName.c_str(), mapCooldown); - } + for (std::shared_ptr pCooldown : m_vecCooldowns) + if (pCooldown->GetTimeCooldown() > std::time(0)) + pKV->AddUint64(pCooldown->GetMapName(), pCooldown->GetTimeCooldown()); if (!pKV->SaveToFile(g_pFullFileSystem, pszPath)) { @@ -946,10 +1135,10 @@ bool CMapVoteSystem::WriteMapCooldownsToFile() void CMapVoteSystem::ClearInvalidNominations() { - if (!g_bVoteManagerEnable || m_bIsVoteOngoing) + if (!g_bVoteManagerEnable || m_bIsVoteOngoing || !GetGlobals()) return; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { int iNominatedMapIndex = m_arrPlayerNominations[i]; @@ -968,4 +1157,278 @@ void CMapVoteSystem::ClearInvalidNominations() ClientPrint(pPlayer, HUD_PRINTTALK, CHAT_PREFIX "Your nomination for \x06%s \x01has been removed because the player count requirements are no longer met.", GetMapName(iNominatedMapIndex)); } } -} \ No newline at end of file +} + +void CMapVoteSystem::UpdateCurrentMapIndex() +{ + for (int i = 0; i < GetMapListSize(); i++) + { + if (!V_strcasecmp(GetMapName(i), GetCurrentMapName()) || (GetCurrentWorkshopMap() != 0 && GetCurrentWorkshopMap() == GetMapWorkshopId(i))) + { + m_iCurrentMapIndex = i; + return; + } + } + + m_iCurrentMapIndex = -1; +} + +void CMapVoteSystem::ApplyGameSettings(KeyValues* pKV) +{ + if (!g_bVoteManagerEnable) + return; + + if (pKV->FindKey("launchoptions") && pKV->FindKey("launchoptions")->FindKey("customgamemode")) + SetCurrentWorkshopMap(pKV->FindKey("launchoptions")->GetUint64("customgamemode")); + else + SetCurrentWorkshopMap(0); + + if (pKV->FindKey("launchoptions") && pKV->FindKey("launchoptions")->FindKey("levelname")) + SetCurrentMapName(pKV->FindKey("launchoptions")->GetString("levelname")); + else + SetCurrentMapName("MISSING_MAP"); + + UpdateCurrentMapIndex(); + ProcessGroupCooldowns(); +} + +void CMapVoteSystem::OnLevelShutdown() +{ + // Put the map on cooldown as we transition to the next map + PutMapOnCooldown(GetCurrentMapName()); + + // Fully apply pending group cooldowns + for (std::shared_ptr pCooldown : m_vecCooldowns) + if (pCooldown->GetPendingCooldown() > 0.0f) + PutMapOnCooldown(pCooldown->GetMapName(), pCooldown->GetPendingCooldown()); + + WriteMapCooldownsToFile(); +} + +std::string CMapVoteSystem::ConvertFloatToString(float fValue, int precision) +{ + std::stringstream stream; + stream.precision(precision); + stream << std::fixed << fValue; + std::string str = stream.str(); + + // Ensure that there is a decimal point somewhere (there should be) + if (str.find('.') != std::string::npos) + { + // Remove trailing zeroes + str = str.substr(0, str.find_last_not_of('0') + 1); + // If the decimal point is now the last character, remove that as well + if (str.find('.') == str.size() - 1) + str = str.substr(0, str.size() - 1); + } + + return str; +} + +std::string CMapVoteSystem::StringToLower(std::string sValue) +{ + for (int i = 0; sValue[i]; i++) + sValue[i] = tolower(sValue[i]); + + return sValue; +} + +std::string CMapVoteSystem::GetMapCooldownText(const char* pszMapName, bool bPlural) +{ + std::shared_ptr pCooldown = GetMapCooldown(pszMapName); + float fValue; + std::string response; + + if (pCooldown->GetCurrentCooldown() > 23.99f) + { + fValue = roundf(pCooldown->GetCurrentCooldown() / 24 * 100) / 100; + response = ConvertFloatToString(fValue, 2) + " day"; + } + else if (pCooldown->GetCurrentCooldown() <= 0.995f) + { + fValue = roundf(pCooldown->GetCurrentCooldown() * 60); + + // Rounding edge case + if (fValue == 0.0f) + fValue = 1.0f; + + response = ConvertFloatToString(fValue, 0) + " minute"; + } + else + { + fValue = roundf(pCooldown->GetCurrentCooldown() * 100) / 100; + response = ConvertFloatToString(fValue, 2) + " hour"; + } + + if (bPlural && fValue != 1.0f) + response += "s"; + + if (pCooldown->IsPending()) + response += " pending"; + + return response; +} + +void CMapVoteSystem::PutMapOnCooldown(const char* pszMapName, float fCooldown) +{ + if (g_bDisableCooldowns) + return; + + int iMapIndex = GetMapIndexFromString(pszMapName); + + // If custom cooldown wasn't passed, use the normal cooldown for this map + if (fCooldown == 0.0f) + { + if (iMapIndex != -1 && GetMapCustomCooldown(iMapIndex) != 0.0f) + fCooldown = GetMapCustomCooldown(iMapIndex); + else + fCooldown = GetDefaultMapCooldown(); + } + + time_t timeCooldown = std::time(0) + (time_t)(fCooldown * 60 * 60); + std::shared_ptr pCooldown = GetMapCooldown(pszMapName); + + // Ensure we don't overwrite a longer cooldown + if (pCooldown->GetTimeCooldown() < timeCooldown) + { + pCooldown->SetTimeCooldown(timeCooldown); + + // Pending cooldown should be invalidated at this point + pCooldown->SetPendingCooldown(0.0f); + } +} + +void CMapVoteSystem::ProcessGroupCooldowns() +{ + int iCurrentMapIndex = GetCurrentMapIndex(); + + if (iCurrentMapIndex == -1) + return; + + std::vector vecCurrentMapGroups = m_vecMapList[iCurrentMapIndex]->GetGroups(); + + for (std::string groupName : vecCurrentMapGroups) + { + std::shared_ptr pGroup = GetGroupFromString(groupName.c_str()); + + if (!pGroup) + { + Panic("Invalid group name %s defined for map %s\n", groupName.c_str(), GetMapName(iCurrentMapIndex)); + continue; + } + + if (!pGroup->IsEnabled()) + continue; + + // Check entire map list for other maps in this group, and give them the group cooldown (pending) + for (int i = 0; i < GetMapListSize(); i++) + { + if (iCurrentMapIndex != i && GetMapEnabledStatus(i) && m_vecMapList[i]->HasGroup(groupName)) + { + float fCooldown = pGroup->GetCooldown() == 0.0f ? GetDefaultMapCooldown() : pGroup->GetCooldown(); + std::shared_ptr pCooldown = GetMapCooldown(i); + + // Ensure we don't overwrite a longer cooldown + if (pCooldown->GetPendingCooldown() < fCooldown) + pCooldown->SetPendingCooldown(fCooldown); + } + } + } +} + +std::shared_ptr CMapVoteSystem::GetMapCooldown(const char* pszMapName) +{ + for (std::shared_ptr pCooldown : m_vecCooldowns) + if (!V_strcasecmp(pszMapName, pCooldown->GetMapName())) + return pCooldown; + + // Never been on cooldown, create new object + std::shared_ptr pCooldown = std::make_shared(pszMapName); + m_vecCooldowns.push_back(pCooldown); + + return pCooldown; +} + +float CCooldown::GetCurrentCooldown() +{ + time_t timeCurrent = std::time(0); + + // Use pending cooldown first if present + float fRemainingTime = GetPendingCooldown(); + + // Calculate decimal hours + float fCurrentRemainingTime = (GetTimeCooldown() - timeCurrent) / 60.0f / 60.0f; + + // Check if current cooldown should override + if (GetTimeCooldown() > timeCurrent && fCurrentRemainingTime > fRemainingTime) + fRemainingTime = fCurrentRemainingTime; + + return fRemainingTime; +} + +// TODO: remove this once servers have been given at least a few months to update cs2fixes +bool CMapVoteSystem::ConvertMapListKVToJSON() +{ + Message("Attempting to convert KV1 maplist.cfg to JSON format...\n"); + + const char* pszPath = "addons/cs2fixes/configs/maplist.cfg"; + + KeyValues* pKV = new KeyValues("maplist"); + KeyValues::AutoDelete autoDelete(pKV); + + if (!pKV->LoadFromFile(g_pFullFileSystem, pszPath)) + { + Panic("Failed to load %s\n", pszPath); + return false; + } + + ordered_json jsonMapList; + + jsonMapList["Groups"] = ordered_json(ordered_json::value_t::object); + + for (KeyValues* pKey = pKV->GetFirstSubKey(); pKey; pKey = pKey->GetNextKey()) + { + ordered_json jsonMap; + + if (pKey->FindKey("enabled")) + jsonMap["enabled"] = pKey->GetBool("enabled"); + if (pKey->FindKey("workshop_id")) + jsonMap["workshop_id"] = pKey->GetUint64("workshop_id"); + if (pKey->FindKey("min_players")) + jsonMap["min_players"] = pKey->GetInt("min_players"); + if (pKey->FindKey("max_players")) + jsonMap["max_players"] = pKey->GetInt("max_players"); + if (pKey->FindKey("cooldown")) + jsonMap["cooldown"] = pKey->GetInt("cooldown"); + + jsonMapList["Maps"][pKey->GetName()] = jsonMap; + } + + const char* pszJsonPath = "addons/cs2fixes/configs/maplist.jsonc"; + const char* pszKVConfigRenamePath = "addons/cs2fixes/configs/maplist_old.cfg"; + char szPath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszJsonPath); + std::ofstream jsonFile(szPath); + + if (!jsonFile.is_open()) + { + Panic("Failed to open %s\n", pszJsonPath); + return false; + } + + jsonFile << std::setfill('\t') << std::setw(1) << jsonMapList << std::endl; + + char szKVRenamePath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszPath); + V_snprintf(szKVRenamePath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszKVConfigRenamePath); + + std::rename(szPath, szKVRenamePath); + + // remove old cfg example if it exists + const char* pszKVExamplePath = "addons/cs2fixes/configs/maplist.cfg.example"; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszKVExamplePath); + std::remove(szPath); + + Message("Successfully converted KV1 maplist.cfg to JSON format at %s\n", pszJsonPath); + return true; +} diff --git a/src/map_votes.h b/src/map_votes.h index f6373413..e858093e 100644 --- a/src/map_votes.h +++ b/src/map_votes.h @@ -19,74 +19,97 @@ #include "KeyValues.h" #include "common.h" +#include "entity/ccsplayercontroller.h" #include "steam/isteamugc.h" #include "steam/steam_api_common.h" #include "utlqueue.h" #include "utlstring.h" #include "utlvector.h" +#undef snprintf +#include "vendor/nlohmann/json_fwd.hpp" #include #include #include -// Nomination constants, used as return codes for nomination commands -#ifndef NOMINATION_CONSTANTS_H - #define NOMINATION_CONSTANTS_H -namespace NominationReturnCodes -{ - static const int VOTE_STARTED = -100; - static const int NOMINATION_DISABLED = -101; - static const int NOMINATION_RESET = -102; - static const int NOMINATION_RESET_FAILED = -103; - static const int MAP_NOT_FOUND = -104; - static const int MAP_MULTIPLE = -105; - static const int MAP_DISABLED = -106; - static const int MAP_CURRENT = -107; - static const int MAP_COOLDOWN = -108; - static const int MAP_MINPLAYERS = -109; - static const int MAP_MAXPLAYERS = -110; - static const int MAP_NOMINATED = -111; -} // namespace NominationReturnCodes -#endif - -class CMapInfo +using ordered_json = nlohmann::ordered_json; + +class CMap { public: - CMapInfo(const char* pszName, uint64 iWorkshopId, bool bIsEnabled, int iMinPlayers, int iMaxPlayers, int iBaseCooldown, int iCurrentCooldown) + CMap(std::string sName, uint64 iWorkshopId, bool bIsEnabled, int iMinPlayers, int iMaxPlayers, float fCustomCooldown, std::vector vecGroups) { - V_strcpy(m_pszName, pszName); + m_strName = sName; m_iWorkshopId = iWorkshopId; m_bIsEnabled = bIsEnabled; - m_iBaseCooldown = iBaseCooldown; - m_iCurrentCooldown = iCurrentCooldown; + m_fCustomCooldown = fCustomCooldown; m_iMinPlayers = iMinPlayers; m_iMaxPlayers = iMaxPlayers; + m_vecGroups = vecGroups; } - const char* GetName() { return (const char*)m_pszName; }; + const char* GetName() { return m_strName.c_str(); }; uint64 GetWorkshopId() const { return m_iWorkshopId; }; bool IsEnabled() { return m_bIsEnabled; }; - int GetBaseCooldown() { return m_iBaseCooldown; }; - int GetCooldown() { return m_iCurrentCooldown; }; - void ResetCooldownToBase() { m_iCurrentCooldown = m_iBaseCooldown; }; - void DecrementCooldown() { m_iCurrentCooldown = MAX(0, (m_iCurrentCooldown - 1)); } + float GetCustomCooldown() { return m_fCustomCooldown; }; int GetMinPlayers() { return m_iMinPlayers; }; int GetMaxPlayers() { return m_iMaxPlayers; }; + std::vector GetGroups() { return m_vecGroups; }; + bool HasGroup(std::string strGroup) { return std::find(m_vecGroups.begin(), m_vecGroups.end(), strGroup) != m_vecGroups.end(); }; private: - char m_pszName[64]; + std::string m_strName; uint64 m_iWorkshopId; bool m_bIsEnabled; int m_iMinPlayers; int m_iMaxPlayers; - int m_iBaseCooldown; - int m_iCurrentCooldown; + float m_fCustomCooldown; + std::vector m_vecGroups; }; -typedef struct +class CGroup { - const char* name; - int index; -} MapIndexPair; +public: + CGroup(std::string sName, bool bIsEnabled, float fCooldown) + { + m_strName = sName; + m_bIsEnabled = bIsEnabled; + m_fCooldown = fCooldown; + } + + const char* GetName() { return m_strName.c_str(); }; + bool IsEnabled() { return m_bIsEnabled; }; + float GetCooldown() { return m_fCooldown; }; + +private: + std::string m_strName; + bool m_bIsEnabled; + float m_fCooldown; +}; + +class CCooldown +{ +public: + CCooldown(std::string sMapName) + { + m_strMapName = sMapName; + m_timeCooldown = 0; + m_fPendingCooldown = 0.0f; + } + + const char* GetMapName() { return m_strMapName.c_str(); }; + time_t GetTimeCooldown() { return m_timeCooldown; }; + void SetTimeCooldown(time_t timeCooldown) { m_timeCooldown = timeCooldown; }; + float GetPendingCooldown() { return m_fPendingCooldown; }; + void SetPendingCooldown(float fPendingCooldown) { m_fPendingCooldown = fPendingCooldown; }; + bool IsOnCooldown() { return GetCurrentCooldown() > 0.0f; } + bool IsPending() { return m_fPendingCooldown > 0.0f && m_fPendingCooldown == GetCurrentCooldown(); }; + float GetCurrentCooldown(); + +private: + std::string m_strMapName; + time_t m_timeCooldown; + float m_fPendingCooldown; +}; class CMapVoteSystem { @@ -106,19 +129,27 @@ class CMapVoteSystem void FinishVote(); bool RegisterPlayerVote(CPlayerSlot iPlayerSlot, int iVoteOption); std::vector GetMapIndexesFromSubstring(const char* sMapSubstring); - int GetMapIndexFromString(const char* sMapString); - int GetCooldownMap(int iMapIndex) { return m_vecMapList[iMapIndex].GetCooldown(); }; - void PutMapOnCooldown(int iMapIndex) { m_vecMapList[iMapIndex].ResetCooldownToBase(); }; - void DecrementAllMapCooldowns(); + uint64 HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAllowWorkshopID = false); + int GetMapIndexFromString(const char* pszMapString); + std::shared_ptr GetGroupFromString(const char* pszName); + std::shared_ptr GetMapCooldown(const char* pszMapName); + std::shared_ptr GetMapCooldown(int iMapIndex) { return GetMapCooldown(GetMapName(iMapIndex)); }; + std::string GetMapCooldownText(const char* pszMapName, bool bPlural); + std::string GetMapCooldownText(int iMapIndex, bool bPlural) { return GetMapCooldownText(GetMapName(iMapIndex), bPlural); }; + float GetMapCustomCooldown(int iMapIndex) { return m_vecMapList[iMapIndex]->GetCustomCooldown(); }; + void PutMapOnCooldown(const char* pszMapName, float fCooldown = 0.0f); void SetMaxNominatedMaps(int iMaxNominatedMaps) { m_iMaxNominatedMaps = iMaxNominatedMaps; }; int GetMaxNominatedMaps() { return m_iMaxNominatedMaps; }; - std::pair> AddMapNomination(CPlayerSlot iPlayerSlot, const char* sMapSubstring); + void SetMaxVoteMaps(int iMaxVoteMaps) { m_iMaxVoteMaps = iMaxVoteMaps; }; + int GetMaxVoteMaps() { return m_iMaxVoteMaps; }; + void AttemptNomination(CCSPlayerController* pController, const char* sMapSubstring); + void PrintMapList(CCSPlayerController* pController); bool IsMapIndexEnabled(int iMapIndex); int GetTotalNominations(int iMapIndex); - std::pair> ForceNextMap(const char* sMapSubstring); - int GetMapListSize() { return m_vecMapList.Count(); }; - const char* GetMapName(int iMapIndex) { return m_vecMapList[iMapIndex].GetName(); }; - uint64 GetMapWorkshopId(int iMapIndex) { return m_vecMapList[iMapIndex].GetWorkshopId(); }; + void ForceNextMap(CCSPlayerController* pController, const char* sMapSubstring); + int GetMapListSize() { return m_vecMapList.size(); }; + const char* GetMapName(int iMapIndex) { return m_vecMapList[iMapIndex]->GetName(); }; + uint64 GetMapWorkshopId(int iMapIndex) { return m_vecMapList[iMapIndex]->GetWorkshopId(); }; void ClearPlayerInfo(int iSlot); bool IsVoteOngoing() { return m_bIsVoteOngoing; } bool IsIntermissionAllowed(); @@ -126,51 +157,58 @@ class CMapVoteSystem CUtlStringList CreateWorkshopMapGroup(); void QueueMapDownload(PublishedFileId_t iWorkshopId); void PrintDownloadProgress(); - void SetCurrentWorkshopMap(uint64 iCurrentWorkshopMap) - { - m_iCurrentWorkshopMap = iCurrentWorkshopMap; - m_strCurrentMap = ""; - } - void SetCurrentMap(std::string strCurrentMap) - { - m_iCurrentWorkshopMap = 0; - m_strCurrentMap = strCurrentMap; - } + void SetCurrentMapName(const char* pszCurrentMap) { m_strCurrentMap = pszCurrentMap; } + const char* GetCurrentMapName() { return m_strCurrentMap.c_str(); } + void SetCurrentWorkshopMap(uint64 iCurrentWorkshopMap) { m_iCurrentWorkshopMap = iCurrentWorkshopMap; } uint64 GetCurrentWorkshopMap() { return m_iCurrentWorkshopMap; } - const char* GetCurrentMap() { return m_strCurrentMap.c_str(); } int GetDownloadQueueSize() { return m_DownloadQueue.Count(); } int GetCurrentMapIndex() { return m_iCurrentMapIndex; } - void SetCurrentMapIndex(int iMapIndex) { m_iCurrentMapIndex = iMapIndex; } - int GetMapMinPlayers(int iMapIndex) { return m_vecMapList[iMapIndex].GetMinPlayers(); } - int GetMapMaxPlayers(int iMapIndex) { return m_vecMapList[iMapIndex].GetMaxPlayers(); } - bool GetMapEnabledStatus(int iMapIndex) { return m_vecMapList[iMapIndex].IsEnabled(); } - int GetDefaultMapCooldown() { return m_iDefaultMapCooldown; } - void SetDefaultMapCooldown(int iMapCooldown) { m_iDefaultMapCooldown = iMapCooldown; } + void UpdateCurrentMapIndex(); + int GetMapMinPlayers(int iMapIndex) { return m_vecMapList[iMapIndex]->GetMinPlayers(); } + int GetMapMaxPlayers(int iMapIndex) { return m_vecMapList[iMapIndex]->GetMaxPlayers(); } + bool GetMapEnabledStatus(int iMapIndex) { return m_vecMapList[iMapIndex]->IsEnabled(); } + float GetDefaultMapCooldown() { return m_fDefaultMapCooldown; } + void SetDefaultMapCooldown(float fMapCooldown) { m_fDefaultMapCooldown = fMapCooldown; } void ClearInvalidNominations(); - int GetForcedNextMap() { return m_iForcedNextMapIndex; } + uint64 GetForcedNextMap() { return m_iForcedNextMap; } + std::string GetForcedNextMapName() { return GetForcedNextMap() > GetMapListSize() ? std::to_string(GetForcedNextMap()) : GetMapName(GetForcedNextMap()); } + bool ConvertMapListKVToJSON(); + std::unordered_map GetNominatedMaps(); + void ApplyGameSettings(KeyValues* pKV); + void OnLevelShutdown(); + std::vector> GetMapCooldowns() { return m_vecCooldowns; } + std::string ConvertFloatToString(float fValue, int precision); + std::string StringToLower(std::string sValue); + void SetDisabledCooldowns(bool bValue) { g_bDisableCooldowns = bValue; } // Can be used by custom fork features, e.g. an auto-restart + void ProcessGroupCooldowns(); private: int WinningMapIndex(); bool UpdateWinningMap(); - void GetNominatedMapsForVote(CUtlVector& vecChosenNominatedMaps); + std::vector GetNominatedMapsForVote(); bool WriteMapCooldownsToFile(); STEAM_GAMESERVER_CALLBACK_MANUAL(CMapVoteSystem, OnMapDownloaded, DownloadItemResult_t, m_CallbackDownloadItemResult); CUtlQueue m_DownloadQueue; - CUtlVector m_vecMapList; + std::vector> m_vecMapList; + std::vector> m_vecGroups; + std::vector> m_vecCooldowns; int m_arrPlayerNominations[MAXPLAYERS]; - int m_iForcedNextMapIndex = -1; - int m_iDefaultMapCooldown = 10; + uint64 m_iForcedNextMap = -1; // Can be a map index or a workshop ID + float m_fDefaultMapCooldown = 6.0f; int m_iMaxNominatedMaps = 10; + int m_iMaxVoteMaps = 10; int m_iRandomWinnerShift = 0; int m_arrPlayerVotes[MAXPLAYERS]; - int m_iCurrentMapIndex; + int m_iCurrentMapIndex = -1; bool m_bIsVoteOngoing = false; bool m_bMapListLoaded = false; bool m_bIntermissionStarted = false; uint64 m_iCurrentWorkshopMap = 0; - std::string m_strCurrentMap = ""; + std::string m_strCurrentMap = "MISSING_MAP"; + int m_iVoteSize = 0; + bool g_bDisableCooldowns = false; }; extern CMapVoteSystem* g_pMapVoteSystem; \ No newline at end of file diff --git a/src/netmessages.h b/src/netmessages.h index f0dc9762..9aed1732 100644 --- a/src/netmessages.h +++ b/src/netmessages.h @@ -9,6 +9,8 @@ #include #include #include +#undef min +#undef max class CNETMsg_Tick_t : public CNetMessagePB { diff --git a/src/playermanager.cpp b/src/playermanager.cpp index ade32224..2d7e3f65 100644 --- a/src/playermanager.cpp +++ b/src/playermanager.cpp @@ -34,13 +34,14 @@ #include "user_preferences.h" #include "utils/entity.h" #include "utlstring.h" +#include "votemanager.h" #include <../cs2fixes.h> #include "tier0/memdbgon.h" extern IVEngineServer2* g_pEngineServer2; extern CGameEntitySystem* g_pEntitySystem; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern IGameEventSystem* g_gameEventSystem; extern CUtlVector* GetClientList(); @@ -245,9 +246,10 @@ FAKE_STRING_CVAR(cs2f_beacon_particle, ".vpcf file to be precached and used for bool ZEPlayer::IsFlooding() { - if (m_bGagged) return false; + if (m_bGagged || !GetGlobals()) + return false; - float time = gpGlobals->curtime; + float time = GetGlobals()->curtime; float newTime = time + g_flFloodInterval; if (m_flLastTalkTime >= time) @@ -644,6 +646,12 @@ void CPlayerManager::OnClientDisconnect(CPlayerSlot slot) g_pMapVoteSystem->ClearPlayerInfo(slot.Get()); g_pMapVoteSystem->ClearInvalidNominations(); + // One tick delay, to ensure player count decrements + new CTimer(0.01f, false, true, []() { + g_pVoteManager->CheckRTVStatus(); + return -1.0f; + }); + g_pPanoramaVoteHandler->RemovePlayerFromVote(slot.Get()); } @@ -656,7 +664,10 @@ void CPlayerManager::OnClientPutInServer(CPlayerSlot slot) void CPlayerManager::OnLateLoad() { - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); @@ -731,7 +742,10 @@ void CPlayerManager::OnValidateAuthTicket(ValidateAuthTicketResponse_t* pRespons void CPlayerManager::CheckInfractions() { - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { if (m_vecPlayers[i] == nullptr || m_vecPlayers[i]->IsFakeClient()) continue; @@ -748,12 +762,12 @@ FAKE_BOOL_CVAR(cs2f_flashlight_enable, "Whether to enable flashlights", g_bFlash void CPlayerManager::FlashLightThink() { - if (!g_bFlashLightEnable) + if (!g_bFlashLightEnable || !GetGlobals()) return; VPROF("CPlayerManager::FlashLightThink"); - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(i); @@ -774,12 +788,12 @@ FAKE_BOOL_CVAR(cs2f_hide_teammates_only, "Whether to hide teammates only", g_bHi void CPlayerManager::CheckHideDistances() { - if (!g_pEntitySystem) + if (!g_pEntitySystem || !GetGlobals()) return; VPROF("CPlayerManager::CheckHideDistances"); - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { auto player = GetPlayer(i); @@ -805,7 +819,7 @@ void CPlayerManager::CheckHideDistances() auto vecPosition = pPawn->GetAbsOrigin(); int team = pController->m_iTeamNum; - for (int j = 0; j < gpGlobals->maxClients; j++) + for (int j = 0; j < GetGlobals()->maxClients; j++) { if (j == i) continue; @@ -840,7 +854,10 @@ extern bool g_bEnableHide; void CPlayerManager::UpdatePlayerStates() { - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = GetPlayer(i); @@ -880,12 +897,12 @@ FAKE_BOOL_CVAR(cs2f_infinite_reserve_ammo, "Whether to enable infinite reserve a void CPlayerManager::SetupInfiniteAmmo() { new CTimer(5.0f, false, true, []() { - if (!g_bInfiniteAmmo) + if (!g_bInfiniteAmmo || !GetGlobals()) return 5.0f; VPROF("CPlayerManager::InfiniteAmmoTimer"); - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); @@ -968,6 +985,9 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, int& iNumClients, int* rgiClients, uint64 iBlockedFlags, ETargetType& nType) { + if (!GetGlobals()) + return ETargetError::INVALID; + nType = ETargetType::NONE; ZEPlayer* zpPlayer = pPlayer ? pPlayer->GetZEPlayer() : nullptr; bool bTargetMultiple = false; @@ -1215,7 +1235,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, } else if (bTargetMultiple) { - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { if (m_vecPlayers[i] == nullptr) continue; @@ -1232,7 +1252,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, while (iNumClients == 0 && iAttempts < 10000) { - int iSlot = rand() % (gpGlobals->maxClients - 1); + int iSlot = rand() % (GetGlobals()->maxClients - 1); // Prevent infinite loop iAttempts++; @@ -1255,7 +1275,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, while (iNumClients == 0 && iAttempts < 10000) { - int iSlot = rand() % (gpGlobals->maxClients - 1); + int iSlot = rand() % (GetGlobals()->maxClients - 1); // Prevent infinite loop iAttempts++; @@ -1276,7 +1296,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, if (pRandomPlayer == nullptr) return ETargetError::INVALID; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { if (m_vecPlayers[i] == nullptr) continue; @@ -1312,7 +1332,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, CBaseEntity* entTarget = nullptr; entTarget = UTIL_FindPickerEntity(pPlayer); - if (!entTarget->IsPawn()) + if (!entTarget || !entTarget->IsPawn()) return ETargetError::INVALID; CCSPlayerController* pAimed = CCSPlayerController::FromPawn(static_cast(entTarget)); @@ -1323,7 +1343,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, nType = ETargetType::ALL_BUT_AIM; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { if (m_vecPlayers[i] == nullptr) continue; @@ -1376,7 +1396,7 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer, if (bExactName) pszTarget++; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { if (m_vecPlayers[i] == nullptr) continue; @@ -1497,7 +1517,7 @@ bool CPlayerManager::CanTargetPlayers(CCSPlayerController* pPlayer, const char* ZEPlayer* CPlayerManager::GetPlayer(CPlayerSlot slot) { - if (slot.Get() < 0 || slot.Get() >= gpGlobals->maxClients) + if (slot.Get() < 0 || slot.Get() >= MAXPLAYERS) return nullptr; return m_vecPlayers[slot.Get()]; @@ -1513,7 +1533,7 @@ ZEPlayer* CPlayerManager::GetPlayerFromUserId(uint16 userid) { uint8 index = userid & 0xFF; - if (index >= gpGlobals->maxClients) + if (index >= MAXPLAYERS) return nullptr; return m_vecPlayers[index]; @@ -1606,6 +1626,9 @@ int CPlayerManager::GetOnlinePlayerCount(bool bCountBots) { int iOnlinePlayers = 0; + if (!GetClientList()) + return iOnlinePlayers; + for (int i = 0; i < GetClientList()->Count(); i++) { CServerSideClient* pClient = (*GetClientList())[i]; diff --git a/src/serversideclient.h b/src/serversideclient.h index 2b369fd7..89872659 100644 --- a/src/serversideclient.h +++ b/src/serversideclient.h @@ -18,6 +18,8 @@ #include #include +#undef min +#undef max class CHLTVServer; class INetMessage; diff --git a/src/utils/entity.cpp b/src/utils/entity.cpp index ad938cd1..2ee95566 100644 --- a/src/utils/entity.cpp +++ b/src/utils/entity.cpp @@ -35,6 +35,9 @@ extern CCSGameRules* g_pGameRules; CBaseEntity* UTIL_FindPickerEntity(CBasePlayerController* pPlayer) { + if (!g_pGameRules) + return nullptr; + static int offset = g_GameConfig->GetOffset("CGameRules_FindPickerEntity"); if (offset < 0) diff --git a/src/votemanager.cpp b/src/votemanager.cpp index 3e51e925..3d9b4cc3 100644 --- a/src/votemanager.cpp +++ b/src/votemanager.cpp @@ -22,19 +22,16 @@ #include "ctimer.h" #include "entity/cgamerules.h" #include "icvar.h" -#include "panoramavote.h" #include "playermanager.h" #include "tier0/memdbgon.h" extern CGameEntitySystem* g_pEntitySystem; extern IVEngineServer2* g_pEngineServer2; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; -ERTVState g_RTVState = ERTVState::MAP_START; -EExtendState g_ExtendState = EExtendState::MAP_START; -int g_iExtends = 0; +CVoteManager* g_pVoteManager = nullptr; bool g_bVoteManagerEnable = false; int g_iMaxExtends = 1; @@ -42,12 +39,10 @@ float g_flExtendSucceedRatio = 0.5f; int g_iExtendTimeToAdd = 20; float g_flRTVSucceedRatio = 0.6f; bool g_bRTVEndRound = false; - int g_ExtendVoteMode = (int)EExtendVoteMode::EXTENDVOTE_ADMINONLY; float g_flExtendVoteStartTime = 4.0f; float g_flExtendVoteDuration = 30.0f; float g_flExtendBeginRatio = 0.4f; - float g_flExtendVoteDelay = 300.0f; float g_flRtvDelay = 300.0f; @@ -65,52 +60,48 @@ FAKE_FLOAT_CVAR(cs2f_rtv_vote_delay, "Time after map start until RTV votes can b FAKE_FLOAT_CVAR(cs2f_rtv_success_ratio, "Ratio needed to pass RTV", g_flRTVSucceedRatio, 0.6f, false) FAKE_BOOL_CVAR(cs2f_rtv_endround, "Whether to immediately end the round when RTV succeeds", g_bRTVEndRound, false, false) -static float flExtendVoteTickrate = 1.0f; - -void VoteManager_Init() +void CVoteManager::VoteManager_Init() { // Disable RTV and Extend votes after map has just started - g_RTVState = ERTVState::MAP_START; - g_ExtendState = EExtendState::MAP_START; + m_RTVState = ERTVState::MAP_START; + m_ExtendState = EExtendState::MAP_START; - g_iExtends = 0; + m_iExtends = 0; - new CTimer(g_flExtendVoteDelay, false, true, []() { - if (g_ExtendState < EExtendState::POST_EXTEND_NO_EXTENDS_LEFT) - g_ExtendState = EExtendState::EXTEND_ALLOWED; + new CTimer(g_flExtendVoteDelay, false, true, [this]() { + if (m_ExtendState < EExtendState::POST_EXTEND_NO_EXTENDS_LEFT) + m_ExtendState = EExtendState::EXTEND_ALLOWED; return -1.0f; }); - new CTimer(g_flRtvDelay, false, true, []() { - if (g_RTVState != ERTVState::BLOCKED_BY_ADMIN) - g_RTVState = ERTVState::RTV_ALLOWED; + new CTimer(g_flRtvDelay, false, true, [this]() { + if (m_RTVState != ERTVState::BLOCKED_BY_ADMIN) + m_RTVState = ERTVState::RTV_ALLOWED; return -1.0f; }); - new CTimer(flExtendVoteTickrate, false, true, TimerCheckTimeleft); + new CTimer(m_flExtendVoteTickrate, false, true, std::bind(&CVoteManager::TimerCheckTimeleft, this)); } -int iVoteStartTicks = 3; -bool bVoteStarting = false; -float TimerCheckTimeleft() +float CVoteManager::TimerCheckTimeleft() { - if (!gpGlobals || !g_pGameRules) - return flExtendVoteTickrate; + if (!GetGlobals() || !g_pGameRules) + return m_flExtendVoteTickrate; if (!g_bVoteManagerEnable) - return flExtendVoteTickrate; + return m_flExtendVoteTickrate; // Auto votes disabled, dont stop the timer in case this changes mid-map if (g_ExtendVoteMode != EExtendVoteMode::EXTENDVOTE_AUTO) - return flExtendVoteTickrate; + return m_flExtendVoteTickrate; // Vote already happening - if (bVoteStarting || g_ExtendState == EExtendState::IN_PROGRESS) - return flExtendVoteTickrate; + if (m_bVoteStarting || m_ExtendState == EExtendState::IN_PROGRESS) + return m_flExtendVoteTickrate; // No more extends or map RTVd - if ((g_iMaxExtends - g_iExtends) <= 0 || g_ExtendState >= EExtendState::POST_EXTEND_NO_EXTENDS_LEFT) - return flExtendVoteTickrate; + if ((g_iMaxExtends - m_iExtends) <= 0 || m_ExtendState >= EExtendState::POST_EXTEND_NO_EXTENDS_LEFT) + return m_flExtendVoteTickrate; ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_timelimit")); // CONVAR_TODO @@ -118,39 +109,42 @@ float TimerCheckTimeleft() float flTimelimit = *(float*)&cvar->values; if (flTimelimit <= 0.0) - return flExtendVoteTickrate; + return m_flExtendVoteTickrate; - float flTimeleft = (g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - gpGlobals->curtime; + float flTimeleft = (g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - GetGlobals()->curtime; // Not yet time to start a vote if (flTimeleft > (g_flExtendVoteStartTime * 60.0)) - return flExtendVoteTickrate; + return m_flExtendVoteTickrate; - bVoteStarting = true; + m_bVoteStarting = true; ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote starting in 10 seconds!"); - new CTimer(7.0f, false, true, []() { - if (iVoteStartTicks == 0) + new CTimer(7.0f, false, true, [this]() { + if (m_iVoteStartTicks == 0) { - iVoteStartTicks = 3; - StartExtendVote(VOTE_CALLER_SERVER); - bVoteStarting = false; + m_iVoteStartTicks = 3; + g_pVoteManager->StartExtendVote(VOTE_CALLER_SERVER); + m_bVoteStarting = false; return -1.0f; } - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote starting in %d....", iVoteStartTicks); - iVoteStartTicks--; + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote starting in %d....", m_iVoteStartTicks); + m_iVoteStartTicks--; return 1.0f; }); - return flExtendVoteTickrate; + return m_flExtendVoteTickrate; } -int GetCurrentRTVCount() +int CVoteManager::GetCurrentRTVCount() { int iVoteCount = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return iVoteCount; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -161,16 +155,19 @@ int GetCurrentRTVCount() return iVoteCount; } -int GetNeededRTVCount() +int CVoteManager::GetNeededRTVCount() { return (int)(g_playerManager->GetOnlinePlayerCount(false) * g_flRTVSucceedRatio) + 1; } -int GetCurrentExtendCount() +int CVoteManager::GetCurrentExtendCount() { int iVoteCount = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return iVoteCount; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -181,12 +178,16 @@ int GetCurrentExtendCount() return iVoteCount; } -int GetNeededExtendCount() +// TODO: wtf is going on here? function should be checked/tested, normally off our radar because not used in auto-extend mode +int CVoteManager::GetNeededExtendCount() { int iOnlinePlayers = 0.0f; int iVoteCount = 0; - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return 0; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); @@ -203,7 +204,7 @@ int GetNeededExtendCount() CON_COMMAND_CHAT(rtv, "- Vote to end the current map sooner") { - if (!g_bVoteManagerEnable) + if (!g_bVoteManagerEnable || !GetGlobals()) return; if (!player) @@ -223,7 +224,7 @@ CON_COMMAND_CHAT(rtv, "- Vote to end the current map sooner") return; } - switch (g_RTVState) + switch (g_pVoteManager->GetRTVState()) { case ERTVState::MAP_START: ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "RTV is not open yet."); @@ -239,57 +240,24 @@ CON_COMMAND_CHAT(rtv, "- Vote to end the current map sooner") return; } - int iCurrentRTVCount = GetCurrentRTVCount(); - int iNeededRTVCount = GetNeededRTVCount(); - if (pPlayer->GetRTVVote()) { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "You have already rocked the vote (%i voted, %i needed).", iCurrentRTVCount, iNeededRTVCount); + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "You have already rocked the vote (%i voted, %i needed).", g_pVoteManager->GetCurrentRTVCount(), g_pVoteManager->GetNeededRTVCount()); return; } - if (pPlayer->GetRTVVoteTime() + 60.0f > gpGlobals->curtime) + if (pPlayer->GetRTVVoteTime() + 60.0f > GetGlobals()->curtime) { - int iRemainingTime = (int)(pPlayer->GetRTVVoteTime() + 60.0f - gpGlobals->curtime); + int iRemainingTime = (int)(pPlayer->GetRTVVoteTime() + 60.0f - GetGlobals()->curtime); ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Wait %i seconds before you can RTV again.", iRemainingTime); return; } - if (iCurrentRTVCount + 1 >= iNeededRTVCount) - { - g_RTVState = ERTVState::POST_RTV_SUCCESSFULL; - g_ExtendState = EExtendState::POST_RTV; - // CONVAR_TODO - g_pEngineServer2->ServerCommand("mp_timelimit 0.01"); - - if (g_bRTVEndRound) - { - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "RTV succeeded! Ending the map now..."); - - new CTimer(3.0f, false, true, []() { - g_pGameRules->TerminateRound(5.0f, CSRoundEndReason::Draw); - - return -1.0f; - }); - } - else - { - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "RTV succeeded! This is the last round of the map!"); - } - - for (int i = 0; i < gpGlobals->maxClients; i++) - { - ZEPlayer* pPlayer2 = g_playerManager->GetPlayer(i); - if (pPlayer2) - pPlayer2->SetRTVVote(false); - } - - return; - } - pPlayer->SetRTVVote(true); - pPlayer->SetRTVVoteTime(gpGlobals->curtime); - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s wants to rock the vote (%i voted, %i needed).", player->GetPlayerName(), iCurrentRTVCount + 1, iNeededRTVCount); + pPlayer->SetRTVVoteTime(GetGlobals()->curtime); + + if (!g_pVoteManager->CheckRTVStatus()) + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s wants to rock the vote (%i voted, %i needed).", player->GetPlayerName(), g_pVoteManager->GetCurrentRTVCount(), g_pVoteManager->GetNeededRTVCount()); } CON_COMMAND_CHAT(unrtv, "- Remove your vote to end the current map sooner") @@ -326,7 +294,7 @@ CON_COMMAND_CHAT(unrtv, "- Remove your vote to end the current map sooner") CON_COMMAND_CHAT(ve, "- Vote to extend current map") { - if (!g_bVoteManagerEnable) + if (!g_bVoteManagerEnable || !GetGlobals() || !g_pGameRules) return; if (!player) @@ -345,7 +313,7 @@ CON_COMMAND_CHAT(ve, "- Vote to extend current map") return; case EExtendVoteMode::EXTENDVOTE_AUTO: { - if (g_ExtendState == EExtendState::EXTEND_ALLOWED) + if (g_pVoteManager->GetExtendState() == EExtendState::EXTEND_ALLOWED) { ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_timelimit")); @@ -355,7 +323,7 @@ CON_COMMAND_CHAT(ve, "- Vote to extend current map") if (flTimelimit <= 0.0) return; - float flTimeleft = (g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - gpGlobals->curtime; + float flTimeleft = (g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - GetGlobals()->curtime; int iTimeTillVote = (int)(flTimeleft - (g_flExtendVoteStartTime * 60.0)); div_t div = std::div(iTimeTillVote, 60); @@ -382,7 +350,7 @@ CON_COMMAND_CHAT(ve, "- Vote to extend current map") return; } - switch (g_ExtendState) + switch (g_pVoteManager->GetExtendState()) { case EExtendState::MAP_START: ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Extend vote is not open yet."); @@ -410,8 +378,8 @@ CON_COMMAND_CHAT(ve, "- Vote to extend current map") return; } - int iCurrentExtendCount = GetCurrentExtendCount(); - int iNeededExtendCount = GetNeededExtendCount(); + int iCurrentExtendCount = g_pVoteManager->GetCurrentExtendCount(); + int iNeededExtendCount = g_pVoteManager->GetNeededExtendCount(); if (pPlayer->GetExtendVote()) { @@ -419,22 +387,22 @@ CON_COMMAND_CHAT(ve, "- Vote to extend current map") return; } - if (pPlayer->GetExtendVoteTime() + 60.0f > gpGlobals->curtime) + if (pPlayer->GetExtendVoteTime() + 60.0f > GetGlobals()->curtime) { - int iRemainingTime = (int)(pPlayer->GetExtendVoteTime() + 60.0f - gpGlobals->curtime); + int iRemainingTime = (int)(pPlayer->GetExtendVoteTime() + 60.0f - GetGlobals()->curtime); ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Wait %i seconds before you can vote extend again.", iRemainingTime); return; } if (iCurrentExtendCount + 1 >= iNeededExtendCount) { - StartExtendVote(VOTE_CALLER_SERVER); + g_pVoteManager->StartExtendVote(VOTE_CALLER_SERVER); return; } pPlayer->SetExtendVote(true); - pPlayer->SetExtendVoteTime(gpGlobals->curtime); + pPlayer->SetExtendVoteTime(GetGlobals()->curtime); ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s wants to extend the map (%i voted, %i needed).", player->GetPlayerName(), iCurrentExtendCount + 1, iNeededExtendCount); } @@ -487,7 +455,7 @@ CON_COMMAND_CHAT_FLAGS(adminve, "Start a vote extend immediately.", ADMFLAG_CHAN return; } - if (g_ExtendState == EExtendState::IN_PROGRESS || bVoteStarting) + if (g_pVoteManager->GetExtendState() == EExtendState::IN_PROGRESS || g_pVoteManager->IsVoteStarting()) { ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "An extend vote is already in progress."); return; @@ -497,7 +465,7 @@ CON_COMMAND_CHAT_FLAGS(adminve, "Start a vote extend immediately.", ADMFLAG_CHAN if (player) slot = player->GetPlayerSlot(); - StartExtendVote(slot); + g_pVoteManager->StartExtendVote(slot); } CON_COMMAND_CHAT_FLAGS(disablertv, "- Disable the ability for players to vote to end current map sooner", ADMFLAG_CHANGEMAP) @@ -505,7 +473,7 @@ CON_COMMAND_CHAT_FLAGS(disablertv, "- Disable the ability for players to vote to if (!g_bVoteManagerEnable) return; - if (g_RTVState == ERTVState::BLOCKED_BY_ADMIN) + if (g_pVoteManager->GetRTVState() == ERTVState::BLOCKED_BY_ADMIN) { if (player) ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "RTV is already disabled."); @@ -516,7 +484,7 @@ CON_COMMAND_CHAT_FLAGS(disablertv, "- Disable the ability for players to vote to const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; - g_RTVState = ERTVState::BLOCKED_BY_ADMIN; + g_pVoteManager->SetRTVState(ERTVState::BLOCKED_BY_ADMIN); ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX ADMIN_PREFIX "disabled vote for RTV.", pszCommandPlayerName); } @@ -526,7 +494,7 @@ CON_COMMAND_CHAT_FLAGS(enablertv, "- Restore the ability for players to vote to if (!g_bVoteManagerEnable) return; - if (g_RTVState == ERTVState::RTV_ALLOWED) + if (g_pVoteManager->GetRTVState() == ERTVState::RTV_ALLOWED) { if (player) ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "RTV is not disabled."); @@ -537,7 +505,7 @@ CON_COMMAND_CHAT_FLAGS(enablertv, "- Restore the ability for players to vote to const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; - g_RTVState = ERTVState::RTV_ALLOWED; + g_pVoteManager->SetRTVState(ERTVState::RTV_ALLOWED); ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX ADMIN_PREFIX "enabled vote for RTV.", pszCommandPlayerName); } @@ -547,16 +515,19 @@ CON_COMMAND_CHAT(extendsleft, "- Display amount of extends left for the current if (!g_bVoteManagerEnable) return; - if (g_iMaxExtends - g_iExtends <= 0) - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "There are no extends left, the map was already extended %i/%i times.", g_iExtends, g_iMaxExtends); - else if (g_ExtendState == EExtendState::POST_EXTEND_FAILED) - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The map had %i/%i extends left, but the last extend vote failed.", g_iMaxExtends - g_iExtends, g_iMaxExtends); + if (g_iMaxExtends - g_pVoteManager->GetExtends() <= 0) + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "There are no extends left, the map was already extended %i/%i times.", g_pVoteManager->GetExtends(), g_iMaxExtends); + else if (g_pVoteManager->GetExtendState() == EExtendState::POST_EXTEND_FAILED) + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The map had %i/%i extends left, but the last extend vote failed.", g_iMaxExtends - g_pVoteManager->GetExtends(), g_iMaxExtends); else - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The map has %i/%i extends left.", g_iMaxExtends - g_iExtends, g_iMaxExtends); + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "The map has %i/%i extends left.", g_iMaxExtends - g_pVoteManager->GetExtends(), g_iMaxExtends); } CON_COMMAND_CHAT(timeleft, "- Display time left to end of current map.") { + if (!GetGlobals() || !g_pGameRules) + return; + if (!player) { ClientPrint(player, HUD_PRINTCONSOLE, CHAT_PREFIX "You cannot use this command from the server console."); @@ -575,7 +546,7 @@ CON_COMMAND_CHAT(timeleft, "- Display time left to end of current map.") return; } - int iTimeleft = (int)((g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - gpGlobals->curtime); + int iTimeleft = (int)((g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - GetGlobals()->curtime); if (iTimeleft < 0) { @@ -593,9 +564,11 @@ CON_COMMAND_CHAT(timeleft, "- Display time left to end of current map.") ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Timeleft: %i seconds", iSecondsLeft); } -void ExtendMap(int iMinutes) +void CVoteManager::ExtendMap(int iMinutes, bool bAllowExtraTime) { - // mimic behaviour of !extend + if (!GetGlobals() || !g_pGameRules) + return; + // CONVAR_TODO ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_timelimit")); @@ -603,17 +576,13 @@ void ExtendMap(int iMinutes) // HACK: values is actually the cvar value itself, hence this ugly cast. float flTimelimit = *(float*)&cvar->values; - if (gpGlobals->curtime - g_pGameRules->m_flGameStartTime > flTimelimit * 60) - flTimelimit = (gpGlobals->curtime - g_pGameRules->m_flGameStartTime) / 60.0f + iMinutes; + if (bAllowExtraTime && GetGlobals()->curtime - g_pGameRules->m_flGameStartTime > flTimelimit * 60) + flTimelimit = (GetGlobals()->curtime - g_pGameRules->m_flGameStartTime) / 60.0f + iMinutes; else - { - if (flTimelimit == 1) - flTimelimit = 0; flTimelimit += iMinutes; - } if (flTimelimit <= 0) - flTimelimit = 1; + flTimelimit = 0.01f; char buf[32]; V_snprintf(buf, sizeof(buf), "mp_timelimit %.6f", flTimelimit); @@ -622,7 +591,7 @@ void ExtendMap(int iMinutes) g_pEngineServer2->ServerCommand(buf); } -void VoteExtendHandler(YesNoVoteAction action, int param1, int param2) +void CVoteManager::VoteExtendHandler(YesNoVoteAction action, int param1, int param2) { switch (action) { @@ -646,7 +615,7 @@ void VoteExtendHandler(YesNoVoteAction action, int param1, int param2) // Admin cancelled so stop further votes // It will reenable if an admin manually calls a vote if (g_ExtendVoteMode == EExtendVoteMode::EXTENDVOTE_AUTO) - g_ExtendState = EExtendState::POST_EXTEND_FAILED; + m_ExtendState = EExtendState::POST_EXTEND_FAILED; } break; @@ -655,7 +624,7 @@ void VoteExtendHandler(YesNoVoteAction action, int param1, int param2) } // return true to show vote pass, false to show fail -bool VoteExtendEndCallback(YesNoVoteInfo info) +bool CVoteManager::VoteExtendEndCallback(YesNoVoteInfo info) { // ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Vote end: numvotes:%d yes:%d no:%d numclients:%d", info.num_votes, info.yes_votes, info.no_votes, info.num_clients); @@ -669,36 +638,36 @@ bool VoteExtendEndCallback(YesNoVoteInfo info) if (yes_percent >= g_flExtendSucceedRatio) { ExtendMap(g_iExtendTimeToAdd); - g_iExtends++; + m_iExtends++; - if (g_iMaxExtends - g_iExtends <= 0) + if (g_iMaxExtends - m_iExtends <= 0) // there are no extends left after a successfull extend vote - g_ExtendState = EExtendState::POST_EXTEND_NO_EXTENDS_LEFT; + m_ExtendState = EExtendState::POST_EXTEND_NO_EXTENDS_LEFT; else { // there's an extend left after a successfull extend vote if (g_ExtendVoteMode == EExtendVoteMode::EXTENDVOTE_AUTO) { // small delay to allow cvar change to go through - new CTimer(0.1, false, true, []() { - g_ExtendState = EExtendState::EXTEND_ALLOWED; + new CTimer(0.1, false, true, [this]() { + m_ExtendState = EExtendState::EXTEND_ALLOWED; return -1.0f; }); } else { - g_ExtendState = EExtendState::POST_EXTEND_COOLDOWN; + m_ExtendState = EExtendState::POST_EXTEND_COOLDOWN; // Allow another extend vote after added time lapses - new CTimer(g_iExtendTimeToAdd * 60.0f, false, true, []() { - if (g_ExtendState == EExtendState::POST_EXTEND_COOLDOWN) - g_ExtendState = EExtendState::EXTEND_ALLOWED; + new CTimer(g_iExtendTimeToAdd * 60.0f, false, true, [this]() { + if (m_ExtendState == EExtendState::POST_EXTEND_COOLDOWN) + m_ExtendState = EExtendState::EXTEND_ALLOWED; return -1.0f; }); } } - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < MAXPLAYERS; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); if (pPlayer) @@ -711,36 +680,99 @@ bool VoteExtendEndCallback(YesNoVoteInfo info) } // Vote failed so we don't allow any more votes - g_ExtendState = EExtendState::POST_EXTEND_FAILED; + m_ExtendState = EExtendState::POST_EXTEND_FAILED; ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote failed! Further extend votes disabled!", g_iExtendTimeToAdd); return false; } -static int iVoteEndTicks = 3; -void StartExtendVote(int iCaller) +void CVoteManager::StartExtendVote(int iCaller) { - if (g_ExtendState == EExtendState::IN_PROGRESS) + if (m_ExtendState == EExtendState::IN_PROGRESS) return; char sDetailStr[64]; V_snprintf(sDetailStr, sizeof(sDetailStr), "Vote to extend the map for another %d minutes", g_iExtendTimeToAdd); - g_ExtendState = EExtendState::IN_PROGRESS; + m_ExtendState = EExtendState::IN_PROGRESS; - g_pPanoramaVoteHandler->SendYesNoVoteToAll(g_flExtendVoteDuration, iCaller, "#SFUI_vote_passed_nextlevel_extend", - sDetailStr, &VoteExtendEndCallback, &VoteExtendHandler); + g_pPanoramaVoteHandler->SendYesNoVoteToAll(g_flExtendVoteDuration, iCaller, "#SFUI_vote_passed_nextlevel_extend", sDetailStr, + std::bind(&CVoteManager::VoteExtendEndCallback, this, std::placeholders::_1), std::bind(&CVoteManager::VoteExtendHandler, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - new CTimer(g_flExtendVoteDuration - 3.0f, false, true, []() { - if (iVoteEndTicks == 0 || g_ExtendState != EExtendState::IN_PROGRESS) + new CTimer(g_flExtendVoteDuration - 3.0f, false, true, [this]() { + if (m_iVoteEndTicks == 0 || m_ExtendState != EExtendState::IN_PROGRESS) { - iVoteEndTicks = 3; + m_iVoteEndTicks = 3; return -1.0f; } - ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote ending in %d....", iVoteEndTicks); - iVoteEndTicks--; + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Extend vote ending in %d....", m_iVoteEndTicks); + m_iVoteEndTicks--; return 1.0f; }); } + +void CVoteManager::OnRoundEnd() +{ + if (!GetGlobals() || !g_pGameRules) + return; + + ConVar* cvar = g_pCVar->GetConVar(g_pCVar->FindConVar("mp_timelimit")); + + // CONVAR_TODO + // HACK: values is actually the cvar value itself, hence this ugly cast. + float flTimelimit = *(float*)&cvar->values; + + int iTimeleft = (int)((g_pGameRules->m_flGameStartTime + flTimelimit * 60.0f) - GetGlobals()->curtime); + + // check for end of last round + if (iTimeleft < 0) + { + m_RTVState = ERTVState::POST_LAST_ROUND_END; + m_ExtendState = EExtendState::POST_LAST_ROUND_END; + } +} + +bool CVoteManager::CheckRTVStatus() +{ + if (!g_bVoteManagerEnable || m_RTVState != ERTVState::RTV_ALLOWED || !GetGlobals() || !g_pGameRules) + return false; + + int iCurrentRTVCount = GetCurrentRTVCount(); + int iNeededRTVCount = GetNeededRTVCount(); + + if (iCurrentRTVCount >= iNeededRTVCount) + { + m_RTVState = ERTVState::POST_RTV_SUCCESSFULL; + m_ExtendState = EExtendState::POST_RTV; + // CONVAR_TODO + g_pEngineServer2->ServerCommand("mp_timelimit 0.01"); + + if (g_bRTVEndRound) + { + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "RTV succeeded! Ending the map now..."); + + new CTimer(3.0f, false, true, []() { + g_pGameRules->TerminateRound(5.0f, CSRoundEndReason::Draw); + + return -1.0f; + }); + } + else + { + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "RTV succeeded! This is the last round of the map!"); + } + + for (int i = 0; i < GetGlobals()->maxClients; i++) + { + ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); + if (pPlayer) + pPlayer->SetRTVVote(false); + } + + return true; + } + + return false; +} diff --git a/src/votemanager.h b/src/votemanager.h index 51b2016a..85e4cc45 100644 --- a/src/votemanager.h +++ b/src/votemanager.h @@ -20,6 +20,8 @@ #pragma once +#include "panoramavote.h" + enum class ERTVState { MAP_START, @@ -50,13 +52,39 @@ enum EExtendVoteMode EXTENDVOTE_AUTO, // Extend votes can be triggered by !ve or when map timelimit reaches a given value }; -extern ERTVState g_RTVState; -extern EExtendState g_ExtendState; extern bool g_bVoteManagerEnable; -void VoteManager_Init(); -void SetExtendsLeft(); -void ExtendMap(int iMinutes); +class CVoteManager +{ +public: + void VoteManager_Init(); + float TimerCheckTimeleft(); + int GetCurrentRTVCount(); + int GetNeededRTVCount(); + int GetCurrentExtendCount(); + int GetNeededExtendCount(); + void ExtendMap(int iMinutes, bool bAllowExtraTime = true); + void VoteExtendHandler(YesNoVoteAction action, int param1, int param2); + bool VoteExtendEndCallback(YesNoVoteInfo info); + void StartExtendVote(int iCaller); + void OnRoundEnd(); + bool CheckRTVStatus(); + + ERTVState GetRTVState() { return m_RTVState; } + EExtendState GetExtendState() { return m_ExtendState; } + int GetExtends() { return m_iExtends; } + bool IsVoteStarting() { return m_bVoteStarting; } + void SetRTVState(ERTVState RTVState) { m_RTVState = RTVState; } + void SetExtendState(EExtendState ExtendState) { m_ExtendState = ExtendState; } + +private: + ERTVState m_RTVState = ERTVState::MAP_START; + EExtendState m_ExtendState = EExtendState::MAP_START; + int m_iExtends = 0; + int m_iVoteEndTicks = 3; + int m_iVoteStartTicks = 3; + bool m_bVoteStarting = false; + const float m_flExtendVoteTickrate = 1.0f; +}; -float TimerCheckTimeleft(); -void StartExtendVote(int iCaller); \ No newline at end of file +extern CVoteManager* g_pVoteManager; \ No newline at end of file diff --git a/src/zombiereborn.cpp b/src/zombiereborn.cpp index 891c087d..a46e7969 100644 --- a/src/zombiereborn.cpp +++ b/src/zombiereborn.cpp @@ -47,7 +47,7 @@ using ordered_json = nlohmann::ordered_json; extern CGameEntitySystem* g_pEntitySystem; extern IVEngineServer2* g_pEngineServer2; -extern CGlobalVars* gpGlobals; +extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; extern IGameEventManager2* g_gameEventManager; extern IGameEventSystem* g_gameEventSystem; @@ -375,7 +375,13 @@ void CZRPlayerClassManager::LoadPlayerClass() // Less code than constantly traversing the full class vectors, temporary lifetime anyways std::set setClassNames; - ordered_json jsonPlayerClasses = ordered_json::parse(jsoncFile, nullptr, true, true); + ordered_json jsonPlayerClasses = ordered_json::parse(jsoncFile, nullptr, false, true); + + if (jsonPlayerClasses.is_discarded()) + { + Panic("Failed parsing JSON from %s. Playerclasses not loaded\n", pszJsonPath); + return; + } for (auto& [szTeamName, jsonTeamClasses] : jsonPlayerClasses.items()) { @@ -959,7 +965,10 @@ std::shared_ptr ZRHitgroupConfig::FindHitgroupIndex(int iIndex) void ZR_RespawnAll() { - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); @@ -988,7 +997,10 @@ void ZR_OnRoundPrestart(IGameEvent* pEvent) g_ZRRoundState = EZRRoundState::ROUND_START; ToggleRespawn(true, true); - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); @@ -1035,7 +1047,10 @@ void ZR_OnRoundStart(IGameEvent* pEvent) SetupRespawnToggler(); CZRRegenTimer::RemoveAllTimers(); - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals()) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); @@ -1241,9 +1256,13 @@ void ZR_InfectShake(CCSPlayerController* pController) std::vector ZR_GetSpawns() { + std::vector spawns; + + if (!g_pGameRules) + return spawns; + CUtlVector* ctSpawns = g_pGameRules->m_CTSpawnPoints(); CUtlVector* tSpawns = g_pGameRules->m_TerroristSpawnPoints(); - std::vector spawns; FOR_EACH_VEC(*ctSpawns, i) spawns.push_back((*ctSpawns)[i]); @@ -1343,9 +1362,12 @@ void ZR_InfectMotherZombie(CCSPlayerController* pVictimController, std::vector pCandidateControllers; - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { CCSPlayerController* pController = CCSPlayerController::FromSlot(i); if (!pController || !pController->IsConnected() || pController->m_iTeamNum() != CS_TEAM_CT) @@ -1432,7 +1454,7 @@ void ZR_InitialInfection() } // reduce everyone's immunity except mz - for (int i = 0; i < gpGlobals->maxClients; i++) + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); if (!pPlayer || vecIsMZ[i]) @@ -1570,6 +1592,9 @@ void SpawnPlayer(CCSPlayerController* pController) // Make sure the round ends if spawning into an empty server if (!ZR_IsTeamAlive(CS_TEAM_CT) && !ZR_IsTeamAlive(CS_TEAM_T) && g_ZRRoundState != EZRRoundState::ROUND_END) { + if (!g_pGameRules) + return; + g_pGameRules->TerminateRound(1.0f, CSRoundEndReason::GameStart); g_ZRRoundState = EZRRoundState::ROUND_END; return; @@ -1721,7 +1746,10 @@ void ZR_EndRoundAndAddTeamScore(int iTeamNum) { bool bServerIdle = true; - for (int i = 0; i < gpGlobals->maxClients; i++) + if (!GetGlobals() || !g_pGameRules) + return; + + for (int i = 0; i < GetGlobals()->maxClients; i++) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);