Skip to content

Commit

Permalink
Add start of game ready up mechanic
Browse files Browse the repository at this point in the history
* A (not enabled by default) ready up mechanic that will only start a
  game if the players ready match the defined threshold (default to
  5v5)
* Allows to override the limit if desired, although for either scenarios
  will still require equal amount of players between Jinrai and NSF
* New native player commands to set/unset ready state, configure
  override limit, and check players not ready
* The override limit will reset to false when players drops below threshold
* Automatic intermission can be turned off with the ready-up feature on
* HUD round state fixed to just directly take the const wchar_t strings
  and therefore no need for ANSI->WCHAR conversions
* Make sure round wins and XPs are resetting on non-intermission to ready
* Player commands
    * !ready - For the player to signal they're ready to play
    * !unready - For the player to signal they're not ready to play
    * !overridelimit - A player to signal in behalf of everyone that
      they'll be playing over the stated limit
    * !playersnotready - Prints the players that are not ready and give
      info on why it's not starting
* ConVars
    * neo_sv_readyup_lobby - 0 by default, toggles the ready-up feature
    * neo_sv_readyup_teamplayersthres - 5 by default, the exact amount
      of players to ready-up and participate to start a game
    * neo_sv_readyup_skipwarmup - 1 by default, if ready-up feature is
      on and this is enabled, skip warmup state
    * neo_sv_readyup_autointermission - 0 by default, if disabled will
      not automatically enter intermission when the match ends
* fixes NeotokyoRebuild#218
  • Loading branch information
