From e397fbe215c23b8bce2238e1678ada2a036c1bea Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:12:05 +0100 Subject: [PATCH] Initial deathmatch spawn implementation * Use info_player_deathmatch entity if possible * Otherwise can just use the separate file * also add some info command to tell if the map can dm or not * fixes #691 --- mp/src/game/client/CMakeLists.txt | 2 + .../client/neo/ui/neo_hud_friendly_marker.cpp | 29 +- .../game/client/neo/ui/neo_hud_game_event.cpp | 47 ++-- .../client/neo/ui/neo_hud_ghost_cap_point.cpp | 2 +- .../client/neo/ui/neo_hud_round_state.cpp | 213 ++++++++++----- .../game/client/neo/ui/neo_hud_round_state.h | 5 +- mp/src/game/client/neo/ui/neo_scoreboard.cpp | 53 +++- mp/src/game/client/neo/ui/neo_ui.cpp | 28 +- mp/src/game/server/CMakeLists.txt | 4 + mp/src/game/server/neo/neo_dm_spawn.cpp | 251 ++++++++++++++++++ mp/src/game/server/neo/neo_dm_spawn.h | 20 ++ mp/src/game/server/neo/neo_player.cpp | 21 +- mp/src/game/shared/hl2mp/hl2mp_gamerules.h | 6 +- mp/src/game/shared/neo/neo_gamerules.cpp | 233 +++++++++++++++- mp/src/game/shared/neo/neo_gamerules.h | 23 +- mp/src/game/shared/neo/neo_misc.cpp | 24 ++ mp/src/game/shared/neo/neo_misc.h | 14 + mp/src/game/shared/neo/neo_player_shared.cpp | 40 +++ mp/src/game/shared/neo/neo_player_shared.h | 10 + 19 files changed, 880 insertions(+), 145 deletions(-) create mode 100644 mp/src/game/server/neo/neo_dm_spawn.cpp create mode 100644 mp/src/game/server/neo/neo_dm_spawn.h create mode 100644 mp/src/game/shared/neo/neo_misc.cpp create mode 100644 mp/src/game/shared/neo/neo_misc.h diff --git a/mp/src/game/client/CMakeLists.txt b/mp/src/game/client/CMakeLists.txt index b23feddc13..55bdffefe7 100644 --- a/mp/src/game/client/CMakeLists.txt +++ b/mp/src/game/client/CMakeLists.txt @@ -1576,6 +1576,8 @@ target_sources_grouped( ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version_info.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version_info.h + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_misc.cpp + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_misc.h ) target_sources_grouped( diff --git a/mp/src/game/client/neo/ui/neo_hud_friendly_marker.cpp b/mp/src/game/client/neo/ui/neo_hud_friendly_marker.cpp index e993af6354..419396d416 100644 --- a/mp/src/game/client/neo/ui/neo_hud_friendly_marker.cpp +++ b/mp/src/game/client/neo/ui/neo_hud_friendly_marker.cpp @@ -89,17 +89,32 @@ void CNEOHud_FriendlyMarker::DrawNeoHudElement() const auto *pTargetPlayer = (localPlayer->GetObserverMode() == OBS_MODE_IN_EYE) ? dynamic_cast(localPlayer->GetObserverTarget()) : nullptr; - if (m_IsSpectator) + if (NEORules()->IsTeamplay()) { - auto nsf = GetGlobalTeam(TEAM_NSF); - DrawPlayerForTeam(nsf, localPlayer, pTargetPlayer); - - auto jinrai = GetGlobalTeam(TEAM_JINRAI); - DrawPlayerForTeam(jinrai, localPlayer, pTargetPlayer); + if (m_IsSpectator) + { + auto nsf = GetGlobalTeam(TEAM_NSF); + DrawPlayerForTeam(nsf, localPlayer, pTargetPlayer); + + auto jinrai = GetGlobalTeam(TEAM_JINRAI); + DrawPlayerForTeam(jinrai, localPlayer, pTargetPlayer); + } + else + { + DrawPlayerForTeam(team, localPlayer, pTargetPlayer); + } } else { - DrawPlayerForTeam(team, localPlayer, pTargetPlayer); + if (m_IsSpectator) + { + // TODO: They're not really Jinrai/NSF? + auto nsf = GetGlobalTeam(TEAM_NSF); + DrawPlayerForTeam(nsf, localPlayer, pTargetPlayer); + + auto jinrai = GetGlobalTeam(TEAM_JINRAI); + DrawPlayerForTeam(jinrai, localPlayer, pTargetPlayer); + } } } diff --git a/mp/src/game/client/neo/ui/neo_hud_game_event.cpp b/mp/src/game/client/neo/ui/neo_hud_game_event.cpp index 544eddb209..b6a696fdd8 100644 --- a/mp/src/game/client/neo/ui/neo_hud_game_event.cpp +++ b/mp/src/game/client/neo/ui/neo_hud_game_event.cpp @@ -140,30 +140,33 @@ void CNEOHud_GameEvent::DrawNeoHudElement() surface()->DrawSetTextPos(textPosX, text_y_offset); surface()->DrawPrintText(messageWord, messageLength); // the address of the null character minus the address of the first character - if (Q_stristr(teamWon, "jinrai")) + if (NEORules()->IsTeamplay()) { - surface()->DrawSetTexture(jinraiWin); + if (Q_stristr(teamWon, "jinrai")) + { + surface()->DrawSetTexture(jinraiWin); + } + else if (Q_stristr(teamWon, "nsf")) + { + surface()->DrawSetTexture(nsfWin); + } + else + { + surface()->DrawSetTexture(tie); + } + + const vgui::IntRect texRect{ + .x0 = (m_iResX/2) - 256, + .y0 = image_y_offset, + .x1 = (m_iResX / 2) + 256, + .y1 = image_y_offset + 256, + }; + surface()->DrawSetColor(Color(0, 0, 0, alpha)); + surface()->DrawTexturedRect(texRect.x0 + 2, texRect.y0 + 2, texRect.x1 + 2, texRect.y1 + 2); + + surface()->DrawSetColor(Color(255, 255, 255, alpha)); + surface()->DrawTexturedRect(texRect.x0, texRect.y0, texRect.x1, texRect.y1); } - else if (Q_stristr(teamWon, "nsf")) - { - surface()->DrawSetTexture(nsfWin); - } - else - { - surface()->DrawSetTexture(tie); - } - - const vgui::IntRect texRect{ - .x0 = (m_iResX/2) - 256, - .y0 = image_y_offset, - .x1 = (m_iResX / 2) + 256, - .y1 = image_y_offset + 256, - }; - surface()->DrawSetColor(Color(0, 0, 0, alpha)); - surface()->DrawTexturedRect(texRect.x0 + 2, texRect.y0 + 2, texRect.x1 + 2, texRect.y1 + 2); - - surface()->DrawSetColor(Color(255, 255, 255, alpha)); - surface()->DrawTexturedRect(texRect.x0, texRect.y0, texRect.x1, texRect.y1); } void CNEOHud_GameEvent::Paint() diff --git a/mp/src/game/client/neo/ui/neo_hud_ghost_cap_point.cpp b/mp/src/game/client/neo/ui/neo_hud_ghost_cap_point.cpp index af635fd369..72be078853 100644 --- a/mp/src/game/client/neo/ui/neo_hud_ghost_cap_point.cpp +++ b/mp/src/game/client/neo/ui/neo_hud_ghost_cap_point.cpp @@ -70,7 +70,7 @@ void CNEOHud_GhostCapPoint::UpdateStateForNeoHudElementDraw() void CNEOHud_GhostCapPoint::DrawNeoHudElement() { - if (!ShouldDraw() || NEORules()->IsRoundOver()) + if (!ShouldDraw() || NEORules()->IsRoundOver() || !NEORules()->IsTeamplay()) { return; } diff --git a/mp/src/game/client/neo/ui/neo_hud_round_state.cpp b/mp/src/game/client/neo/ui/neo_hud_round_state.cpp index 097293ccf9..f5675849dc 100644 --- a/mp/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/mp/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -31,6 +31,7 @@ NEO_HUD_ELEMENT_DECLARE_FREQ_CVAR(RoundState, 0.1) ConVar neo_cl_squad_hud_original("neo_cl_squad_hud_original", "0", FCVAR_ARCHIVE, "Use the old squad HUD", true, 0.0, true, 1.0); ConVar neo_cl_squad_hud_star_scale("neo_cl_squad_hud_star_scale", "0", FCVAR_ARCHIVE, "Scaling to apply from 1080p, 0 disables scaling"); +extern ConVar neo_sv_dm_win_xp; namespace { constexpr int Y_POS = 2; @@ -261,9 +262,14 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() roundTimeLeft = 0; } - char szRoundANSI[9] = {}; - V_sprintf_safe(szRoundANSI, "ROUND %i", NEORules()->roundNumber()); - g_pVGuiLocalize->ConvertANSIToUnicode(szRoundANSI, m_wszRoundUnicode, sizeof(m_wszRoundUnicode)); + if (NEORules()->GetGameType() == NEO_GAME_TYPE_DM) + { + m_iWszRoundUCSize = V_swprintf_safe(m_wszRoundUnicode, L"DEATHMATCH"); + } + else + { + m_iWszRoundUCSize = V_swprintf_safe(m_wszRoundUnicode, L"ROUND %i", NEORules()->roundNumber()); + } if (roundStatus == NeoRoundStatus::PreRoundFreeze) roundTimeLeft = NEORules()->GetRemainingPreRoundFreezeTime(true); @@ -273,18 +279,40 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() const int minutes = (secsTotal - secsRemainder) / 60; V_snwprintf(m_wszTime, 6, L"%02d:%02d", minutes, secsRemainder); + int iDMHighestXP = 0; + const int localPlayerTeam = GetLocalPlayerTeam(); - if (localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF) { - V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(localPlayerTeam)->GetRoundsWon()); - V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(NEORules()->GetOpposingTeam(localPlayerTeam))->GetRoundsWon()); + if (NEORules()->IsTeamplay()) + { + if (localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF) { + V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(localPlayerTeam)->GetRoundsWon()); + V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(NEORules()->GetOpposingTeam(localPlayerTeam))->GetRoundsWon()); + } + else { + V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(TEAM_JINRAI)->GetRoundsWon()); + V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(TEAM_NSF)->GetRoundsWon()); + } } - else { - V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(TEAM_JINRAI)->GetRoundsWon()); - V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(TEAM_NSF)->GetRoundsWon()); + else + { + [[maybe_unused]] int iDMHighestTotal; + NEORules()->GetDMHighestScorers(&iDMHighestTotal, &iDMHighestXP); } - char szPlayersAliveANSI[9] = {}; - if (NEORules()->GetGameType() == NEO_GAME_TYPE_TDM) + char szPlayersAliveANSI[ARRAYSIZE(m_wszPlayersAliveUnicode)] = {}; + if (NEORules()->GetGameType() == NEO_GAME_TYPE_DM) + { + // NEO NOTE (nullsystem): Show highest player score + if (neo_sv_dm_win_xp.GetInt() > 0) + { + V_sprintf_safe(szPlayersAliveANSI, "Lead: %d/%d", iDMHighestXP, neo_sv_dm_win_xp.GetInt()); + } + else + { + V_sprintf_safe(szPlayersAliveANSI, "Lead: %d", iDMHighestXP); + } + } + else if (NEORules()->GetGameType() == NEO_GAME_TYPE_TDM) { if (localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF) { V_sprintf_safe(szPlayersAliveANSI, "%i:%i", GetGlobalTeam(localPlayerTeam)->Get_Score(), GetGlobalTeam(NEORules()->GetOpposingTeam(localPlayerTeam))->Get_Score()); @@ -301,6 +329,10 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() // Update Objective switch (NEORules()->GetGameType()) { + case NEO_GAME_TYPE_DM: + // Don't print objective for deathmatch + szGameTypeDescription[0] = '\0'; + break; case NEO_GAME_TYPE_TDM: V_sprintf_safe(szGameTypeDescription, "Score the most Points\n"); break; @@ -373,7 +405,7 @@ void CNEOHud_RoundState::DrawNeoHudElement() surface()->GetTextSize(m_hOCRSmallFont, m_wszRoundUnicode, fontWidth, fontHeight); surface()->DrawSetTextPos(m_iXpos - (fontWidth / 2), 0); surface()->DrawSetTextColor(COLOR_WHITE); - surface()->DrawPrintText(m_wszRoundUnicode, 9); + surface()->DrawPrintText(m_wszRoundUnicode, m_iWszRoundUCSize); // Draw round status surface()->GetTextSize(m_hOCRSmallFont, m_pWszStatusUnicode, fontWidth, fontHeight); @@ -382,9 +414,9 @@ void CNEOHud_RoundState::DrawNeoHudElement() const int localPlayerTeam = GetLocalPlayerTeam(); const int localPlayerIndex = GetLocalPlayerIndex(); - const bool localPlayerSpec = !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); + const bool localPlayerSpecOrNoTeam = !NEORules()->IsTeamplay() || !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); - const int leftTeam = localPlayerSpec ? TEAM_JINRAI : localPlayerTeam; + const int leftTeam = localPlayerSpecOrNoTeam ? TEAM_JINRAI : localPlayerTeam; const int rightTeam = (leftTeam == TEAM_JINRAI) ? TEAM_NSF : TEAM_JINRAI; const auto leftTeamInfo = m_teamLogoColors[leftTeam]; const auto rightTeamInfo = m_teamLogoColors[rightTeam]; @@ -399,71 +431,115 @@ void CNEOHud_RoundState::DrawNeoHudElement() m_iRightPlayersTotal = 0; int leftCount = 0; int rightCount = 0; - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + bool bDMRightSide = false; + if (NEORules()->IsTeamplay()) { - if (g_PR->IsConnected(i)) + for (int i = 0; i < (MAX_PLAYERS + 1); i++) { - const int playerTeam = g_PR->GetTeam(i); - if (playerTeam == leftTeam) + if (g_PR->IsConnected(i)) { - const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); - if (localPlayerSpec || isSameSquad) + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam == leftTeam) { - const int xOffset = (m_iLeftOffset - 4) - ((leftCount + 1) * m_ilogoSize) - (leftCount * 2); - DrawPlayer(i, leftCount, leftTeamInfo, xOffset, true); - leftCount++; + const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); + if (localPlayerSpecOrNoTeam || isSameSquad) + { + const int xOffset = (m_iLeftOffset - 4) - ((leftCount + 1) * m_ilogoSize) - (leftCount * 2); + DrawPlayer(i, leftCount, leftTeamInfo, xOffset, true); + leftCount++; + } + + if (g_PR->IsAlive(i)) + m_iLeftPlayersAlive++; + m_iLeftPlayersTotal++; } + else if (playerTeam == rightTeam) + { + const int xOffset = (m_iRightOffset + 4) + (rightCount * m_ilogoSize) + (rightCount * 2); + DrawPlayer(i, rightCount, rightTeamInfo, xOffset, localPlayerSpecOrNoTeam); + rightCount++; - if (g_PR->IsAlive(i)) - m_iLeftPlayersAlive++; - m_iLeftPlayersTotal++; + if (g_PR->IsAlive(i)) + m_iRightPlayersAlive++; + m_iRightPlayersTotal++; + } + } + } + } + else + { + PlayerXPInfo playersOrder[MAX_PLAYERS + 1] = {}; + int iTotalPlayers = 0; + DMClSortedPlayers(&playersOrder, &iTotalPlayers); + + // Second pass: Render the players in this order + const int iLTRSwitch = Ceil2Int(iTotalPlayers / 2.0f); + leftCount = (iLTRSwitch - 1); // Start from furthest leftCount index from the center + rightCount = 0; + bool bOnLeft = true; + for (int i = 0; i < iTotalPlayers; ++i) + { + if (i == iLTRSwitch) + { + bOnLeft = false; + } + + const int iPlayerIdx = playersOrder[i].idx; + const bool bPlayerLocal = g_PR->IsLocalPlayer(iPlayerIdx); + + // NEO NOTE (nullsystem): Even though they can be Jinrai/NSF, at most it's just a skin and different + // color in non-teamplay deathmatch mode. + const auto lrTeamInfo = (g_PR->GetTeam(iPlayerIdx) == leftTeam) ? leftTeamInfo : rightTeamInfo; + if (bOnLeft) + { + const int xOffset = (m_iLeftOffset - 4) - ((leftCount + 1) * m_ilogoSize) - (leftCount * 2); + DrawPlayer(iPlayerIdx, leftCount, lrTeamInfo, xOffset, bPlayerLocal); + --leftCount; } - else if (playerTeam == rightTeam) + else { const int xOffset = (m_iRightOffset + 4) + (rightCount * m_ilogoSize) + (rightCount * 2); - DrawPlayer(i, rightCount, rightTeamInfo, xOffset, localPlayerSpec); + DrawPlayer(iPlayerIdx, rightCount, lrTeamInfo, xOffset, bPlayerLocal); rightCount++; - - if (g_PR->IsAlive(i)) - m_iRightPlayersAlive++; - m_iRightPlayersTotal++; } } } - // Draw score logo - surface()->DrawSetTexture(leftTeamInfo.totalLogo); - surface()->DrawSetColor(COLOR_FADED_DARK); - surface()->DrawTexturedRect(m_rectLeftTeamTotalLogo.x0, - m_rectLeftTeamTotalLogo.y0, - m_rectLeftTeamTotalLogo.x1, - m_rectLeftTeamTotalLogo.y1); - - surface()->DrawSetTexture(rightTeamInfo.totalLogo); - surface()->DrawTexturedRect(m_rectRightTeamTotalLogo.x0, - m_rectRightTeamTotalLogo.y0, - m_rectRightTeamTotalLogo.x1, - m_rectRightTeamTotalLogo.y1); - - // Draw score - surface()->GetTextSize(m_hOCRFont, m_wszLeftTeamScore, fontWidth, fontHeight); - surface()->DrawSetTextFont(m_hOCRFont); - surface()->DrawSetTextPos(m_posLeftTeamScore.x - (fontWidth / 2), m_posLeftTeamScore.y); - surface()->DrawSetTextColor(leftTeamInfo.color); - surface()->DrawPrintText(m_wszLeftTeamScore, 2); - - surface()->GetTextSize(m_hOCRFont, m_wszRightTeamScore, fontWidth, fontHeight); - surface()->DrawSetTextPos(m_posRightTeamScore.x - (fontWidth / 2), m_posRightTeamScore.y); - surface()->DrawSetTextColor(rightTeamInfo.color); - surface()->DrawPrintText(m_wszRightTeamScore, 2); - - // Draw total players alive + if (NEORules()->IsTeamplay()) + { + // Draw score logo + surface()->DrawSetTexture(leftTeamInfo.totalLogo); + surface()->DrawSetColor(COLOR_FADED_DARK); + surface()->DrawTexturedRect(m_rectLeftTeamTotalLogo.x0, + m_rectLeftTeamTotalLogo.y0, + m_rectLeftTeamTotalLogo.x1, + m_rectLeftTeamTotalLogo.y1); + + surface()->DrawSetTexture(rightTeamInfo.totalLogo); + surface()->DrawTexturedRect(m_rectRightTeamTotalLogo.x0, + m_rectRightTeamTotalLogo.y0, + m_rectRightTeamTotalLogo.x1, + m_rectRightTeamTotalLogo.y1); + + // Draw score + surface()->GetTextSize(m_hOCRFont, m_wszLeftTeamScore, fontWidth, fontHeight); + surface()->DrawSetTextFont(m_hOCRFont); + surface()->DrawSetTextPos(m_posLeftTeamScore.x - (fontWidth / 2), m_posLeftTeamScore.y); + surface()->DrawSetTextColor(leftTeamInfo.color); + surface()->DrawPrintText(m_wszLeftTeamScore, 2); + + surface()->GetTextSize(m_hOCRFont, m_wszRightTeamScore, fontWidth, fontHeight); + surface()->DrawSetTextPos(m_posRightTeamScore.x - (fontWidth / 2), m_posRightTeamScore.y); + surface()->DrawSetTextColor(rightTeamInfo.color); + surface()->DrawPrintText(m_wszRightTeamScore, 2); + } + + // Draw total players alive (or score) surface()->DrawSetTextFont(m_hOCRSmallFont); surface()->GetTextSize(m_hOCRSmallFont, m_wszPlayersAliveUnicode, fontWidth, fontHeight); surface()->DrawSetTextColor(COLOR_WHITE); surface()->DrawSetTextPos(m_iXpos - (fontWidth / 2), m_ilogoSize); - surface()->DrawPrintText(m_wszPlayersAliveUnicode, 9); - + surface()->DrawPrintText(m_wszPlayersAliveUnicode, ARRAYSIZE(m_wszPlayersAliveUnicode) - 1); } else { @@ -572,6 +648,21 @@ void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLo surface()->DrawSetColor(COLOR_DARK); surface()->DrawTexturedRect(xOffset, Y_POS + 1, xOffset + m_ilogoSize, Y_POS + m_ilogoSize + 1); + // Deathmatch only: Draw XP on everyone + if (!NEORules()->IsTeamplay()) + { + wchar_t wszXP[9]; + const int iWszLen = V_swprintf_safe(wszXP, L"%d", g_PR->GetXP(playerIndex)); + int fontWidth, fontHeight; + surface()->DrawSetTextFont(m_hOCRSmallFont); + surface()->GetTextSize(m_hOCRSmallFont, wszXP, fontWidth, fontHeight); + const int iYExtra = drawHealthClass ? m_ilogoSize : fontHeight; + surface()->DrawSetTextPos(xOffset + ((m_ilogoSize / 2) - (fontWidth / 2)), + Y_POS + m_ilogoSize + 2 - (fontHeight / 2) + iYExtra); + surface()->DrawSetTextColor(COLOR_WHITE); + surface()->DrawPrintText(wszXP, iWszLen); + } + // Return early to not draw healthbar and class icon if (!drawHealthClass) { diff --git a/mp/src/game/client/neo/ui/neo_hud_round_state.h b/mp/src/game/client/neo/ui/neo_hud_round_state.h index 645ebb5405..075215a758 100644 --- a/mp/src/game/client/neo/ui/neo_hud_round_state.h +++ b/mp/src/game/client/neo/ui/neo_hud_round_state.h @@ -60,11 +60,12 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ int m_iSmallFontHeight = 0; // Center Box info - wchar_t m_wszRoundUnicode[9] = {}; + wchar_t m_wszRoundUnicode[12] = {}; + int m_iWszRoundUCSize = 0; wchar_t m_wszTime[6] = {}; wchar_t m_wszLeftTeamScore[3] = {}; wchar_t m_wszRightTeamScore[3] = {}; - wchar_t m_wszPlayersAliveUnicode[9] = {}; + wchar_t m_wszPlayersAliveUnicode[16] = {}; const wchar_t *m_pWszStatusUnicode = nullptr; int m_iStatusUnicodeSize = 0; wchar_t m_wszGameTypeDescription[MAX_GAME_TYPE_OBJECTIVE_LENGTH] = {}; diff --git a/mp/src/game/client/neo/ui/neo_scoreboard.cpp b/mp/src/game/client/neo/ui/neo_scoreboard.cpp index a69a6be162..feed3faea4 100644 --- a/mp/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/mp/src/game/client/neo/ui/neo_scoreboard.cpp @@ -490,6 +490,15 @@ void CNEOScoreBoard::UpdatePlayerInfo() if ( !gr ) return; + PlayerXPInfo playersOrder[MAX_PLAYERS + 1] = {}; + int iTotalPlayers = 0; + const bool bNotTeamplay = !NEORules()->IsTeamplay(); + if (bNotTeamplay) + { + DMClSortedPlayers(&playersOrder, &iTotalPlayers); + } + const int iLTRSwitch = Ceil2Int(iTotalPlayers / 2.0f); + // walk all the players and make sure they're in the scoreboard for ( int i = 1; i <= gpGlobals->maxClients; ++i ) { @@ -510,6 +519,26 @@ void CNEOScoreBoard::UpdatePlayerInfo() int team = gr->GetTeam( i ); int sectionID = GetSectionFromTeamNumber( team ); + // NEO JANK (nullsystem): Currently the place higher XPs in Jinrai/left, lower XPs in NSF/right + const bool bPlayerInDM = (bNotTeamplay && (team == TEAM_JINRAI || team == TEAM_NSF)); + if (bPlayerInDM) + { + int playerBoardPos = -1; + for (int iPO = 0; iPO < iTotalPlayers; ++iPO) + { + if (playersOrder[iPO].idx == i) + { + playerBoardPos = iPO; + break; + } + } + + if (playerBoardPos >= 0) + { + team = (playerBoardPos < iLTRSwitch) ? TEAM_JINRAI : TEAM_NSF; + } + } + auto pPlayerList = GetPanelForTeam(team); int itemID = FindItemIDForPlayerIndex( pPlayerList, i ); @@ -520,7 +549,14 @@ void CNEOScoreBoard::UpdatePlayerInfo() // add a new row itemID = pPlayerList->AddItem( sectionID, playerData ); // set the row color based on the players team - pPlayerList->SetItemFgColor( itemID, gr->GetTeamColor( team ) ); + if (bPlayerInDM) + { + pPlayerList->SetItemFgColor( itemID, gr->IsLocalPlayer(i) ? COLOR_NEO_ORANGE : COLOR_NEO_WHITE); + } + else + { + pPlayerList->SetItemFgColor( itemID, gr->GetTeamColor( team ) ); + } } else { @@ -530,7 +566,7 @@ void CNEOScoreBoard::UpdatePlayerInfo() if ( gr->IsLocalPlayer( i ) ) { - Color color = gr->GetTeamColor(team); + Color color = bPlayerInDM ? COLOR_NEO_WHITE : gr->GetTeamColor(team); color.SetColor(color.r(), color.g(), color.b(), 16); pPlayerList->SetItemBgColor(itemID, color); } @@ -601,7 +637,14 @@ void CNEOScoreBoard::AddSection(int teamType, int teamNumber) } // set the section to have the team color - pPlayerList->SetSectionFgColor(sectionID, (teamNumber == TEAM_SPECTATOR) ? COLOR_NEO_WHITE : GameResources()->GetTeamColor(teamNumber)); + if (NEORules()->IsTeamplay()) + { + pPlayerList->SetSectionFgColor(sectionID, (teamNumber == TEAM_SPECTATOR) ? COLOR_NEO_WHITE : GameResources()->GetTeamColor(teamNumber)); + } + else + { + pPlayerList->SetSectionFgColor(sectionID, COLOR_NEO_WHITE); + } } //----------------------------------------------------------------------------- @@ -654,7 +697,9 @@ void CNEOScoreBoard::GetPlayerScoreInfo(int playerIndex, KeyValues *kv) CBasePlayer* player = C_BasePlayer::GetLocalPlayer(); const int playerNeoTeam = player->GetTeamNumber(); - const bool oppositeTeam = (playerNeoTeam == TEAM_JINRAI || playerNeoTeam == TEAM_NSF) && (neoTeam != playerNeoTeam); + const bool oppositeTeam = (NEORules()->IsTeamplay()) ? + ((playerNeoTeam == TEAM_JINRAI || playerNeoTeam == TEAM_NSF) && (neoTeam != playerNeoTeam)) : + (!g_PR->IsLocalPlayer(playerIndex)); int statusIcon = -1; if (neoTeam == TEAM_JINRAI || neoTeam == TEAM_NSF) diff --git a/mp/src/game/client/neo/ui/neo_ui.cpp b/mp/src/game/client/neo/ui/neo_ui.cpp index 8488b73231..ee1ea43e76 100644 --- a/mp/src/game/client/neo/ui/neo_ui.cpp +++ b/mp/src/game/client/neo/ui/neo_ui.cpp @@ -5,6 +5,8 @@ #include #include +#include "neo_misc.h" + using namespace vgui; static const wchar_t *ENABLED_LABELS[] = { @@ -12,32 +14,6 @@ static const wchar_t *ENABLED_LABELS[] = { L"Enabled", }; -#define IN_BETWEEN_AR(min, cmp, max) (((min) <= (cmp)) && ((cmp) < (max))) -#define IN_BETWEEN_EQ(min, cmp, max) (((min) <= (cmp)) && ((cmp) <= (max))) - -[[nodiscard]] static bool InRect(const vgui::IntRect &rect, const int x, const int y) -{ - return IN_BETWEEN_EQ(rect.x0, x, rect.x1) && IN_BETWEEN_EQ(rect.y0, y, rect.y1); -} - -[[nodiscard]] static int LoopAroundMinMax(const int iValue, const int iMin, const int iMax) -{ - if (iValue < iMin) - { - return iMax; - } - else if (iValue > iMax) - { - return iMin; - } - return iValue; -} - -[[nodiscard]] static int LoopAroundInArray(const int iValue, const int iSize) -{ - return LoopAroundMinMax(iValue, 0, iSize - 1); -} - namespace NeoUI { diff --git a/mp/src/game/server/CMakeLists.txt b/mp/src/game/server/CMakeLists.txt index e2643b3e64..39e3395a15 100644 --- a/mp/src/game/server/CMakeLists.txt +++ b/mp/src/game/server/CMakeLists.txt @@ -1328,6 +1328,8 @@ target_sources_grouped( ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version_info.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_version_info.h + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_misc.cpp + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_misc.h neo/neo_client.cpp neo/neo_detpack.cpp neo/neo_detpack.h @@ -1344,6 +1346,8 @@ target_sources_grouped( neo/neo_te_tocflash.cpp neo/neo_te_tocflash.h neo/neo_tracefilter_collisiongroupdelta.h + neo/neo_dm_spawn.cpp + neo/neo_dm_spawn.h ) target_sources_grouped( diff --git a/mp/src/game/server/neo/neo_dm_spawn.cpp b/mp/src/game/server/neo/neo_dm_spawn.cpp new file mode 100644 index 0000000000..223a3fdc7f --- /dev/null +++ b/mp/src/game/server/neo/neo_dm_spawn.cpp @@ -0,0 +1,251 @@ +#include "neo_dm_spawn.h" + +#include +#include "cbase.h" +#include "inetchannelinfo.h" +#include "neo_player.h" +#include "neo_misc.h" +#include "util_shared.h" +#include "filesystem.h" +#include "utlbuffer.h" + +#include "tier0/memdbgon.h" + +static inline CUtlVector gDMSpawnLocs; +static inline int gDMSpawnCur = 0; +static inline CUtlVector gDMSpawnUsed; +static inline int gDMSpawnUsedTally = 0; + +namespace DMSpawn +{ +bool HasDMSpawn() +{ + return !gDMSpawnLocs.IsEmpty(); +} + +Info GiveNextSpawn() +{ + if (gDMSpawnLocs.IsEmpty()) return Info{}; + + // Try to give the next available spawn rather than just giving only a random number + const int iSpawnTotal = gDMSpawnLocs.Size(); + if (gDMSpawnUsedTally >= iSpawnTotal) + { + gDMSpawnUsed.SetCount(iSpawnTotal); + gDMSpawnUsedTally = 0; + } + + // iChecked for sanity check so it never inf. loop + static bool bCheckLeft = false; // Alternate check directions per spawning + int iPickSpawn = RandomInt(0, iSpawnTotal - 1); + for (int iChecked = 0; gDMSpawnUsed[iPickSpawn] && iChecked < iSpawnTotal; ++iChecked) + { + iPickSpawn = LoopAroundInArray(iPickSpawn + (bCheckLeft ? -1 : +1), iSpawnTotal); + } + bCheckLeft = !bCheckLeft; + + gDMSpawnUsed[iPickSpawn] = true; + ++gDMSpawnUsedTally; + return gDMSpawnLocs[iPickSpawn]; +} +} + +static CNEO_Player *LoopbackPlayer() +{ + for (int i = 1; i <= gpGlobals->maxClients; ++i) + { + auto *pPlayer = static_cast(UTIL_PlayerByIndex(i)); + if (!pPlayer || pPlayer->IsBot()) + { + continue; + } + INetChannelInfo *nci = engine->GetPlayerNetInfo(i); + if (nci->IsLoopback()) + { + return pPlayer; + } + } + return nullptr; +} + +static void DMSpawnComCallbackCreate() +{ + if (auto *player = LoopbackPlayer()) + { + const Vector newLoc = player->GetAbsOrigin(); + const QAngle lookAngle = player->GetAbsAngles(); + bool bAlreadyApplied = false; + for (const auto &spawn : gDMSpawnLocs) + { + if (spawn.pos == newLoc) + { + bAlreadyApplied = true; + break; + } + } + if (bAlreadyApplied) + { + Msg("DM Spawn already applied\n"); + } + else + { + gDMSpawnLocs.AddToTail(DMSpawn::Info{newLoc, lookAngle.y}); + gDMSpawnUsed.AddToTail(false); + Msg("DM Spawn: %.2f %.2f %.2f (%.2f) created\n", newLoc.x, newLoc.y, newLoc.z, lookAngle.y); + } + } +} + +static void DMSpawnComCallbackRemoveAll() +{ + gDMSpawnLocs.RemoveAll(); + gDMSpawnUsed.RemoveAll(); +} + +static void DMSpawnComCallbackRemoveOne(const CCommand &command) +{ + static constexpr char USAGE_MSG[] = "Usage: neo_sv_dmspawn_removeone [spawn index]\n"; + if (command.ArgC() != 2) + { + Msg(USAGE_MSG); + return; + } + const auto optIdx = StrToInt(command.Arg(1)); + if (!optIdx) + { + Msg(USAGE_MSG); + return; + } + + const int idx = *optIdx; + if (!IN_BETWEEN_AR(0, idx, gDMSpawnLocs.Size())) + { + Msg("Error: Index %d is not within 0 to %d.\n", idx, gDMSpawnLocs.Size() - 1); + return; + } + + gDMSpawnLocs.Remove(idx); + gDMSpawnUsed.Remove(idx); + Msg("Removed spawn %d\n", idx); +} + +static void DMSpawnComCallbackPrintLocs() +{ + for (int i = 0; i < gDMSpawnLocs.Size(); ++i) + { + const auto &spawn = gDMSpawnLocs[i]; + Msg("DM Spawn [%d]: %.2f %.2f %.2f (%.2f)\n", i, spawn.pos.x, spawn.pos.y, spawn.pos.z, spawn.lookY); + } +} + +static constexpr unsigned int MAGIC_NUMBER = 0x15AF73DA; +static constexpr int DMSPAWN_SERIAL_CURRENT = 1; + +void DMSpawnComCallbackSave([[maybe_unused]] const CCommand &command) +{ + if (gDMSpawnLocs.IsEmpty()) + { + return; + } + + CUtlBuffer buf; + buf.PutUnsignedInt(MAGIC_NUMBER); + buf.PutInt(DMSPAWN_SERIAL_CURRENT); + buf.PutInt(gDMSpawnLocs.Size()); + for (const auto &spawn : gDMSpawnLocs) + { + buf.PutFloat(spawn.pos.x); + buf.PutFloat(spawn.pos.y); + buf.PutFloat(spawn.pos.z); + buf.PutFloat(spawn.lookY); + } + + char szCurrentMapName[MAX_MAP_NAME + 1]; + V_strcpy_safe(szCurrentMapName, STRING(gpGlobals->mapname)); + filesystem->CreateDirHierarchy("maps/dm_locs"); + + char szFName[512]; + V_sprintf_safe(szFName, "maps/dm_locs/%s.loc", szCurrentMapName); + if (!filesystem->WriteFile(szFName, nullptr, buf)) + { + Msg("Failed to write file: %s\n", szFName); + return; + } + Msg("DMSpawn file saved: %s\n", szFName); +} + +void DMSpawnComCallbackLoad([[maybe_unused]] const CCommand &command) +{ + gDMSpawnLocs.RemoveAll(); + + char szCurrentMapName[MAX_MAP_NAME + 1]; + V_strcpy_safe(szCurrentMapName, STRING(gpGlobals->mapname)); + + CUtlBuffer buf(0, 0, CUtlBuffer::READ_ONLY); + char szFName[512]; + V_sprintf_safe(szFName, "maps/dm_locs/%s.loc", szCurrentMapName); + if (!filesystem->ReadFile(szFName, nullptr, buf)) + { + Msg("DMSpawn file not found: %s", szFName); + return; + } + + if (buf.GetUnsignedInt() != MAGIC_NUMBER) + { + return; + } + + const int version = buf.GetInt(); + const int locsSize = buf.GetInt(); + for (int i = 0; i < locsSize && buf.IsValid(); ++i) + { + DMSpawn::Info spawn = {}; + spawn.pos.x = buf.GetFloat(); + spawn.pos.y = buf.GetFloat(); + spawn.pos.z = buf.GetFloat(); + spawn.lookY = buf.GetFloat(); + gDMSpawnLocs.AddToTail(spawn); + } + gDMSpawnUsed.SetCount(gDMSpawnLocs.Size()); + Msg("DMSpawn file loaded: %s", szFName); +} + +static void DMSpawnComCallbackTeleportNext() +{ + if (gDMSpawnLocs.IsEmpty()) return; + gDMSpawnCur = LoopAroundInArray(gDMSpawnCur + 1, gDMSpawnLocs.Size()); + if (auto *pPlayer = LoopbackPlayer()) + { + const auto &spawn = gDMSpawnLocs[gDMSpawnCur]; + const QAngle spawnAngle{0, spawn.lookY, 0}; + pPlayer->SetAbsOrigin(spawn.pos); + pPlayer->SetAbsAngles(spawnAngle); + pPlayer->SnapEyeAngles(spawnAngle); + Msg("Teleported to spawn %d: %.2f %.2f %.2f (%.2f)\n", gDMSpawnCur, spawn.pos.x, spawn.pos.y, spawn.pos.z, spawn.lookY); + } +} + +static void DMSpawnComCallbackMapinfo() +{ + char szCurrentMapName[MAX_MAP_NAME + 1]; + V_strcpy_safe(szCurrentMapName, STRING(gpGlobals->mapname)); + + int iEntCount = 0; + CBaseEntity *entDMSpawn = nullptr; + while ((entDMSpawn = gEntList.FindEntityByClassname(entDMSpawn, "info_player_deathmatch"))) + { + ++iEntCount; + } + + Msg("Deathmatch spawns for: %s\ndmspawn spawns count: %d\ninfo_player_deathmatch count: %d\nAllow deathmatch: %s", + szCurrentMapName, gDMSpawnLocs.Size(), iEntCount, (iEntCount > 0 || !gDMSpawnLocs.IsEmpty()) ? "YES" : "NO"); +} + +ConCommand neo_sv_dmspawn_create("neo_sv_dmspawn_create", &DMSpawnComCallbackCreate, "DMSpawn - Create a new spawn", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_removeallspawns("neo_sv_dmspawn_removeallspawns", &DMSpawnComCallbackRemoveAll, "DMSpawn - Remove all spawns", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_removeone("neo_sv_dmspawn_removeone", &DMSpawnComCallbackRemoveOne, "DMSpawn - Remove one spawn by a given index", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_printlocs("neo_sv_dmspawn_printlocs", &DMSpawnComCallbackPrintLocs, "DMSpawn - Print locations and Y-angle of all spawns", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_save("neo_sv_dmspawn_save", &DMSpawnComCallbackSave, "DMSpawn - Save spawn file to filesystem", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_load("neo_sv_dmspawn_load", &DMSpawnComCallbackLoad, "DMSpawn - Load spawn file from filesystem", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_teleportnext("neo_sv_dmspawn_teleportnext", &DMSpawnComCallbackTeleportNext, "DMSpawn - Teleport to the next spawn", FCVAR_USERINFO | FCVAR_CHEAT); +ConCommand neo_sv_dmspawn_mapinfo("neo_sv_dmspawn_mapinfo", &DMSpawnComCallbackMapinfo, "DMSpawn - Map deathmatch spawns check and information", FCVAR_USERINFO); diff --git a/mp/src/game/server/neo/neo_dm_spawn.h b/mp/src/game/server/neo/neo_dm_spawn.h new file mode 100644 index 0000000000..6c483bf726 --- /dev/null +++ b/mp/src/game/server/neo/neo_dm_spawn.h @@ -0,0 +1,20 @@ +#pragma once + +#include "mathlib/vector.h" +#include "convar.h" + +namespace DMSpawn +{ + +struct Info +{ + Vector pos; + float lookY; +}; + +bool HasDMSpawn(); +Info GiveNextSpawn(); + +} + +void DMSpawnComCallbackLoad([[maybe_unused]] const CCommand &command = CCommand{}); diff --git a/mp/src/game/server/neo/neo_player.cpp b/mp/src/game/server/neo/neo_player.cpp index c6965a8821..cd96794dc5 100644 --- a/mp/src/game/server/neo/neo_player.cpp +++ b/mp/src/game/server/neo/neo_player.cpp @@ -133,11 +133,11 @@ void CNEO_Player::RequestSetClass(int newClass) return; } + const bool bIsTypeDM = (NEORules()->GetGameType() == NEO_GAME_TYPE_TDM || NEORules()->GetGameType() == NEO_GAME_TYPE_DM); const NeoRoundStatus status = NEORules()->GetRoundStatus(); if (IsDead() || sv_neo_can_change_classes_anytime.GetBool() || (!m_bIneligibleForLoadoutPick && NEORules()->GetRemainingPreRoundFreezeTime(false) > 0) || - (NEORules()->GetGameType() == NEO_GAME_TYPE_TDM && !m_bIneligibleForLoadoutPick && - GetAliveDuration() < neo_sv_dm_max_class_dur.GetFloat()) || + (bIsTypeDM && !m_bIneligibleForLoadoutPick && GetAliveDuration() < neo_sv_dm_max_class_dur.GetFloat()) || (status == NeoRoundStatus::Idle || status == NeoRoundStatus::Warmup)) { m_iNeoClass = newClass; @@ -1198,7 +1198,6 @@ bool CNEO_Player::ClientCommand( const CCommand &args ) if (m_iDmgMenuCurPage > 0) { - // menuselect being used by damage stats switch (slot) { case DAMAGE_MENU_SELECT_DISMISS: @@ -1663,7 +1662,8 @@ void CNEO_Player::SetPlayerCorpseModel(int type) float CNEO_Player::GetReceivedDamageScale(CBaseEntity* pAttacker) { - if (NEORules()->GetGameType() == NEO_GAME_TYPE_TDM && m_aliveTimer.IsLessThen(5.f)) + if ((NEORules()->GetGameType() == NEO_GAME_TYPE_TDM || NEORules()->GetGameType() == NEO_GAME_TYPE_DM) + && m_aliveTimer.IsLessThen(5.f) && !m_bIneligibleForLoadoutPick) { // 5 seconds of invincibility in TDM return 0.f; } @@ -1939,7 +1939,15 @@ CBaseEntity* CNEO_Player::EntSelectSpawnPoint( void ) const char *pSpawnpointName = "info_player_start"; const auto alternate = NEORules()->roundAlternate(); - if (NEORules()->IsTeamplay()) + // NEO TODO (nullsystem): Teamplay vs non-teamplay + // info_player_deathmatch is from HL2MP, but maps can utilize HL2MP and this entity anyway + const bool bIsTeamplay = NEORules()->IsTeamplay(); + if (!bIsTeamplay) + { + pSpawnpointName = "info_player_deathmatch"; + pLastSpawnPoint = g_pLastSpawn; + } + else { if (GetTeamNumber() == TEAM_JINRAI) { @@ -2424,7 +2432,8 @@ void CNEO_Player::GiveLoadoutWeapon(void) { const NeoRoundStatus status = NEORules()->GetRoundStatus(); if (!(status == NeoRoundStatus::Idle || status == NeoRoundStatus::Warmup) && - (IsObserver() || IsDead() || m_bIneligibleForLoadoutPick || m_aliveTimer.IsGreaterThen(sv_neo_time_alive_until_cant_change_loadout.GetFloat()))) + (IsObserver() || IsDead() || m_bIneligibleForLoadoutPick + || m_aliveTimer.IsGreaterThen(sv_neo_time_alive_until_cant_change_loadout.GetFloat()))) { return; } diff --git a/mp/src/game/shared/hl2mp/hl2mp_gamerules.h b/mp/src/game/shared/hl2mp/hl2mp_gamerules.h index 75f8f2fe8d..d902e4da64 100644 --- a/mp/src/game/shared/hl2mp/hl2mp_gamerules.h +++ b/mp/src/game/shared/hl2mp/hl2mp_gamerules.h @@ -162,10 +162,14 @@ class CHL2MPRules : public CTeamplayRules void PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info ); #endif +#ifdef NEO + virtual bool GetTeamPlayEnabled() const { return true; } +#endif + bool IsTeamplay( void ) { #ifdef NEO - return true; + return GetTeamPlayEnabled(); #else return m_bTeamPlayEnabled; #endif diff --git a/mp/src/game/shared/neo/neo_gamerules.cpp b/mp/src/game/shared/neo/neo_gamerules.cpp index 6682058370..65d4037f4e 100644 --- a/mp/src/game/shared/neo/neo_gamerules.cpp +++ b/mp/src/game/shared/neo/neo_gamerules.cpp @@ -10,6 +10,7 @@ #ifdef CLIENT_DLL #include "c_neo_player.h" #include "c_team.h" + #include "c_playerresource.h" #else #include "neo_player.h" #include "team.h" @@ -23,6 +24,7 @@ #include "hl2mp_gameinterface.h" #include "player_resource.h" #include "inetchannelinfo.h" + #include "neo_dm_spawn.h" extern ConVar weaponstay; #endif @@ -40,7 +42,7 @@ ConVar neo_name("neo_name", "", FCVAR_USERINFO | FCVAR_ARCHIVE, "The nickname to ConVar cl_onlysteamnick("cl_onlysteamnick", "0", FCVAR_USERINFO | FCVAR_ARCHIVE, "Only show players Steam names, otherwise show player set names.", true, 0.0f, true, 1.0f); #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", true, 0, true, 2); +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); 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); @@ -94,6 +96,8 @@ ConVar neo_sv_readyup_autointermission("neo_sv_readyup_autointermission", "0", F // 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); +ConVar snd_victory_volume("snd_victory_volume", "0.33", FCVAR_ARCHIVE | FCVAR_DONTRECORD | FCVAR_USERINFO, "Loudness of the victory jingle (0-1).", true, 0.0, true, 1.0); + REGISTER_GAMERULES_CLASS( CNEORules ); BEGIN_NETWORK_TABLE_NOBASE( CNEORules, DT_NEORules ) @@ -206,9 +210,16 @@ ConVar neo_ctg_round_timelimit("neo_ctg_round_timelimit", "3.25", FCVAR_REPLICAT ConVar neo_vip_round_timelimit("neo_vip_round_timelimit", "3.25", FCVAR_REPLICATED, "VIP round timelimit, in minutes.", true, 0.0f, false, 600.0f); +ConVar neo_dm_round_timelimit("neo_dm_round_timelimit", "10.25", FCVAR_REPLICATED, "DM round timelimit, in minutes.", + true, 0.0f, false, 600.0f); + ConVar neo_sv_ignore_wep_xp_limit("neo_sv_ignore_wep_xp_limit", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "If true, allow equipping any loadout regardless of player XP.", true, 0.0f, true, 1.0f); +ConVar neo_sv_dm_win_xp("neo_sv_dm_win_xp", "50", FCVAR_REPLICATED, "The XP limit to win the match.", + true, 0.0f, true, 1000.0f); + + #ifdef CLIENT_DLL extern ConVar neo_fov; #endif @@ -522,6 +533,7 @@ void CNEORules::ResetMapSessionCommon() m_bTeamBeenAwardedDueToCapPrevent = false; V_memset(m_arrayiEntPrevCap, 0, sizeof(m_arrayiEntPrevCap)); m_iEntPrevCapSize = 0; + DMSpawnComCallbackLoad(); #endif } @@ -554,6 +566,64 @@ bool CNEORules::CheckGameOver(void) return gameOver; } +void CNEORules::GetDMHighestScorers( +#ifdef GAME_DLL + CNEO_Player *(*pHighestPlayers)[MAX_PLAYERS + 1], +#endif + int *iHighestPlayersTotal, + int *iHighestXP) const +{ + *iHighestPlayersTotal = 0; + *iHighestXP = 0; +#ifdef GAME_DLL + for (int i = 1; i <= gpGlobals->maxClients; ++i) +#else + if (!g_PR) + { + return; + } + + for (int i = 0; i < (MAX_PLAYERS + 1); ++i) +#endif + { + int iXP = 0; + +#ifdef GAME_DLL + auto pCmpPlayer = static_cast(UTIL_PlayerByIndex(i)); + if (!pCmpPlayer) + { + continue; + } + iXP = pCmpPlayer->m_iXP; +#else + if (!g_PR->IsConnected(i)) + { + continue; + } + iXP = g_PR->GetXP(i); +#endif + + if (iXP == *iHighestXP) + { +#ifdef GAME_DLL + (*pHighestPlayers)[(*iHighestPlayersTotal)++] = pCmpPlayer; +#else + (*iHighestPlayersTotal)++; +#endif + } + else if (iXP > *iHighestXP) + { + *iHighestPlayersTotal = 0; + *iHighestXP = iXP; +#ifdef GAME_DLL + (*pHighestPlayers)[(*iHighestPlayersTotal)++] = pCmpPlayer; +#else + (*iHighestPlayersTotal)++; +#endif + } + } +} + void CNEORules::Think(void) { #ifdef GAME_DLL @@ -565,7 +635,7 @@ void CNEORules::Think(void) } // Allow respawn if it's an idle, warmup round, or deathmatch-type gamemode - const bool bIsDMType = (m_nGameTypeSelected == NEO_GAME_TYPE_TDM); + const bool bIsDMType = (m_nGameTypeSelected == NEO_GAME_TYPE_DM || m_nGameTypeSelected == NEO_GAME_TYPE_TDM); if (bIsDMType || bIsIdleState) { CRecipientFilter filter; @@ -579,8 +649,6 @@ void CNEORules::Think(void) const int playerTeam = player->GetTeamNumber(); if ((playerTeam == TEAM_JINRAI || playerTeam == TEAM_NSF) && RespawnWithRet(player, false)) { - filter.AddRecipient(player); - player->m_bInAim = false; player->m_bInThermOpticCamo = false; player->m_bInVision = false; @@ -707,7 +775,25 @@ void CNEORules::Think(void) return; } } - SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, false, true, false); + else if (GetGameType() == NEO_GAME_TYPE_DM) + { + // Winning player + CNEO_Player *pWinners[MAX_PLAYERS + 1] = {}; + int iWinnersTotal = 0; + int iWinnerXP = 0; + GetDMHighestScorers(&pWinners, &iWinnersTotal, &iWinnerXP); + if (iWinnersTotal == 1) + { + SetWinningDMPlayer(pWinners[0]); + return; + } + // Otherwise go into overtime + } + + if (IsTeamplay()) + { + SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, false, true, false); + } } if (m_pGhost) @@ -906,7 +992,7 @@ void CNEORules::Think(void) if (m_nRoundStatus == NeoRoundStatus::RoundLive) { COMPILE_TIME_ASSERT(TEAM_JINRAI == 2 && TEAM_NSF == 3); - if (GetGameType() != NEO_GAME_TYPE_TDM) + if (GetGameType() != NEO_GAME_TYPE_TDM && GetGameType() != NEO_GAME_TYPE_DM) { for (int team = TEAM_JINRAI; team <= TEAM_NSF; ++team) { @@ -916,11 +1002,78 @@ void CNEORules::Think(void) } } } + if (GetGameType() == NEO_GAME_TYPE_DM && neo_sv_dm_win_xp.GetInt() > 0) + { + // End game early if there's already a player past the winning XP + CNEO_Player *pHighestPlayers[MAX_PLAYERS + 1] = {}; + int iWinningTotal = 0; + int iWinningXP = 0; + GetDMHighestScorers(&pHighestPlayers, &iWinningTotal, &iWinningXP); + if (iWinningXP >= neo_sv_dm_win_xp.GetInt() && iWinningTotal == 1) + { + SetWinningDMPlayer(pHighestPlayers[0]); + } + } } } #endif } +#ifdef GAME_DLL +void CNEORules::SetWinningDMPlayer(CNEO_Player *pWinner) +{ + if (IsRoundOver()) + { + return; + } + + SetRoundStatus(NeoRoundStatus::PostRound); + char victoryMsg[128]; + // TODO: Per client since client has neo_name settings + V_sprintf_safe(victoryMsg, "%s is the winner of the deathmatch!\n", pWinner->GetNeoPlayerName()); + + CRecipientFilter filter; + filter.AddAllPlayers(); + UserMessageBegin(filter, "RoundResult"); + WRITE_STRING("tie"); + WRITE_FLOAT(gpGlobals->curtime); + WRITE_STRING(victoryMsg); + MessageEnd(); + + EmitSound_t soundParams; + soundParams.m_nChannel = CHAN_AUTO; + soundParams.m_SoundLevel = SNDLVL_NONE; + soundParams.m_flVolume = 0.33f; + // Differing between Jinrai/NSF only as a sound cosmetic (no affect on DM) + const int team = pWinner->GetTeamNumber(); + soundParams.m_pSoundName = (team == TEAM_JINRAI) ? "gameplay/jinrai.mp3" : (team == TEAM_NSF) ? "gameplay/nsf.mp3" : "gameplay/draw.mp3"; + soundParams.m_bWarnOnDirectWaveReference = false; + soundParams.m_bEmitCloseCaption = false; + + for (int i = 1; i <= gpGlobals->maxClients; ++i) + { + CBasePlayer* basePlayer = UTIL_PlayerByIndex(i); + auto player = static_cast(basePlayer); + if (player) + { + if (!player->IsBot() || player->IsHLTV()) + { + const char* volStr = engine->GetClientConVarValue(i, snd_victory_volume.GetName()); + const float jingleVolume = volStr ? atof(volStr) : 0.33f; + soundParams.m_flVolume = jingleVolume; + + CRecipientFilter soundFilter; + soundFilter.AddRecipient(basePlayer); + soundFilter.MakeReliable(); + player->EmitSound(soundFilter, i, soundParams); + } + } + } + + GoToIntermission(); +} +#endif + void CNEORules::AwardRankUp(int client) { auto player = UTIL_PlayerByIndex(client); @@ -980,6 +1133,9 @@ float CNEORules::GetRoundRemainingTime() const case NEO_GAME_TYPE_VIP: roundTimeLimit = neo_vip_round_timelimit.GetFloat() * 60.f; break; + case NEO_GAME_TYPE_DM: + roundTimeLimit = neo_dm_round_timelimit.GetFloat() * 60.f; + break; default: break; } @@ -1194,7 +1350,7 @@ void CNEORules::SelectTheVIP() void CNEORules::GatherGameTypeVotes() { - int gameTypes[NEO_GAME_TYPE_TOTAL] = {}; + int gameTypes[NEO_GAME_TYPE__TOTAL] = {}; for (int i = 1; i <= gpGlobals->maxClients; i++) { @@ -1214,7 +1370,7 @@ void CNEORules::GatherGameTypeVotes() int mostVotes = gameTypes[0]; int mostPopularGameType = 0; - for (int i = 1; i < NEO_GAME_TYPE_TOTAL; i++) + for (int i = 1; i < NEO_GAME_TYPE__TOTAL; i++) { if (gameTypes[i] > mostVotes) // NEOTODO (Adam) Handle draws { @@ -1469,6 +1625,11 @@ void CNEORules::StartNextRound() SetRoundStatus(NeoRoundStatus::PreRoundFreeze); + 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); @@ -1620,6 +1781,8 @@ const char *CNEORules::GetGameDescription(void) 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(); } @@ -1862,6 +2025,18 @@ void CNEORules::SetGameRelatedVars() GetGlobalTeam(i)->SetScore(0); } } + + if (GetGameType() == NEO_GAME_TYPE_DM) + { + for (int i = 1; i <= gpGlobals->maxClients; ++i) + { + auto pPlayer = static_cast(UTIL_PlayerByIndex(i)); + if (pPlayer) + { + pPlayer->m_iXP.GetForModify() = 0; + } + } + } } void CNEORules::ResetTDM() @@ -2165,9 +2340,8 @@ bool CNEORules::RoundIsMatchPoint() const } return false; } -#ifdef CLIENT_DLL -ConVar snd_victory_volume("snd_victory_volume", "0.33", FCVAR_ARCHIVE | FCVAR_DONTRECORD | FCVAR_USERINFO, "Loudness of the victory jingle (0-1).", true, 0.0, true, 1.0); -#else + +#ifdef GAME_DLL extern ConVar snd_musicvolume; void CNEORules::SetWinningTeam(int team, int iWinReason, bool bForceMapReset, bool bSwitchTeams, bool bDontAddScore, bool bFinal) { @@ -2429,6 +2603,11 @@ static CNEO_Player* FetchAssists(CNEO_Player* attacker, CNEO_Player* victim) #ifdef GAME_DLL void CNEORules::CheckIfCapPrevent(CNEO_Player *capPreventerPlayer) { + if (m_nGameTypeSelected != NEO_GAME_TYPE_CTG && m_nGameTypeSelected != NEO_GAME_TYPE_VIP) + { + return; + } + // If this is the only player alive left before the suicide/disconnect and the other team was holding // the ghost, reward the other team an XP to the next rank as a ghost cap was prevented. const bool bShouldCheck = (neo_sv_suicide_prevent_cap_punish.GetBool() @@ -2513,7 +2692,7 @@ void CNEORules::PlayerKilled(CBasePlayer *pVictim, const CTakeDamageInfo &info) else if (attacker) { // Team kill - if (attacker->GetTeamNumber() == victim->GetTeamNumber()) + if (IsTeamplay() && attacker->GetTeamNumber() == victim->GetTeamNumber()) { attacker->m_iXP.GetForModify() -= 1; #ifdef GAME_DLL @@ -2845,12 +3024,17 @@ void CNEORules::ClientDisconnected(edict_t* pClient) } #endif +bool CNEORules::GetTeamPlayEnabled() const +{ + return m_nGameTypeSelected != NEO_GAME_TYPE_DM; +} + #ifdef GAME_DLL bool CNEORules::FPlayerCanRespawn(CBasePlayer* pPlayer) { auto gameType = GetGameType(); - if (gameType == NEO_GAME_TYPE_TDM) + if (gameType == NEO_GAME_TYPE_TDM || gameType == NEO_GAME_TYPE_DM) { return true; } @@ -2891,6 +3075,27 @@ bool CNEORules::FPlayerCanRespawn(CBasePlayer* pPlayer) return false; } + +CBaseEntity *CNEORules::GetPlayerSpawnSpot(CBasePlayer *pPlayer) +{ + // NEO NOTE (nullsystem): If available + DM, instead of by entity, player spawn + // by set position. It doesn't seem anything utilizes what returned anyway. + if (m_nGameTypeSelected == NEO_GAME_TYPE_DM && DMSpawn::HasDMSpawn()) + { + const auto spawn = DMSpawn::GiveNextSpawn(); + const QAngle spawnAngle{0, spawn.lookY, 0}; + pPlayer->SetLocalOrigin(spawn.pos + Vector(0,0,1)); + pPlayer->SetAbsVelocity(vec3_origin); + pPlayer->SetLocalAngles(spawnAngle); + pPlayer->m_Local.m_vecPunchAngle = vec3_angle; + pPlayer->m_Local.m_vecPunchAngleVel = vec3_angle; + pPlayer->SnapEyeAngles(spawnAngle); + return nullptr; + } + + return BaseClass::GetPlayerSpawnSpot(pPlayer); +} + #endif void CNEORules::SetRoundStatus(NeoRoundStatus status) @@ -2937,6 +3142,8 @@ const char* CNEORules::GetGameTypeName(void) return "CTG"; case NEO_GAME_TYPE_VIP: return "VIP"; + case NEO_GAME_TYPE_DM: + return "DM"; default: Assert(false); return "NAN"; diff --git a/mp/src/game/shared/neo/neo_gamerules.h b/mp/src/game/shared/neo/neo_gamerules.h index ab39348434..aedddb50d6 100644 --- a/mp/src/game/shared/neo/neo_gamerules.h +++ b/mp/src/game/shared/neo/neo_gamerules.h @@ -12,7 +12,9 @@ #include "GameEventListener.h" -#ifndef CLIENT_DLL +#ifdef CLIENT_DLL + #include "c_neo_player.h" +#else #include "neo_player.h" #include "utlhashtable.h" #endif @@ -88,6 +90,7 @@ extern ConVar neo_sv_mirror_teamdamage_multiplier; extern ConVar neo_sv_mirror_teamdamage_duration; extern ConVar neo_sv_mirror_teamdamage_immunity; extern ConVar neo_sv_teamdamage_kick; + #else class C_NEO_Player; #endif @@ -98,8 +101,9 @@ enum NeoGameType { NEO_GAME_TYPE_TDM = 0, NEO_GAME_TYPE_CTG, NEO_GAME_TYPE_VIP, + NEO_GAME_TYPE_DM, - NEO_GAME_TYPE_TOTAL // Number of game types + NEO_GAME_TYPE__TOTAL // Number of game types }; enum NeoRoundStatus { @@ -133,15 +137,23 @@ class CNEORules : public CHL2MPRules, public CGameEventListener virtual bool ClientCommand(CBaseEntity* pEdict, const CCommand& args) OVERRIDE; virtual void SetWinningTeam(int team, int iWinReason, bool bForceMapReset = true, bool bSwitchTeams = false, bool bDontAddScore = false, bool bFinal = false) OVERRIDE; + void SetWinningDMPlayer(CNEO_Player *pWinner); virtual void ChangeLevel(void) OVERRIDE; virtual void ClientDisconnected(edict_t* pClient) OVERRIDE; + + CBaseEntity *GetPlayerSpawnSpot(CBasePlayer *pPlayer) override; #endif virtual bool ShouldCollide( int collisionGroup0, int collisionGroup1 ) OVERRIDE; virtual const char* GetGameName() { return NEO_GAME_NAME; } +#ifdef NEO + bool GetTeamPlayEnabled() const override; +#endif + + #ifdef GAME_DLL virtual bool FPlayerCanRespawn(CBasePlayer* pPlayer) OVERRIDE; #endif @@ -149,6 +161,13 @@ class CNEORules : public CHL2MPRules, public CGameEventListener virtual int GetGameType(void) OVERRIDE; virtual const char* GetGameTypeName(void) OVERRIDE; + void GetDMHighestScorers( +#ifdef GAME_DLL + CNEO_Player *(*pHighestPlayers)[MAX_PLAYERS + 1], +#endif + int *iHighestPlayersTotal, + int *iHighestXP) const; + virtual void Think( void ) OVERRIDE; virtual void CreateStandardEntities( void ) OVERRIDE; diff --git a/mp/src/game/shared/neo/neo_misc.cpp b/mp/src/game/shared/neo/neo_misc.cpp new file mode 100644 index 0000000000..3a08350033 --- /dev/null +++ b/mp/src/game/shared/neo/neo_misc.cpp @@ -0,0 +1,24 @@ +#include "neo_misc.h" + +[[nodiscard]] bool InRect(const vgui::IntRect &rect, const int x, const int y) +{ + return IN_BETWEEN_EQ(rect.x0, x, rect.x1) && IN_BETWEEN_EQ(rect.y0, y, rect.y1); +} + +[[nodiscard]] int LoopAroundMinMax(const int iValue, const int iMin, const int iMax) +{ + if (iValue < iMin) + { + return iMax; + } + else if (iValue > iMax) + { + return iMin; + } + return iValue; +} + +[[nodiscard]] int LoopAroundInArray(const int iValue, const int iSize) +{ + return LoopAroundMinMax(iValue, 0, iSize - 1); +} diff --git a/mp/src/game/shared/neo/neo_misc.h b/mp/src/game/shared/neo/neo_misc.h new file mode 100644 index 0000000000..9a80389046 --- /dev/null +++ b/mp/src/game/shared/neo/neo_misc.h @@ -0,0 +1,14 @@ +#pragma once + +#include "vgui/ISurface.h" + +/* + * neo_misc.h - Miscellaneous functions/macros that doesn't belong at a specific place. + */ + +#define IN_BETWEEN_AR(min, cmp, max) (((min) <= (cmp)) && ((cmp) < (max))) +#define IN_BETWEEN_EQ(min, cmp, max) (((min) <= (cmp)) && ((cmp) <= (max))) + +[[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); diff --git a/mp/src/game/shared/neo/neo_player_shared.cpp b/mp/src/game/shared/neo/neo_player_shared.cpp index 0e43d57b07..d2a4318735 100644 --- a/mp/src/game/shared/neo/neo_player_shared.cpp +++ b/mp/src/game/shared/neo/neo_player_shared.cpp @@ -11,6 +11,7 @@ #ifdef CLIENT_DLL #include "c_neo_player.h" +#include "c_playerresource.h" #ifndef CNEO_Player #define CNEO_Player C_NEO_Player #endif @@ -161,3 +162,42 @@ void KillerLineStr(char* killByLine, const int killByLineMax, auto *neoWep = dynamic_cast(wep); return (neoWep) ? neoWep->GetWpnData().iAimFOV : fovDef - FOV_AIM_OFFSET_FALLBACK; } + +#ifdef CLIENT_DLL +void DMClSortedPlayers(PlayerXPInfo (*pPlayersOrder)[MAX_PLAYERS + 1], int *piTotalPlayers) +{ + int iTotalPlayers = 0; + + // First pass: Find all scores of all players + for (int i = 0; i < (MAX_PLAYERS + 1); i++) + { + if (g_PR->IsConnected(i)) + { + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam == TEAM_JINRAI || playerTeam == TEAM_NSF) + { + (*pPlayersOrder)[iTotalPlayers++] = PlayerXPInfo{ + .idx = i, + .xp = g_PR->GetXP(i), + .deaths = g_PR->GetDeaths(i), + }; + } + } + } + + V_qsort_s(*pPlayersOrder, iTotalPlayers, sizeof(PlayerXPInfo), + []([[maybe_unused]] void *vpCtx, const void *vpLeft, const void *vpRight) -> int { + auto *pLeft = static_cast(vpLeft); + auto *pRight = static_cast(vpRight); + if (pRight->xp == pLeft->xp) + { + // More deaths = lower + return pLeft->deaths - pRight->deaths; + } + // More XP = higher + return pRight->xp - pLeft->xp; + }, nullptr); + + *piTotalPlayers = iTotalPlayers; +} +#endif diff --git a/mp/src/game/shared/neo/neo_player_shared.h b/mp/src/game/shared/neo/neo_player_shared.h index a2290e01ed..1b2bffd19c 100644 --- a/mp/src/game/shared/neo/neo_player_shared.h +++ b/mp/src/game/shared/neo/neo_player_shared.h @@ -301,4 +301,14 @@ struct IntDim int h; }; +#ifdef CLIENT_DLL +struct PlayerXPInfo +{ + int idx; + int xp; + int deaths; +}; +void DMClSortedPlayers(PlayerXPInfo (*pPlayersOrder)[MAX_PLAYERS + 1], int *piTotalPlayers); +#endif + #endif // NEO_PLAYER_SHARED_H