//=============================================================================// // // Purpose: Runs the state machine for the host & server. // //=============================================================================// // host_state.cpp:. // ///////////////////////////////////////////////////////////////////////////////// #include "core/stdafx.h" #include "tier0/jobthread.h" #include "tier0/commandline.h" #include "tier0/fasttimer.h" #include "tier0/frametask.h" #include "tier1/cvar.h" #include "tier1/NetAdr.h" #include "tier2/socketcreator.h" #include "datacache/mdlcache.h" #ifndef CLIENT_DLL #include "engine/server/sv_rcon.h" #include "engine/server/server.h" #endif // !CLIENT_DLL #ifndef DEDICATED #include "engine/client/cl_rcon.h" #include "engine/client/cl_main.h" #include "engine/client/clientstate.h" #endif // DEDICATED #include "engine/cmd.h" #include "engine/net.h" #include "engine/gl_screen.h" #include "engine/host.h" #include "engine/host_cmd.h" #include "engine/host_state.h" #include "engine/sys_engine.h" #include "engine/modelloader.h" #include "engine/cmodel_bsp.h" #ifndef CLIENT_DLL #include "engine/server/server.h" #endif // !CLIENT_DLL #include "rtech/stryder/stryder.h" #include "rtech/playlists/playlists.h" #ifndef DEDICATED #include "vgui/vgui_baseui_interface.h" #include "client/vengineclient_impl.h" #include "gameui/imgui_system.h" #endif // DEDICATED #include "networksystem/pylon.h" #ifndef CLIENT_DLL #include "networksystem/bansystem.h" #include "networksystem/hostmanager.h" #endif // !CLIENT_DLL #include "networksystem/listmanager.h" #include "public/edict.h" #ifndef CLIENT_DLL #include "game/server/gameinterface.h" #endif // !CLIENT_DLL #include "game/shared/vscript_shared.h" #ifndef CLIENT_DLL static ConVar sv_pylonVisibility("sv_pylonVisibility", "0", FCVAR_RELEASE, "Determines the visibility to the Pylon master server.", "0 = Offline, 1 = Hidden, 2 = Public."); static ConVar sv_pylonRefreshRate("sv_pylonRefreshRate", "5.0", FCVAR_DEVELOPMENTONLY, "Pylon host refresh rate (seconds)."); static ConVar sv_autoReloadRate("sv_autoReloadRate", "0", FCVAR_RELEASE, "Time in seconds between each server auto-reload (disabled if null)."); #endif // !CLIENT_DLL #ifdef DEDICATED static ConVar hostdesc("hostdesc", "", FCVAR_RELEASE, "Host game server description."); //----------------------------------------------------------------------------- // Purpose: Send keep alive request to Pylon Master Server. // Output : Returns true on success, false otherwise. //----------------------------------------------------------------------------- static void HostState_KeepAlive() { if (!g_pServer->IsActive() || !sv_pylonVisibility.GetBool()) // Check for active game. { return; } const NetGameServer_t gameServer { hostname->GetString(), hostdesc.GetString(), sv_pylonVisibility.GetInt() == ServerVisibility_e::HIDDEN, g_pHostState->m_levelName, v_Playlists_GetCurrent(), hostip->GetString(), hostport->GetInt(), g_pNetKey->GetBase64NetKey(), *g_nServerRemoteChecksum, SDK_VERSION, g_pServer->GetNumClients(), g_ServerGlobalVariables->m_nMaxClients, std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count() }; std::thread request([&, gameServer] { string errorMsg; string hostToken; string hostIp; const bool result = g_MasterServer.PostServerHost(errorMsg, hostToken, hostIp, gameServer); // Apply the data the next frame g_TaskQueue.Dispatch([result, errorMsg, hostToken, hostIp] { if (!result) { if (!errorMsg.empty() && g_ServerHostManager.GetCurrentError().compare(errorMsg) != NULL) { g_ServerHostManager.SetCurrentError(errorMsg); Error(eDLL_T::SERVER, NO_ERROR, "%s\n", errorMsg.c_str()); } } else // Attempt to log the token, if there is one. { if (!hostToken.empty() && g_ServerHostManager.GetCurrentToken().compare(hostToken) != NULL) { g_ServerHostManager.SetCurrentToken(hostToken); Msg(eDLL_T::SERVER, "Published server with token: %s'%s%s%s'\n", g_svReset, g_svGreyB, hostToken.c_str(), g_svReset); } } if (hostIp.length() != 0) g_ServerHostManager.SetHostIP(hostIp); }, 0); } ); request.detach(); } #endif // DEDICATED //----------------------------------------------------------------------------- // Purpose: state machine's main processing loop //----------------------------------------------------------------------------- void CHostState::FrameUpdate(CHostState* pHostState, double flCurrentTime, float flFrameTime) { static bool bInitialized = false; static bool bResetIdleName = false; if (!bInitialized) { g_pHostState->Setup(); bInitialized = true; } g_pHostState->Think(); #ifndef CLIENT_DLL RCONServer()->RunFrame(); #endif // !CLIENT_DLL #ifndef DEDICATED RCONClient()->RunFrame(); #endif // !DEDICATED // Disable "warning C4611: interaction between '_setjmp' and C++ object destruction is non-portable" #pragma warning(push) #pragma warning(disable : 4611) if (setjmp(*host_abortserver)) { g_pHostState->Init(); return; } #pragma warning(pop) else { #ifndef CLIENT_DLL *g_bAbortServerSet = true; #endif // !CLIENT_DLL while (true) { Cbuf_Execute(); const HostStates_t oldState = g_pHostState->m_iCurrentState; switch (g_pHostState->m_iCurrentState) { case HostStates_t::HS_NEW_GAME: { g_pHostState->State_NewGame(); break; } case HostStates_t::HS_CHANGE_LEVEL_SP: { g_pHostState->State_ChangeLevelSP(); break; } case HostStates_t::HS_CHANGE_LEVEL_MP: { g_pHostState->State_ChangeLevelMP(); break; } case HostStates_t::HS_RUN: { if (!g_pHostState->m_bActiveGame) { if (bResetIdleName) { g_pHostState->ResetLevelName(); bResetIdleName = false; } } else // Reset idle name the next non-active frame. { bResetIdleName = true; } CHostState__State_Run(&g_pHostState->m_iCurrentState, flCurrentTime, flFrameTime); break; } case HostStates_t::HS_GAME_SHUTDOWN: { Msg(eDLL_T::ENGINE, "%s: Shutdown host game\n", __FUNCTION__); CHostState__State_GameShutDown(g_pHostState); break; } case HostStates_t::HS_RESTART: { Msg(eDLL_T::ENGINE, "%s: Restarting state machine\n", __FUNCTION__); #ifndef DEDICATED v_CL_EndMovie(); #endif // !DEDICATED v_Stryder_SendOfflineRequest(); // We have hostnames nulled anyway. g_pEngine->SetNextState(IEngine::DLL_RESTART); break; } case HostStates_t::HS_SHUTDOWN: { Msg(eDLL_T::ENGINE, "%s: Shutdown state machine\n", __FUNCTION__); #ifndef DEDICATED v_CL_EndMovie(); #endif // !DEDICATED v_Stryder_SendOfflineRequest(); // We have hostnames nulled anyway. g_pEngine->SetNextState(IEngine::DLL_CLOSE); break; } default: { break; } } // only do a single pass at HS_RUN per frame. All other states loop until they reach HS_RUN if (oldState == HostStates_t::HS_RUN && (g_pHostState->m_iNextState != HostStates_t::HS_LOAD_GAME || !single_frame_shutdown_for_reload->GetBool())) break; // shutting down if (oldState == HostStates_t::HS_SHUTDOWN || oldState == HostStates_t::HS_RESTART) break; } } } //----------------------------------------------------------------------------- // Purpose: state machine initialization //----------------------------------------------------------------------------- void CHostState::Init(void) { if (m_iNextState != HostStates_t::HS_SHUTDOWN) { if (m_iNextState == HostStates_t::HS_GAME_SHUTDOWN) { CHostState__State_GameShutDown(this); } else { m_iCurrentState = HostStates_t::HS_RUN; if (m_iNextState != HostStates_t::HS_SHUTDOWN || !single_frame_shutdown_for_reload->GetInt()) m_iNextState = HostStates_t::HS_RUN; } } m_flShortFrameTime = 1.0f; m_bActiveGame = false; m_bRememberLocation = false; m_bBackgroundLevel = false; m_bWaitingForConnection = false; m_levelName[0] = 0; m_landMarkName[0] = 0; m_mapGroupName[0] = 0; m_bSplitScreenConnect = false; m_bGameHasShutDownAndFlushedMemory = true; m_vecLocation.Init(); m_angLocation.Init(); m_iServerState = HostStates_t::HS_NEW_GAME; } //----------------------------------------------------------------------------- // Purpose: state machine setup //----------------------------------------------------------------------------- void CHostState::Setup(void) { g_pHostState->LoadConfig(); #ifndef CLIENT_DLL g_BanSystem.LoadList(); #endif // !CLIENT_DLL ConVar_PurgeHostNames(); #ifndef CLIENT_DLL RCONServer()->Init(); #endif // !CLIENT_DLL #ifndef DEDICATED RCONClient()->Init(); #endif // !DEDICATED if (net_useRandomKey.GetBool()) { NET_GenerateKey(); } #if !defined (DEDICATED) && !defined (CLIENT_DLL) // Parallel processing of 'C_BaseAnimating::SetupBones()' is currently // not supported on listen servers running the local client due to an // engine bug specific to S3 that still needs to be addressed. Remove // this once the issue has been solved: if (cl_threaded_bone_setup->GetBool()) { cl_threaded_bone_setup->SetValue(false); } #endif // !DEDICATED && !CLIENT_DLL ResetLevelName(); } //----------------------------------------------------------------------------- // Purpose: think //----------------------------------------------------------------------------- void CHostState::Think(void) const { #ifndef CLIENT_DLL static bool bInitialized = false; static CFastTimer statsTimer; static CFastTimer banListTimer; #ifdef DEDICATED static CFastTimer pylonTimer; #endif // DEDICATED if (!bInitialized) // Initialize clocks. { statsTimer.Start(); banListTimer.Start(); #ifdef DEDICATED pylonTimer.Start(); #endif // DEDICATED bInitialized = true; } if (sv_autoReloadRate.GetBool()) { if (g_ServerGlobalVariables->m_flCurTime > sv_autoReloadRate.GetFloat()) { Cbuf_AddText(Cbuf_GetCurrentPlayer(), "reload\n", cmd_source_t::kCommandSrcCode); } } if (statsTimer.GetDurationInProgress().GetSeconds() > sv_statusRefreshRate.GetFloat()) { SetConsoleTitleA(Format("%s - %d/%d Players (%s on %s) - %d%% Server CPU (%.3f msec on frame %d)", hostname->GetString(), g_pServer->GetNumClients(), g_ServerGlobalVariables->m_nMaxClients, v_Playlists_GetCurrent(), m_levelName, static_cast(g_pServer->GetCPUUsage() * 100.0f), (g_pEngine->GetFrameTime() * 1000.0f), g_pServer->GetTick()).c_str()); statsTimer.Start(); } if (sv_globalBanlist.GetBool() && banListTimer.GetDurationInProgress().GetSeconds() > sv_banlistRefreshRate.GetFloat()) { SV_CheckClientsForBan(); banListTimer.Start(); } #ifdef DEDICATED if (pylonTimer.GetDurationInProgress().GetSeconds() > sv_pylonRefreshRate.GetFloat()) { HostState_KeepAlive(); pylonTimer.Start(); } #endif // DEDICATED #endif // !CLIENT_DLL } //----------------------------------------------------------------------------- // Purpose: load and execute configuration files //----------------------------------------------------------------------------- void CHostState::LoadConfig(void) const { if (CommandLine()->ParmValue("-launcher", 0) < 1) // Launcher level 1 indicates everything is handled from the commandline/launcher. { if (!CommandLine()->CheckParm("-devsdk")) { Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec.cfg\n", cmd_source_t::kCommandSrcCode); #ifndef CLIENT_DLL Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec_server.cfg\n", cmd_source_t::kCommandSrcCode); Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec tools/rcon_server.cfg\n", cmd_source_t::kCommandSrcCode); #endif //!CLIENT_DLL #ifndef DEDICATED Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec_client.cfg\n", cmd_source_t::kCommandSrcCode); Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec tools/rcon_client.cfg\n", cmd_source_t::kCommandSrcCode); #endif // !DEDICATED } else // Development configs. { Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec_dev.cfg\n", cmd_source_t::kCommandSrcCode); #ifndef CLIENT_DLL Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec_server_dev.cfg\n", cmd_source_t::kCommandSrcCode); Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec tools/rcon_server_dev.cfg\n", cmd_source_t::kCommandSrcCode); #endif //!CLIENT_DLL #ifndef DEDICATED Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec system/autoexec_client_dev.cfg\n", cmd_source_t::kCommandSrcCode); Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec tools/rcon_client_dev.cfg\n", cmd_source_t::kCommandSrcCode); #endif // !DEDICATED } #ifndef DEDICATED Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec bind.cfg\n", cmd_source_t::kCommandSrcCode); #endif // !DEDICATED } } //----------------------------------------------------------------------------- // Purpose: set state machine // Input : newState - // clearNext - //----------------------------------------------------------------------------- void CHostState::SetState(const HostStates_t newState) { m_iCurrentState = newState; // If our next state isn't a shutdown, or its a forced shutdown then set // next state to run. if (m_iNextState != HostStates_t::HS_SHUTDOWN || !host_hasIrreversibleShutdown->GetBool()) { m_iNextState = newState; } } //----------------------------------------------------------------------------- // Purpose: shutdown active game //----------------------------------------------------------------------------- void CHostState::GameShutDown(void) { if (m_bActiveGame) { #ifndef CLIENT_DLL g_pServerGameDLL->GameShutdown(); #endif // !CLIENT_DLL m_bActiveGame = false; ResetLevelName(); } } //----------------------------------------------------------------------------- // Purpose: initialize new game //----------------------------------------------------------------------------- void CHostState::State_NewGame(void) { Msg(eDLL_T::ENGINE, "%s: Loading level: '%s'\n", __FUNCTION__, g_pHostState->m_levelName); LARGE_INTEGER time{}; #ifndef CLIENT_DLL const bool bSplitScreenConnect = m_bSplitScreenConnect; m_bSplitScreenConnect = false; if (!g_pServerGameClients) // Init Game if it ain't valid. { SV_InitGameDLL(); } #endif // !CLIENT_DLL #ifndef CLIENT_DLL if (!CModelLoader__Map_IsValid(g_pModelLoader, m_levelName) // Check if map is valid and if we can start a new game. || !v_Host_NewGame(m_levelName, nullptr, m_bBackgroundLevel, bSplitScreenConnect, time) || !g_pServerGameClients) { Error(eDLL_T::ENGINE, NO_ERROR, "%s: Level not valid\n", __FUNCTION__); #ifndef DEDICATED SCR_EndLoadingPlaque(); #endif // !DEDICATED GameShutDown(); } #endif // !CLIENT_DLL SetState(HostStates_t::HS_RUN); } //----------------------------------------------------------------------------- // Purpose: change singleplayer level //----------------------------------------------------------------------------- void CHostState::State_ChangeLevelSP(void) { Msg(eDLL_T::ENGINE, "%s: Changing singleplayer level to: '%s'\n", __FUNCTION__, m_levelName); m_flShortFrameTime = 1.5; // Set frame time. if (CModelLoader__Map_IsValid(g_pModelLoader, m_levelName)) // Check if map is valid and if we can start a new game. { v_Host_ChangeLevel(true, m_levelName, m_mapGroupName); // Call change level as singleplayer level. } else { Error(eDLL_T::ENGINE, NO_ERROR, "%s: Unable to find level: '%s'\n", __FUNCTION__, m_levelName); } // Set current state to run. SetState(HostStates_t::HS_RUN); } //----------------------------------------------------------------------------- // Purpose: change multiplayer level //----------------------------------------------------------------------------- void CHostState::State_ChangeLevelMP(void) { Msg(eDLL_T::ENGINE, "%s: Changing multiplayer level to: '%s'\n", __FUNCTION__, m_levelName); m_flShortFrameTime = 0.5; // Set frame time. #ifndef CLIENT_DLL g_pServerGameDLL->LevelShutdown(); #endif // !CLIENT_DLL if (CModelLoader__Map_IsValid(g_pModelLoader, m_levelName)) // Check if map is valid and if we can start a new game. { #ifndef DEDICATED g_pEngineVGui->EnabledProgressBarForNextLoad(); #endif // !DEDICATED v_Host_ChangeLevel(false, m_levelName, m_mapGroupName); // Call change level as multiplayer level. } else { Error(eDLL_T::ENGINE, NO_ERROR, "%s: Unable to find level: '%s'\n", __FUNCTION__, m_levelName); } // Set current state to run. SetState(HostStates_t::HS_RUN); } //----------------------------------------------------------------------------- // Purpose: resets the level name //----------------------------------------------------------------------------- void CHostState::ResetLevelName(void) { static const char* szNoMap = "no_map"; Q_snprintf(const_cast(m_levelName), sizeof(m_levelName), "%s", szNoMap); } void VHostState::Detour(const bool bAttach) const { DetourSetup(&CHostState__FrameUpdate, &CHostState::FrameUpdate, bAttach); } /////////////////////////////////////////////////////////////////////////////// CHostState* g_pHostState = nullptr;