nullsystem committed Oct 6, 2024
1 parent 7f8390b commit 113d259
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 22 deletions.
20 changes: 10 additions & 10 deletions mp/src/game/client/neo/ui/neo_hud_round_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ CNEOHud_RoundState::CNEOHud_RoundState(const char *pElementName, vgui::Panel *pa
, Panel(parent, pElementName)
, m_pImageList(new vgui::ImageList(true))
{
m_pWszStatusUnicode = L"";
SetAutoDelete(true);

if (parent)
Expand Down Expand Up @@ -196,29 +197,28 @@ void CNEOHud_RoundState::ApplySchemeSettings(vgui::IScheme* pScheme)
SetBounds(0, Y_POS, res.w, res.h);
}

extern ConVar neo_sv_readyup_lobby;

void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw()
{
float roundTimeLeft = NEORules()->GetRoundRemainingTime();
const NeoRoundStatus roundStatus = NEORules()->GetRoundStatus();
const bool inSuddenDeath = NEORules()->RoundIsInSuddenDeath();
const bool inMatchPoint = NEORules()->RoundIsMatchPoint();

const char *prefixStr = (roundStatus == NeoRoundStatus::Warmup) ? "Warmup" : "";
m_pWszStatusUnicode = (roundStatus == NeoRoundStatus::Warmup) ? L"Warmup" : L"";
if (roundStatus == NeoRoundStatus::Idle) {
prefixStr = "Waiting for players";
m_pWszStatusUnicode = neo_sv_readyup_lobby.GetBool() ? L"Waiting for players to ready up" : L"Waiting for players";
}
else if (inSuddenDeath)
{
prefixStr = "Sudden death";
m_pWszStatusUnicode = L"Sudden death";
}
else if (inMatchPoint)
{
prefixStr = "Match point";
m_pWszStatusUnicode = L"Match point";
}
char szStatusANSI[24] = {};
V_sprintf_safe(szStatusANSI, "%s", prefixStr);
memset(m_wszStatusUnicode, 0, sizeof(m_wszStatusUnicode)); // NOTE (nullsystem): Clear it or get junk after warmup ends
g_pVGuiLocalize->ConvertANSIToUnicode(szStatusANSI, m_wszStatusUnicode, sizeof(m_wszStatusUnicode));
m_iStatusUnicodeSize = V_wcslen(m_pWszStatusUnicode);

// Update steam images
if (gpGlobals->curtime > m_iNextAvatarUpdate) {
Expand Down Expand Up @@ -304,9 +304,9 @@ void CNEOHud_RoundState::DrawNeoHudElement()
surface()->DrawPrintText(m_wszRoundUnicode, 9);

// Draw round status
surface()->GetTextSize(m_hOCRSmallFont, m_wszStatusUnicode, fontWidth, fontHeight);
surface()->GetTextSize(m_hOCRSmallFont, m_pWszStatusUnicode, fontWidth, fontHeight);
surface()->DrawSetTextPos(m_iXpos - (fontWidth / 2), m_iBoxYEnd);
surface()->DrawPrintText(m_wszStatusUnicode, 24);
surface()->DrawPrintText(m_pWszStatusUnicode, m_iStatusUnicodeSize);

const int localPlayerTeam = GetLocalPlayerTeam();
const int localPlayerIndex = GetLocalPlayerIndex();
Expand Down
3 changes: 2 additions & 1 deletion mp/src/game/client/neo/ui/neo_hud_round_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ
wchar_t m_wszLeftTeamScore[3] = {};
wchar_t m_wszRightTeamScore[3] = {};
wchar_t m_wszPlayersAliveUnicode[9] = {};
wchar_t m_wszStatusUnicode[24] = {};
const wchar_t *m_pWszStatusUnicode = nullptr;
int m_iStatusUnicodeSize = 0;

// Totals info
int m_iLeftPlayersAlive = 0;
Expand Down
5 changes: 5 additions & 0 deletions mp/src/game/server/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

#ifdef NEO
#include "neo_player.h"
#include "neo_gamerules.h"
#endif

// memdbgon must be the last include file in a .cpp file!!!
Expand Down Expand Up @@ -276,6 +277,10 @@ void Host_Say( edict_t *pEdict, const CCommand &args, bool teamonly )

Q_strncat( text, p, sizeof( text ), COPY_ALL_CHARACTERS );
Q_strncat( text, "\n", sizeof( text ), COPY_ALL_CHARACTERS );

#ifdef NEO
static_cast<CNEORules *>(g_pGameRules)->CheckChatCommand(static_cast<CNEO_Player *>(pPlayer), p);
#endif

// loop through all players
// Start with the first player.
Expand Down
227 changes: 216 additions & 11 deletions mp/src/game/shared/neo/neo_gamerules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ConVar cl_onlysteamnick("cl_onlysteamnick", "0", FCVAR_USERINFO | FCVAR_ARCHIVE,
static constexpr char INTEGRITY_CHECK_DBG[] = "1";
#else
static constexpr char INTEGRITY_CHECK_DBG[] = "0";
#endif
#endif // DEBUG
ConVar neo_sv_build_integrity_check("neo_sv_build_integrity_check", "1", FCVAR_GAMEDLL | FCVAR_REPLICATED,
"If enabled, the server checks the build's Git hash between the client and"
" the server. If it doesn't match, the server rejects and disconnects the client.",
Expand All @@ -60,7 +60,7 @@ ConVar neo_sv_build_integrity_check_allow_debug("neo_sv_build_integrity_check_al
static constexpr char TEAMDMG_MULTI[] = "0";
#else
static constexpr char TEAMDMG_MULTI[] = "2";
#endif
#endif // DEBUG
ConVar neo_sv_mirror_teamdamage_multiplier("neo_sv_mirror_teamdamage_multiplier", TEAMDMG_MULTI, FCVAR_REPLICATED, "The damage multiplier given to the friendly-firing individual. Set value to 0 to disable mirror team damage.", true, 0.0f, true, 100.0f);
ConVar neo_sv_mirror_teamdamage_duration("neo_sv_mirror_teamdamage_duration", "7", FCVAR_REPLICATED, "How long in seconds the mirror damage is active for the start of each round. Set to 0 for the entire round.", true, 0.0f, true, 10000.0f);
ConVar neo_sv_mirror_teamdamage_immunity("neo_sv_mirror_teamdamage_immunity", "1", FCVAR_REPLICATED, "If enabled, the victim will not take damage from a teammate during the mirror team damage duration.", true, 0.0f, true, 1.0f);
Expand All @@ -73,7 +73,14 @@ ConVar neo_sv_suicide_prevent_cap_punish("neo_sv_suicide_prevent_cap_punish", "1
"while the other team is holding the ghost, reward the ghost holder team "
"a rank up.",
true, 0.0f, true, 1.0f);
#endif

ConVar neo_sv_readyup_teamplayersthres("neo_sv_readyup_teamplayersthres", "5", FCVAR_REPLICATED, "The exact total players per team to be in and ready up to start a game.", true, 0.0f, true, (MAX_PLAYERS - 1) / 2);
ConVar neo_sv_readyup_skipwarmup("neo_sv_readyup_skipwarmup", "1", FCVAR_REPLICATED, "Skip the warmup round when already using ready up.", true, 0.0f, true, 1.0f);
ConVar neo_sv_readyup_autointermission("neo_sv_readyup_autointermission", "0", FCVAR_REPLICATED, "If disabled, skips the automatic intermission at the end of the match.", true, 0.0f, true, 1.0f);
#endif // GAME_DLL

// Both CLIENT_DLL + GAME_DLL, but server-side setting so it's replicated onto client to read the values
ConVar neo_sv_readyup_lobby("neo_sv_readyup_lobby", "0", FCVAR_REPLICATED, "If enabled, players would need to ready up and match the players total requirements to start a game.", true, 0.0f, true, 1.0f);

REGISTER_GAMERULES_CLASS( CNEORules );

Expand Down Expand Up @@ -470,6 +477,8 @@ void CNEORules::ResetMapSessionCommon()
m_flNeoNextRoundStartTime = 0.0f;
#ifdef GAME_DLL
m_pRestoredInfos.Purge();
m_readyAccIDs.Purge();
m_bIgnoreOverThreshold = false;

for (int i = 1; i <= gpGlobals->maxClients; i++)
{
Expand All @@ -494,7 +503,14 @@ void CNEORules::ResetMapSessionCommon()
void CNEORules::ChangeLevel(void)
{
ResetMapSessionCommon();
BaseClass::ChangeLevel();
if (neo_sv_readyup_lobby.GetBool() && !neo_sv_readyup_autointermission.GetBool())
{
m_bChangelevelDone = false;
}
else
{
BaseClass::ChangeLevel();
}
}

#endif
Expand Down Expand Up @@ -560,8 +576,8 @@ void CNEORules::Think(void)
{
if (!m_bChangelevelDone)
{
ChangeLevel(); // intermission is over
m_bChangelevelDone = true;
ChangeLevel(); // intermission is over
}
}

Expand Down Expand Up @@ -960,11 +976,184 @@ void CNEORules::SpawnTheGhost()
m_pGhost->GetAbsOrigin().z);
}

void CNEORules::StartNextRound()
bool CNEORules::ReadyUpPlayerIsReady(CNEO_Player *pNeoPlayer) const
{
if (!pNeoPlayer) return false;

const CSteamID steamID = GetSteamIDForPlayerIndex(pNeoPlayer->entindex());
return pNeoPlayer->IsBot() || (steamID.IsValid() && m_readyAccIDs.HasElement(steamID.GetAccountID()));
}

void CNEORules::CheckChatCommand(CNEO_Player *pNeoCmdPlayer, const char *pSzChat)
{
if (GetGlobalTeam(TEAM_JINRAI)->GetNumPlayers() == 0 || GetGlobalTeam(TEAM_NSF)->GetNumPlayers() == 0)
const bool bHasCmds = neo_sv_readyup_lobby.GetBool();
if (!bHasCmds || !pNeoCmdPlayer || !pSzChat || pSzChat[0] != '!') return;
++pSzChat;

if (V_strcmp(pSzChat, "help") == 0)
{
UTIL_CenterPrintAll("Waiting for players on both teams.\n"); // NEO TODO (Rain): actual message
char szHelpText[512];
V_sprintf_safe(szHelpText, "Available commands:\n%s",
neo_sv_readyup_lobby.GetBool() ?
"Ready up commands:\n"
"!ready - Ready up yourself\n"
"!unready - Unready yourself\n"
"!overridelimit - Override players amount restriction\n"
"!playersnotready - List players that are not ready\n"
: "");
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, szHelpText);
}

if (neo_sv_readyup_lobby.GetBool())
{
const CSteamID steamID = GetSteamIDForPlayerIndex(pNeoCmdPlayer->entindex());
if (steamID.IsValid())
{
const int iThres = neo_sv_readyup_teamplayersthres.GetInt();
if (V_strcmp(pSzChat, "ready") == 0)
{
m_readyAccIDs.Insert(steamID.GetAccountID());
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "You are now marked as ready.");
const auto readyPlayers = FetchReadyPlayers();
if (readyPlayers.array[TEAM_JINRAI] == iThres && readyPlayers.array[TEAM_NSF] == iThres)
{
UTIL_ClientPrintAll(HUD_PRINTTALK, "All players are ready! Starting soon...");
}
}
else if (V_strcmp(pSzChat, "unready") == 0)
{
m_readyAccIDs.Remove(steamID.GetAccountID());
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "You are now marked as unready.");
}
else if (V_strcmp(pSzChat, "overridelimit") == 0)
{
const auto readyPlayers = FetchReadyPlayers();
if (readyPlayers.array[TEAM_JINRAI] > iThres && readyPlayers.array[TEAM_NSF] > iThres)
{
m_bIgnoreOverThreshold = true;
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "Overriding threshold, allowing more players.");
}
else
{
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "You must go pass the threshold in order to set override.");
}
}
else if (V_strcmp(pSzChat, "playersnotready") == 0)
{
bool bHasUnreadyPlayers = false;
char szPrintText[((MAX_PLAYER_NAME_LENGTH + 1) * MAX_PLAYERS) + 32];
szPrintText[0] = '\0';
ReadyPlayers readyPlayers = {};
for (int i = 1; i <= gpGlobals->maxClients; i++)
{
if (auto *pNeoOtherPlayer = static_cast<CNEO_Player *>(UTIL_PlayerByIndex(i)))
{
const bool bPlayerReady = ReadyUpPlayerIsReady(pNeoOtherPlayer);
readyPlayers.array[pNeoOtherPlayer->GetTeamNumber()] += bPlayerReady;
if (!bPlayerReady)
{
if (!bHasUnreadyPlayers)
{
V_strcat_safe(szPrintText, "Players not ready:\n");
bHasUnreadyPlayers = true;
}
V_strcat_safe(szPrintText, pNeoOtherPlayer->GetNeoPlayerName(pNeoCmdPlayer));
V_strcat_safe(szPrintText, "\n");
}
}
}
if (bHasUnreadyPlayers)
{
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, szPrintText);
}
else
{
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "All players are ready.");
if (readyPlayers.array[TEAM_JINRAI] < iThres || readyPlayers.array[TEAM_NSF] < iThres)
{
const int iNeedJin = max(0, iThres - readyPlayers.array[TEAM_JINRAI]);
const int iNeedNSF = max(0, iThres - readyPlayers.array[TEAM_NSF]);
char szPrintNeed[100];
V_sprintf_safe(szPrintNeed, "Jinrai need %d players and NSF need %d players "
"to ready up to start.", iNeedJin, iNeedNSF);
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, szPrintNeed);
}
else if (readyPlayers.array[TEAM_JINRAI] > iThres || readyPlayers.array[TEAM_NSF] > iThres)
{
const int iExtraJin = max(0, readyPlayers.array[TEAM_JINRAI] - iThres);
const int iExtraNSF = max(0, readyPlayers.array[TEAM_NSF] - iThres);
char szPrintNeed[100];
V_sprintf_safe(szPrintNeed, "Jinrai have %d extra players and NSF have %d extra players "
"over the %d per team threshold.", iExtraJin, iExtraNSF, iThres);
ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, szPrintNeed);
}
}
}
}
}
}

