diff --git a/mp/src/game/client/neo/ui/neo_root.cpp b/mp/src/game/client/neo/ui/neo_root.cpp index ea2f42b89..0d32f4dc0 100644 --- a/mp/src/game/client/neo/ui/neo_root.cpp +++ b/mp/src/game/client/neo/ui/neo_root.cpp @@ -20,6 +20,8 @@ #include "tier1/interface.h" #include #include "ui/neo_loading.h" +#include "neo_gamerules.h" +#include "neo_misc.h" #include #include @@ -348,9 +350,9 @@ void CNeoRoot::ApplySchemeSettings(IScheme *pScheme) g_uiCtx.iMarginY = tall / 108; g_iAvatar = wide / 30; const float flWide = static_cast(wide); - float flWideAs43 = static_cast(tall) * (4.0f / 3.0f); - if (flWideAs43 > flWide) flWideAs43 = flWide; - g_iRootSubPanelWide = static_cast(flWideAs43 * 0.9f); + m_flWideAs43 = static_cast(tall) * (4.0f / 3.0f); + if (m_flWideAs43 > flWide) m_flWideAs43 = flWide; + g_iRootSubPanelWide = static_cast(m_flWideAs43 * 0.9f); constexpr int PARTITION = GSIW__TOTAL * 4; const int iSubDiv = g_iRootSubPanelWide / PARTITION; @@ -669,7 +671,15 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) g_uiCtx.dPanel.x = iRightXPos; g_uiCtx.dPanel.y = iRightSideYStart; - g_uiCtx.dPanel.wide = g_iRootSubPanelWide - iRightXPos + (g_uiCtx.iMarginX * 2); + if (engine->IsInGame()) + { + g_uiCtx.dPanel.wide = m_flWideAs43 * 0.7f; + g_uiCtx.flWgXPerc = 0.25f; + } + else + { + g_uiCtx.dPanel.wide = GetWide() - iRightXPos - (g_uiCtx.iMarginX * 2); + } NeoUI::BeginSection(); { if (engine->IsInGame()) @@ -677,6 +687,7 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) // Show the current server's information NeoUI::Label(L"Hostname:", m_wszHostname); NeoUI::Label(L"Map:", m_wszMap); + NeoUI::Label(L"Game mode:", NEO_GAME_TYPE_DESC_STRS[NEORules()->GetGameType()].wszStr); // TODO: more info, g_PR, scoreboard stuff, etc... } else diff --git a/mp/src/game/client/neo/ui/neo_root.h b/mp/src/game/client/neo/ui/neo_root.h index d435b41a5..9dda8a5e2 100644 --- a/mp/src/game/client/neo/ui/neo_root.h +++ b/mp/src/game/client/neo/ui/neo_root.h @@ -193,6 +193,7 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener bool m_bOnLoadingScreen = false; int m_iSavedYOffsets[NeoUI::MAX_SECTIONS] = {}; + float m_flWideAs43 = 0.0f; }; extern CNeoRoot *g_pNeoRoot; diff --git a/mp/src/game/client/neo/ui/neo_ui.cpp b/mp/src/game/client/neo/ui/neo_ui.cpp index 5a12c4f99..d0845f504 100644 --- a/mp/src/game/client/neo/ui/neo_ui.cpp +++ b/mp/src/game/client/neo/ui/neo_ui.cpp @@ -58,11 +58,11 @@ void BeginContext(NeoUI::Context *ctx, const NeoUI::Mode eMode, const wchar_t *w g_pCtx->eMode = eMode; g_pCtx->iLayoutY = -(g_pCtx->iYOffset[0] * g_pCtx->iRowTall); g_pCtx->iWidget = 0; - g_pCtx->iWgXPos = static_cast(g_pCtx->dPanel.wide * 0.4f); g_pCtx->iSection = 0; g_pCtx->iHasMouseInPanel = 0; g_pCtx->iHorizontalWidth = 0; g_pCtx->iHorizontalMargin = 0; + g_pCtx->flWgXPerc = 0.4f; g_pCtx->bValueEdited = false; g_pCtx->eButtonTextStyle = TEXTSTYLE_CENTER; g_pCtx->eLabelTextStyle = TEXTSTYLE_LEFT; @@ -194,6 +194,7 @@ void BeginSection(const bool bDefaultFocus) g_pCtx->iLayoutY = -(g_pCtx->iYOffset[g_pCtx->iSection] * g_pCtx->iRowTall); g_pCtx->iWidget = 0; g_pCtx->iCanActives = 0; + g_pCtx->iWgXPos = static_cast(g_pCtx->dPanel.wide * g_pCtx->flWgXPerc); g_pCtx->iMouseRelX = g_pCtx->iMouseAbsX - g_pCtx->dPanel.x; g_pCtx->iMouseRelY = g_pCtx->iMouseAbsY - g_pCtx->dPanel.y; diff --git a/mp/src/game/client/neo/ui/neo_ui.h b/mp/src/game/client/neo/ui/neo_ui.h index fa4c1c70a..9ab077338 100644 --- a/mp/src/game/client/neo/ui/neo_ui.h +++ b/mp/src/game/client/neo/ui/neo_ui.h @@ -143,6 +143,7 @@ struct Context int iPartitionY; // Only increments when Y-pos goes down int iLayoutX; int iLayoutY; + float flWgXPerc; int iWgXPos; int iYOffset[MAX_SECTIONS] = {}; bool abYMouseDragOffset[MAX_SECTIONS] = {}; diff --git a/mp/src/game/server/CMakeLists.txt b/mp/src/game/server/CMakeLists.txt index 39e3395a1..4eb8247cf 100644 --- a/mp/src/game/server/CMakeLists.txt +++ b/mp/src/game/server/CMakeLists.txt @@ -1333,6 +1333,8 @@ target_sources_grouped( neo/neo_client.cpp neo/neo_detpack.cpp neo/neo_detpack.h + neo/neo_game_config.cpp + neo/neo_game_config.h neo/neo_ghost_spawn_point.cpp neo/neo_ghost_spawn_point.h neo/neo_grenade.cpp diff --git a/mp/src/game/server/neo/neo_game_config.cpp b/mp/src/game/server/neo/neo_game_config.cpp new file mode 100644 index 000000000..6944c476c --- /dev/null +++ b/mp/src/game/server/neo/neo_game_config.cpp @@ -0,0 +1,10 @@ +#include "neo_game_config.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS(neo_game_config, CNEOGameConfig); + +BEGIN_DATADESC(CNEOGameConfig) + DEFINE_KEYFIELD(m_GameType, FIELD_INTEGER, "GameType"), +END_DATADESC() diff --git a/mp/src/game/server/neo/neo_game_config.h b/mp/src/game/server/neo/neo_game_config.h new file mode 100644 index 000000000..c56651b15 --- /dev/null +++ b/mp/src/game/server/neo/neo_game_config.h @@ -0,0 +1,14 @@ +#pragma once + +#include "cbase.h" +#include "baseentity.h" +#include "neo_gamerules.h" + +class CNEOGameConfig : public CLogicalEntity +{ + DECLARE_CLASS(CNEOGameConfig, CBaseEntity); + DECLARE_DATADESC(); + +public: + int m_GameType = NEO_GAME_TYPE_TDM; +}; diff --git a/mp/src/game/server/neo/neo_ghost_spawn_point.h b/mp/src/game/server/neo/neo_ghost_spawn_point.h index 584eeb563..cb936475a 100644 --- a/mp/src/game/server/neo/neo_ghost_spawn_point.h +++ b/mp/src/game/server/neo/neo_ghost_spawn_point.h @@ -9,4 +9,4 @@ class CNEOGhostSpawnPoint : public CPointEntity DECLARE_CLASS(CNEOGhostSpawnPoint, CPointEntity); }; -#endif // NEO_GHOST_SPAWN_POINT_H \ No newline at end of file +#endif // NEO_GHOST_SPAWN_POINT_H diff --git a/mp/src/game/shared/neo/neo_gamerules.cpp b/mp/src/game/shared/neo/neo_gamerules.cpp index 353f4227e..ff8d8e127 100644 --- a/mp/src/game/shared/neo/neo_gamerules.cpp +++ b/mp/src/game/shared/neo/neo_gamerules.cpp @@ -26,6 +26,7 @@ #include "inetchannelinfo.h" #include "neo_dm_spawn.h" #include "neo_misc.h" + #include "neo_game_config.h" extern ConVar weaponstay; #endif @@ -49,14 +50,33 @@ ConVar neo_sv_clantag_allow("neo_sv_clantag_allow", "1", FCVAR_REPLICATED, "", t ConVar neo_sv_dev_test_clantag("neo_sv_dev_test_clantag", "", FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Debug-mode only - Override all clantags with this value."); #endif -ConVar neo_vote_game_mode("neo_vote_game_mode", "1", FCVAR_USERINFO, "Vote on game mode to play. TDM=0, CTG=1, VIP=2, DM=3", true, 0, true, NEO_GAME_TYPE__TOTAL - 1); +#define STR_GAMEOPTS "TDM=0, CTG=1, VIP=2, DM=3" +#define STR_GAMEBWOPTS "TDM=1, CTG=2, VIP=4, DM=8" +ConVar neo_vote_game_mode("neo_vote_game_mode", "1", FCVAR_USERINFO, "Vote on game mode to play. " STR_GAMEOPTS, true, 0, true, NEO_GAME_TYPE__TOTAL - 1); ConVar neo_vip_eligible("neo_cl_vip_eligible", "1", FCVAR_ARCHIVE, "Eligible for VIP", true, 0, true, 1); #ifdef GAME_DLL ConVar sv_neo_vip_ctg_on_death("sv_neo_vip_ctg_on_death", "0", FCVAR_ARCHIVE, "Spawn Ghost when VIP dies, continue the game", true, 0, true, 1); #endif #ifdef GAME_DLL -ConVar sv_neo_change_game_type_mid_round("sv_neo_change_game_type_mid_round", "1", FCVAR_REPLICATED, "Allow game type change mid-match"); +// NEO TODO (nullsystem): Change how voting done from convar to menu selection +enum eGamemodeEnforcement +{ + GAMEMODE_ENFORCEMENT_MAP = 0, // Only use the gamemode enforced by the map + GAMEMODE_ENFORCEMENT_SINGLE, // Only use the single gamemode enforced by the server + GAMEMODE_ENFORCEMENT_RAND, // Randomly choose a gamemode on each map initialization based on a list + GAMEMODE_ENFORCEMENT_VOTE, // Allow vote by players on pre-match + + GAMEMODE_ENFORCEMENT__TOTAL, +}; +ConVar neo_sv_gamemode_enforcement("neo_sv_gamemode_enforcement", "0", FCVAR_REPLICATED, + "How the gamemode are determined. 0 = By map, 1 = By neo_sv_gamemode_single, 2 = Random, 3 = Pre-match voting", + true, 0.0f, true, GAMEMODE_ENFORCEMENT__TOTAL - 1); +ConVar neo_sv_gamemode_single("neo_sv_gamemode_single", "3", FCVAR_REPLICATED, "The gamemode that is enforced by the server. " STR_GAMEOPTS, + true, 0.0f, true, NEO_GAME_TYPE__TOTAL - 1); +ConVar neo_sv_gamemode_random_allow("neo_sv_gamemode_random_allow", "11", FCVAR_REPLICATED, + "In bitwise, the gamemodes that are allowed for random selection. Default = TDM+CTG+DM. " STR_GAMEBWOPTS, + true, 1.0f, true, (1 << NEO_GAME_TYPE__TOTAL)); // Can't be zero, minimum has to set to a bitwise value #endif #ifdef GAME_DLL @@ -244,6 +264,7 @@ extern CBaseEntity *g_pLastJinraiSpawn, *g_pLastNSFSpawn; static const char *s_NeoPreserveEnts[] = { "neo_gamerules", + "neo_game_config", "info_player_attacker", "info_player_defender", "info_player_start", @@ -366,10 +387,7 @@ CNEORules::CNEORules() } } - if (GetGameType() == NEO_GAME_TYPE_CTG || GetGameType() == NEO_GAME_TYPE_VIP) - { - ResetGhostCapPoints(); - } + m_nGameTypeSelected = NEO_GAME_TYPE_CTG; #endif ResetMapSessionCommon(); @@ -641,6 +659,97 @@ void CNEORules::GetDMHighestScorers( } } +#ifdef GAME_DLL +void CNEORules::CheckGameType() +{ + // Static as CNEORules doesn't persists through map changes + static int iStaticInitOnCmd = -1; + static int iStaticInitOnRandAllow = -1; + static bool staticGamemodesCanPick[NEO_GAME_TYPE__TOTAL] = {}; + static int iStaticLastPick = -1; // Mostly so it doesn't repeat on array refresh + + const int iGamemodeEnforce = neo_sv_gamemode_enforcement.GetInt(); + const int iGamemodeRandAllow = neo_sv_gamemode_random_allow.GetInt(); + // Update on what to select on first map load or server operator changes neo_sv_gamemode_enforcement + const bool bCheckOnGameType = (!m_bGamemodeTypeBeenInitialized || iGamemodeEnforce != iStaticInitOnCmd || + iGamemodeRandAllow != iStaticInitOnRandAllow); + if (!bCheckOnGameType) + { + return; + } + + // NEO NOTE (nullsystem): CNEORules always recreated on map change, yet entities properly found + // happens later. So checking and init on game type will execute here once. + switch (iGamemodeEnforce) + { + case GAMEMODE_ENFORCEMENT_SINGLE: + { + m_nGameTypeSelected = neo_sv_gamemode_single.GetInt(); + } break; + case GAMEMODE_ENFORCEMENT_RAND: + { + const int iBWAllow = neo_sv_gamemode_random_allow.GetInt(); // Min of 1, cannot be zero + Assert(iBWAllow > 0); + + // Check if all are used up + { + int iAllowsPicks = 0; + for (int i = 0; i < NEO_GAME_TYPE__TOTAL; ++i) + { + iAllowsPicks += staticGamemodesCanPick[i]; + } + if (iAllowsPicks == 0 || iGamemodeRandAllow != iStaticInitOnRandAllow) + { +#ifdef DEBUG + DevMsg("Array reset!\n"); +#endif + // Preset true to those not-allowed, preset false to those allowed + int iTotalPicks = 0; + for (int i = 0; i < NEO_GAME_TYPE__TOTAL; ++i) + { + const bool bCanPick = (iBWAllow & (1 << i)); + iTotalPicks += bCanPick; + staticGamemodesCanPick[i] = bCanPick; + } + if (iTotalPicks <= 1) + { + iStaticLastPick = -1; + } + } + } + + m_nGameTypeSelected = RandomInt(0, NEO_GAME_TYPE__TOTAL - 1); + for (int iWalk = 0; + (!staticGamemodesCanPick[m_nGameTypeSelected] || m_nGameTypeSelected == iStaticLastPick) && + iWalk < NEO_GAME_TYPE__TOTAL; + ++iWalk) + { + m_nGameTypeSelected = LoopAroundInArray(m_nGameTypeSelected + 1, NEO_GAME_TYPE__TOTAL); + } + +#ifdef DEBUG + for (int i = 0; i < NEO_GAME_TYPE__TOTAL; ++i) + { + DevMsg("%d | %s: %s\n", i, NEO_GAME_TYPE_DESC_STRS[i].szStr, staticGamemodesCanPick[i] ? "Allowed" : "Not allowed"); + } + DevMsg("Pick: %d | Prev: %d\n", m_nGameTypeSelected.Get(), iStaticLastPick); +#endif + + staticGamemodesCanPick[m_nGameTypeSelected] = false; + iStaticLastPick = m_nGameTypeSelected; + } break; + default: + { + const auto pEntGameCfg = static_cast(gEntList.FindEntityByClassname(nullptr, "neo_game_config")); + m_nGameTypeSelected = (pEntGameCfg) ? pEntGameCfg->m_GameType : NEO_GAME_TYPE_CTG; + } break; + } + m_bGamemodeTypeBeenInitialized = true; + iStaticInitOnCmd = iGamemodeEnforce; + iStaticInitOnRandAllow = iGamemodeRandAllow; +} +#endif + void CNEORules::Think(void) { #ifdef GAME_DLL @@ -648,6 +757,7 @@ void CNEORules::Think(void) bool bIsPause = m_nRoundStatus == NeoRoundStatus::Pause; if (bIsIdleState && gpGlobals->curtime > m_flNeoNextRoundStartTime) { + CheckGameType(); StartNextRound(); return; } @@ -1881,6 +1991,11 @@ void CNEORules::StartNextRound() CleanUpMap(); + if (neo_sv_gamemode_enforcement.GetInt() == GAMEMODE_ENFORCEMENT_VOTE && m_nRoundStatus == NeoRoundStatus::Warmup) + { + GatherGameTypeVotes(); + } + // NEO TODO (nullsystem): There should be a more sophisticated logic to be able to restore XP // for when moving from idle to preroundfreeze, or in the future, competitive with whatever // extra stuff in there. But to keep it simple: just clear if it was a warmup. @@ -1888,11 +2003,6 @@ void CNEORules::StartNextRound() SetRoundStatus(NeoRoundStatus::PreRoundFreeze); ++m_iRoundNumber; - if (!GetGameType() || sv_neo_change_game_type_mid_round.GetBool()) - { - GatherGameTypeVotes(); - } - for (int i = 1; i <= gpGlobals->maxClients; i++) { CNEO_Player *pPlayer = (CNEO_Player*)UTIL_PlayerByIndex(i); @@ -1957,11 +2067,6 @@ void CNEORules::StartNextRound() FireLegacyEvent_NeoRoundEnd(); - if (!GetGameType() || sv_neo_change_game_type_mid_round.GetBool()) - { - GatherGameTypeVotes(); - } - char RoundMsg[27]; static_assert(sizeof(RoundMsg) == sizeof("- CTG ROUND 99 STARTED -\n\0"), "RoundMsg requires to fit round numbers up to 2 digits"); V_sprintf_safe(RoundMsg, "- %s ROUND %d STARTED -\n", GetGameTypeName(), Min(99, m_iRoundNumber.Get())); @@ -2032,23 +2137,16 @@ void CNEORules::CreateStandardEntities(void) #endif } +const SZWSZTexts NEO_GAME_TYPE_DESC_STRS[NEO_GAME_TYPE__TOTAL] = { + SZWSZ_INIT("Team Deathmatch"), + SZWSZ_INIT("Capture the Ghost"), + SZWSZ_INIT("Extract or Kill the VIP"), + SZWSZ_INIT("Deathmatch"), +}; + const char *CNEORules::GetGameDescription(void) { - //DevMsg("Querying CNEORules game description\n"); - - // NEO TODO (Rain): get a neo_game_config so we can specify better - switch(GetGameType()) { - case NEO_GAME_TYPE_TDM: - return "Team Deathmatch"; - case NEO_GAME_TYPE_CTG: - return "Capture the Ghost"; - case NEO_GAME_TYPE_VIP: - return "Extract or Kill the VIP"; - case NEO_GAME_TYPE_DM: - return "Deathmatch"; - default: - return BaseClass::GetGameDescription(); - } + return NEO_GAME_TYPE_DESC_STRS[GetGameType()].szStr; } const CViewVectors *CNEORules::GetViewVectors() const diff --git a/mp/src/game/shared/neo/neo_gamerules.h b/mp/src/game/shared/neo/neo_gamerules.h index c1d0d2789..21ae48054 100644 --- a/mp/src/game/shared/neo/neo_gamerules.h +++ b/mp/src/game/shared/neo/neo_gamerules.h @@ -12,6 +12,7 @@ #include "GameEventListener.h" #include "neo_player_shared.h" +#include "neo_misc.h" #ifdef CLIENT_DLL #include "c_neo_player.h" @@ -107,6 +108,8 @@ enum NeoGameType { NEO_GAME_TYPE__TOTAL // Number of game types }; +extern const SZWSZTexts NEO_GAME_TYPE_DESC_STRS[NEO_GAME_TYPE__TOTAL]; + enum NeoRoundStatus { Idle = 0, Warmup, @@ -257,6 +260,7 @@ class CNEORules : public CHL2MPRules, public CGameEventListener bool m_bIgnoreOverThreshold = false; bool ReadyUpPlayerIsReady(CNEO_Player *pNeoPlayer) const; + void CheckGameType(); void StartNextRound(); virtual const char* GetChatFormat(bool bTeamOnly, CBasePlayer* pPlayer) OVERRIDE; @@ -376,6 +380,7 @@ class CNEORules : public CHL2MPRules, public CGameEventListener int m_arrayiEntPrevCap[MAX_PLAYERS + 1]; // This is to check for cap-prevention workaround attempts int m_iEntPrevCapSize = 0; int m_iPrintHelpCounter = 0; + bool m_bGamemodeTypeBeenInitialized = false; #endif CNetworkVar(int, m_nRoundStatus); CNetworkVar(int, m_nGameTypeSelected); diff --git a/mp/src/game/shared/neo/neo_ghost_cap_point.h b/mp/src/game/shared/neo/neo_ghost_cap_point.h index 4dadd0870..329e39fc0 100644 --- a/mp/src/game/shared/neo/neo_ghost_cap_point.h +++ b/mp/src/game/shared/neo/neo_ghost_cap_point.h @@ -86,7 +86,6 @@ class CNEOGhostCapturePoint : public CBaseEntity CNetworkVar(bool, m_bGhostHasBeenCaptured); CNetworkVar(bool, m_bIsActive); - int m_iGameType; #else int m_iOwningTeam; int m_iSuccessfulCaptorClientIndex; diff --git a/mp/src/game/shared/neo/neo_misc.h b/mp/src/game/shared/neo/neo_misc.h index 9a8038904..66dba6366 100644 --- a/mp/src/game/shared/neo/neo_misc.h +++ b/mp/src/game/shared/neo/neo_misc.h @@ -12,3 +12,10 @@ [[nodiscard]] bool InRect(const vgui::IntRect &rect, const int x, const int y); [[nodiscard]] int LoopAroundMinMax(const int iValue, const int iMin, const int iMax); [[nodiscard]] int LoopAroundInArray(const int iValue, const int iSize); + +struct SZWSZTexts +{ + const char *szStr; + const wchar_t *wszStr; +}; +#define SZWSZ_INIT(STR) {.szStr = STR, .wszStr = L"" STR}