CNEORules::ReadyPlayers CNEORules::FetchReadyPlayers() const
{
ReadyPlayers readyPlayers = {};
if (!neo_sv_readyup_lobby.GetBool())
{
return readyPlayers;
}

for (int i = 1; i <= gpGlobals->maxClients; i++)
{
if (auto *pNeoPlayer = static_cast<CNEO_Player *>(UTIL_PlayerByIndex(i)))
{
readyPlayers.array[pNeoPlayer->GetTeamNumber()] += ReadyUpPlayerIsReady(pNeoPlayer);
}
}

return readyPlayers;
}

void CNEORules::StartNextRound()
{
const bool bLobby = neo_sv_readyup_lobby.GetBool();
const int iThres = neo_sv_readyup_teamplayersthres.GetInt();
const bool bEqualThres = (iThres == GetGlobalTeam(TEAM_JINRAI)->GetNumPlayers()) && (iThres == GetGlobalTeam(TEAM_NSF)->GetNumPlayers());
const auto readyPlayers = FetchReadyPlayers();
// Do not start if: Non-ready-up mode, no players in either teams
if ((!bLobby && (GetGlobalTeam(TEAM_JINRAI)->GetNumPlayers() == 0 || GetGlobalTeam(TEAM_NSF)->GetNumPlayers() == 0))
// If ready-up mode and doesn't exactly match the threshold on ready-up or players
|| (bLobby && !m_bIgnoreOverThreshold && (!bEqualThres || (readyPlayers.array[TEAM_JINRAI] != iThres || readyPlayers.array[TEAM_NSF] != iThres)))
// If ready-up mode, allows over threshold and is lower than threshold or not equal teams
|| (bLobby && m_bIgnoreOverThreshold &&
((readyPlayers.array[TEAM_JINRAI] < iThres || readyPlayers.array[TEAM_NSF] < iThres)
|| GetGlobalTeam(TEAM_JINRAI)->GetNumPlayers() != GetGlobalTeam(TEAM_NSF)->GetNumPlayers()))
)
{
if (bLobby)
{
if (!m_bIgnoreOverThreshold && (readyPlayers.array[TEAM_JINRAI] > iThres || readyPlayers.array[TEAM_NSF] > iThres))
{
char szPrint[128];
V_sprintf_safe(szPrint, "More players than %dv%d! Type !overridelimit to allow more players to start!",
iThres, iThres);
UTIL_ClientPrintAll(HUD_PRINTTALK, szPrint);
}

// Untoggle the overrider if there's suddenly less players than threshold
if (m_bIgnoreOverThreshold && (readyPlayers.array[TEAM_JINRAI] < iThres || readyPlayers.array[TEAM_NSF] < iThres))
{
m_bIgnoreOverThreshold = false;
}

char szPrint[512];
V_sprintf_safe(szPrint, "Waiting for %dv%d: %d Jinrai, %d NSF players ready\n",
iThres, iThres,
readyPlayers.array[TEAM_JINRAI], readyPlayers.array[TEAM_NSF]);
UTIL_CenterPrintAll(szPrint);
}
else
{
UTIL_CenterPrintAll("Waiting for players on both teams.\n"); // NEO TODO (Rain): actual message
}
SetRoundStatus(NeoRoundStatus::Idle);
m_flNeoNextRoundStartTime = gpGlobals->curtime + 10.0f;
return;
Expand Down Expand Up @@ -994,11 +1183,12 @@ void CNEORules::StartNextRound()
}
}

if (!loopbackSkipWarmup)
SetRoundStatus(NeoRoundStatus::Warmup);
m_iRoundNumber = 0;
if (!loopbackSkipWarmup && !(bLobby && neo_sv_readyup_skipwarmup.GetBool()))
{
// Moving from 0 players from either team to playable at team state
UTIL_CenterPrintAll("Warmup countdown started.\n");
SetRoundStatus(NeoRoundStatus::Warmup);
m_flNeoRoundStartTime = gpGlobals->curtime;
m_flNeoNextRoundStartTime = gpGlobals->curtime + mp_neo_warmup_round_time.GetFloat();
return;
Expand Down Expand Up @@ -1071,6 +1261,14 @@ void CNEORules::StartNextRound()
if (clearXP)
{
m_pRestoredInfos.Purge();

CTeam *pJinrai = GetGlobalTeam(TEAM_JINRAI);
CTeam *pNSF = GetGlobalTeam(TEAM_NSF);
Assert(pJinrai && pNSF);
pJinrai->SetScore(0);
pJinrai->SetRoundsWon(0);
pNSF->SetScore(0);
pNSF->SetRoundsWon(0);
}

IGameEvent *event = gameeventmanager->CreateEvent("round_start");
Expand Down Expand Up @@ -1834,7 +2032,14 @@ void CNEORules::SetWinningTeam(int team, int iWinReason, bool bForceMapReset, bo

if (gotMatchWinner)
{
GoToIntermission();
if (neo_sv_readyup_lobby.GetBool() && !neo_sv_readyup_autointermission.GetBool())
{
ResetMapSessionCommon();
}
else
{
GoToIntermission();
}
}
}
#endif
Expand Down
Loading

0 comments on commit 113d259

Please sign in to comment.