diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 336ec0926..c1a92d9a8 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -2095,6 +2095,78 @@ Scheme //"additive" "1" } } + NeoUIScoreboard + { + "1" + { + "name" "Montserrat Bold" + "tall" "14" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "480 599" + //"additive" "1" + } + "2" + { + "name" "Montserrat Bold" + "tall" "16" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "600 767" + //"additive" "1" + } + "3" + { + "name" "Montserrat Bold" + "tall" "18" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "768 1023" + "antialias" "1" + //"additive" "1" + } + "4" + { + "name" "Montserrat Bold" + "tall" "20" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1024 1199" + "antialias" "1" + //"additive" "1" + } + "5" + { + "name" "Montserrat Bold" + "tall" "22" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1200 1440" + "antialias" "1" + //"additive" "1" + } + "6" + { + "name" "Montserrat Bold" + "tall" "26" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1441 1599" + "antialias" "1" + //"additive" "1" + } + "7" + { + "name" "Montserrat Bold" + "tall" "28" + "weight" "550" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1600 6000" + "antialias" "1" + //"additive" "1" + } + } + NeoUISmall { "1" @@ -3206,6 +3278,7 @@ Scheme "13" "resource/neotokyo_press_n.ttf" "14" "resource/killfeedicons.ttf" "15" "resource/montserrat-regular.ttf" + "16" "resource/montserrat-bold.ttf" } } diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt index 19c2458a4..8ff11cc60 100644 --- a/src/game/client/CMakeLists.txt +++ b/src/game/client/CMakeLists.txt @@ -1619,6 +1619,8 @@ target_sources_grouped( neo/ui/neo_hud_worldpos_marker_generic.h neo/ui/neo_scoreboard.cpp neo/ui/neo_scoreboard.h + neo/ui/neoui_scoreboard.cpp + neo/ui/neoui_scoreboard.h neo/ui/neo_hud_context_hint.cpp neo/ui/neo_hud_context_hint.h ) diff --git a/src/game/client/c_playerresource.cpp b/src/game/client/c_playerresource.cpp index 03e7ef1e5..51bbf1120 100644 --- a/src/game/client/c_playerresource.cpp +++ b/src/game/client/c_playerresource.cpp @@ -33,6 +33,8 @@ IMPLEMENT_CLIENTCLASS_DT_NOBASE(C_PlayerResource, DT_PlayerResource, CPlayerReso RecvPropArray3(RECVINFO_ARRAY(m_iStar), RecvPropInt(RECVINFO(m_iStar[0]))), RecvPropArray3(RECVINFO_ARRAY(m_szNeoClantag), RecvPropString(RECVINFO(m_szNeoClantag[0]))), RecvPropArray3(RECVINFO_ARRAY(m_iMaxHealth), RecvPropInt(RECVINFO(m_iMaxHealth[0]))), + RecvPropArray3(RECVINFO_ARRAY(m_szNeoCrosshair), RecvPropString(RECVINFO(m_szNeoCrosshair[0]))), + RecvPropArray3(RECVINFO_ARRAY(m_bReady), RecvPropInt(RECVINFO(m_bReady[0]))), #endif RecvPropArray3( RECVINFO_ARRAY(m_iScore), RecvPropInt( RECVINFO(m_iScore[0]))), RecvPropArray3( RECVINFO_ARRAY(m_iDeaths), RecvPropInt( RECVINFO(m_iDeaths[0]))), @@ -57,6 +59,8 @@ BEGIN_PREDICTION_DATA( C_PlayerResource ) DEFINE_PRED_ARRAY(m_iStar, FIELD_INTEGER, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE), DEFINE_PRED_ARRAY(m_szNeoClantag, FIELD_STRING, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE), DEFINE_PRED_ARRAY(m_iMaxHealth, FIELD_INTEGER, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE), + DEFINE_PRED_ARRAY(m_szNeoCrosshair, FIELD_STRING, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE), + DEFINE_PRED_ARRAY(m_bReady, FIELD_BOOLEAN, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE), #endif DEFINE_PRED_ARRAY( m_iScore, FIELD_INTEGER, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE ), DEFINE_PRED_ARRAY( m_iDeaths, FIELD_INTEGER, MAX_PLAYERS_ARRAY_SAFE, FTYPEDESC_PRIVATE ), @@ -94,6 +98,8 @@ C_PlayerResource::C_PlayerResource() memset(m_iStar, 0, sizeof(m_iStar)); memset(m_szNeoClantag, 0, sizeof(m_szNeoClantag)); memset(m_iMaxHealth, 1, sizeof(m_iMaxHealth)); + memset(m_szNeoCrosshair, 0, sizeof(m_szNeoCrosshair)); + memset(m_bReady, 0, sizeof(m_bReady)); #endif memset( m_iScore, 0, sizeof( m_iScore ) ); memset( m_iDeaths, 0, sizeof( m_iDeaths ) ); @@ -250,6 +256,16 @@ const char *C_PlayerResource::GetClanTag(int iIndex) { return m_szNeoClantag[iIndex]; } + +const char *C_PlayerResource::GetNeoCrosshair(int iIndex) +{ + return m_szNeoCrosshair[iIndex]; +} + +bool C_PlayerResource::IsReady(int iIndex) +{ + return m_bReady[iIndex]; +} #endif bool C_PlayerResource::IsAlive(int iIndex ) diff --git a/src/game/client/c_playerresource.h b/src/game/client/c_playerresource.h index 13402ce81..40fd2f9dd 100644 --- a/src/game/client/c_playerresource.h +++ b/src/game/client/c_playerresource.h @@ -18,6 +18,7 @@ #ifdef NEO #include "neo_player_shared.h" +#include "neo_crosshair.h" #endif #define PLAYER_UNCONNECTED_NAME "unconnected" @@ -61,6 +62,8 @@ public : // IGameResources interface const char *GetClanTag(int index); virtual int GetMaxHealth(int index); virtual int GetDisplayedHealth(int index, int mode); + const char *GetNeoCrosshair(int index); + bool IsReady(int index); #endif virtual int GetFrags( int index ); virtual int GetHealth( int index ); @@ -89,6 +92,8 @@ public : // IGameResources interface int m_iStar[MAX_PLAYERS_ARRAY_SAFE]; char m_szNeoClantag[MAX_PLAYERS_ARRAY_SAFE][NEO_MAX_CLANTAG_LENGTH]; int m_iMaxHealth[MAX_PLAYERS_ARRAY_SAFE]; + char m_szNeoCrosshair[MAX_PLAYERS_ARRAY_SAFE][NEO_XHAIR_SEQMAX]; + bool m_bReady[MAX_PLAYERS_ARRAY_SAFE]; #endif int m_iScore[MAX_PLAYERS_ARRAY_SAFE]; int m_iDeaths[MAX_PLAYERS_ARRAY_SAFE]; diff --git a/src/game/client/clientmode_shared.cpp b/src/game/client/clientmode_shared.cpp index 0faffb760..45412ffba 100644 --- a/src/game/client/clientmode_shared.cpp +++ b/src/game/client/clientmode_shared.cpp @@ -72,6 +72,7 @@ extern ConVar replay_rendersetting_renderglow; #include #include "ui/neo_loading.h" #include "neo_gamerules.h" +#include "ui/neoui_scoreboard.h" #endif #ifdef GLOWS_ENABLE @@ -782,6 +783,17 @@ int ClientModeShared::KeyInput( int down, ButtonCode_t keynum, const char *pszCu } #endif +#ifdef NEO + // Scoreboard right-click mouse capture, higher precedence than spectator + // mouse clicks + if (gViewPortInterface && g_pNeoUIScoreBoard && g_pNeoUIScoreBoard->IsVisible() + && down && MOUSE_RIGHT == keynum && false == g_pNeoUIScoreBoard->IsMouseInputEnabled()) + { + g_pNeoUIScoreBoard->ToggleMouseCapture(true); + return 0; + } +#endif // NEO + C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer(); // if ingame spectator mode, let spectator input intercept key event here diff --git a/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp b/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp index c756774d0..a72f6d453 100644 --- a/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp +++ b/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp @@ -18,7 +18,7 @@ #ifdef NEO #include "prediction.h" - #include "neo/ui/neo_scoreboard.h" + #include "neo/ui/neoui_scoreboard.h" extern ConVar v_viewmodel_fov; #else #include "hl2mpclientscoreboard.h" @@ -94,7 +94,7 @@ IViewPortPanel* CHudViewport::CreatePanelByName(const char *szPanelName) if (Q_strcmp(PANEL_SCOREBOARD, szPanelName) == 0) { #ifdef NEO - newpanel = new CNEOScoreBoard(this); + newpanel = new CNEOUIScoreBoard(this); #else newpanel = new CHL2MPClientScoreBoardDialog( this ); #endif diff --git a/src/game/client/in_main.cpp b/src/game/client/in_main.cpp index beb253d24..90b8ea1ac 100644 --- a/src/game/client/in_main.cpp +++ b/src/game/client/in_main.cpp @@ -47,6 +47,10 @@ extern ConVar cam_idealyaw; // Need this for steam controller #include "clientsteamcontext.h" +#ifdef NEO +#include "ui/neoui_scoreboard.h" +#endif // NEO + // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -764,6 +768,12 @@ void IN_ScoreDown( const CCommand &args ) KeyDown( &in_score, args[1] ); if ( gViewPortInterface ) { +#ifdef NEO + if (g_pNeoUIScoreBoard) + { + g_pNeoUIScoreBoard->ToggleMouseCapture(false); + } +#endif // NEO gViewPortInterface->ShowPanel( PANEL_SCOREBOARD, true ); } } @@ -773,6 +783,12 @@ void IN_ScoreUp( const CCommand &args ) KeyUp( &in_score, args[1] ); if ( gViewPortInterface ) { +#ifdef NEO + if (g_pNeoUIScoreBoard) + { + g_pNeoUIScoreBoard->ToggleMouseCapture(false); + } +#endif // NEO gViewPortInterface->ShowPanel( PANEL_SCOREBOARD, false ); GetClientVoiceMgr()->StopSquelchMode(); } diff --git a/src/game/client/neo/ui/neo_hud_deathnotice.cpp b/src/game/client/neo/ui/neo_hud_deathnotice.cpp index eaee7c988..c542ce640 100644 --- a/src/game/client/neo/ui/neo_hud_deathnotice.cpp +++ b/src/game/client/neo/ui/neo_hud_deathnotice.cpp @@ -23,7 +23,7 @@ #include "spectatorgui.h" #include "takedamageinfo.h" #include "c_neo_killer_damage_infos.h" -#include "neo_scoreboard.h" +#include "neoui_scoreboard.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -227,7 +227,7 @@ void CNEOHud_DeathNotice::VidInit( void ) //----------------------------------------------------------------------------- bool CNEOHud_DeathNotice::ShouldDraw( void ) { - return ( CHudElement::ShouldDraw() && ( m_DeathNotices.Count() ) && ( !cl_neo_hud_scoreboard_hide_others.GetBool() || !g_pNeoScoreBoard->IsVisible() ) ); + return ( CHudElement::ShouldDraw() && ( m_DeathNotices.Count() ) && ( !cl_neo_hud_scoreboard_hide_others.GetBool() || !g_pNeoUIScoreBoard->IsVisible() ) ); } //----------------------------------------------------------------------------- diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index aec87e717..385d2bc97 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -19,7 +19,7 @@ #include "c_team.h" #include "c_playerresource.h" #include "vgui_avatarimage.h" -#include "neo_scoreboard.h" +#include "neoui_scoreboard.h" #include "hltvcamera.h" @@ -34,7 +34,7 @@ NEO_HUD_ELEMENT_DECLARE_FREQ_CVAR(RoundState, 0.1) ConVar cl_neo_hud_team_swap_sides("cl_neo_hud_team_swap_sides", "1", FCVAR_ARCHIVE, "Make the team of the local player always appear on the left side of the round info and scoreboard", true, 0.0, true, 1.0, []([[maybe_unused]] IConVar* var, [[maybe_unused]] const char* pOldValue, [[maybe_unused]] float flOldValue) { - g_pNeoScoreBoard->UpdateTeamColumnsPosition(GetLocalPlayerTeam()); + // TODO REMOVE? g_pNeoUIScoreBoard->UpdateTeamColumnsPosition(GetLocalPlayerTeam()); }); ConVar cl_neo_squad_hud_original("cl_neo_squad_hud_original", "1", FCVAR_ARCHIVE, "Use the old squad HUD", true, 0.0, true, 1.0); ConVar cl_neo_squad_hud_star_scale("cl_neo_squad_hud_star_scale", "0", FCVAR_ARCHIVE, "Scaling to apply from 1080p, 0 disables scaling", @@ -283,7 +283,7 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() { m_pWszStatusUnicode = L"Match point"; } - else if (NEORules()->GetRoundStatus() != NeoRoundStatus::Pause && (NEORules()->IsRoundPreRoundFreeze() || g_pNeoScoreBoard->IsVisible())) + else if (NEORules()->GetRoundStatus() != NeoRoundStatus::Pause && (NEORules()->IsRoundPreRoundFreeze() || g_pNeoUIScoreBoard->IsVisible())) { // Update Objective switch (NEORules()->GetGameType()) { @@ -734,7 +734,7 @@ void CNEOHud_RoundState::DrawPlayerList() offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; } - const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); + const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoUIScoreBoard->IsVisible(); // Draw squad mates if (!localPlayerSpec && g_PR->GetStar(localPlayerIndex) != STAR_NONE && !hideDueToScoreboard) @@ -836,7 +836,7 @@ void CNEOHud_RoundState::DrawPlayerList_BotCmdr() offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; } - const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); + const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoUIScoreBoard->IsVisible(); // Draw squad mates m_commandedList.RemoveAll(); @@ -1249,7 +1249,7 @@ void CNEOHud_RoundState::CheckActiveStar() void CNEOHud_RoundState::SetTextureToAvatar(int playerIndex) { - if (!g_pNeoScoreBoard) + if (!g_pNeoUIScoreBoard) { return; } @@ -1267,11 +1267,11 @@ void CNEOHud_RoundState::SetTextureToAvatar(int playerIndex) return; CSteamID steamIDForPlayer(pi.friendsID, 1, steamapicontext->SteamUtils()->GetConnectedUniverse(), k_EAccountTypeIndividual); - const int mapIndex = g_pNeoScoreBoard->m_mapAvatarsToImageList.Find(steamIDForPlayer); - if ((mapIndex == g_pNeoScoreBoard->m_mapAvatarsToImageList.InvalidIndex())) + const int mapIndex = g_pNeoUIScoreBoard->m_mapAvatarsToImageList.Find(steamIDForPlayer); + if ((mapIndex == g_pNeoUIScoreBoard->m_mapAvatarsToImageList.InvalidIndex())) return; - CAvatarImage* pAvIm = (CAvatarImage*)g_pNeoScoreBoard->m_pImageList->GetImage(g_pNeoScoreBoard->m_mapAvatarsToImageList[mapIndex]); + CAvatarImage* pAvIm = (CAvatarImage*)g_pNeoUIScoreBoard->m_pImageList->GetImage(g_pNeoUIScoreBoard->m_mapAvatarsToImageList[mapIndex].i32Idx); surface()->DrawSetTexture(pAvIm->getTextureID()); surface()->DrawSetColor(COLOR_WHITE); } @@ -1544,4 +1544,4 @@ CON_COMMAND_F( spectate_player_selected_in_hud, "Spectate entity selected in the { engine->IsHLTV() ? HLTVCamera()->SetPrimaryTarget(entityIndex) : engine->ClientCmd(VarArgs("spec_player_entity_number %d", entityIndex)); } -} \ No newline at end of file +} diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index ff9a1a5c0..2f1110741 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -342,7 +342,6 @@ constexpr const char *BTNS_LOCALIZE[MMBTN__TOTAL] = { "#GameUI_GameMenu_FindServers", "#GameUI_GameMenu_CreateServer", "#GameUI_GameMenu_Disconnect", - "#GameUI_GameMenu_PlayerList", "#GameUI_GameMenu_Tutorial", "#GameUI_GameMenu_FiringRange", "#GameUI_GameMenu_Options", @@ -753,7 +752,6 @@ void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) &CNeoRoot::MainLoopMapList, // STATE_MAPLIST &CNeoRoot::MainLoopServerDetails, // STATE_SERVERDETAILS - &CNeoRoot::MainLoopPlayerList, // STATE_PLAYERLIST &CNeoRoot::MainLoopSprayPicker, // STATE_SPRAYPICKER &CNeoRoot::MainLoopSprayPicker, // STATE_SPRAYDELETER @@ -927,10 +925,6 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) m_state = STATE_ROOT; GetGameUI()->SendMainMenuCommand("Disconnect"); } - if (NeoUI::Button(m_wszCachedTexts[MMBTN_PLAYERLIST]).bPressed) - { - m_state = STATE_PLAYERLIST; - } NeoUI::Pad(); } if (NeoUI::Button(m_wszCachedTexts[MMBTN_FINDSERVER]).bPressed) @@ -1671,7 +1665,7 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::OpenPopup(NEOPOPUP_ACTIONBLACKLIST, NeoUI::Dim{ .x = g_uiCtx.iMouseAbsX, .y = g_uiCtx.iMouseAbsY, - .wide = NeoUI::PopupWideByStr("Remove from blacklist"), + .wide = NeoUI::SuitableWideByWStr(L"Remove from blacklist", NeoUI::SUITABLEWIDE_POPUP), .tall = g_uiCtx.layout.iDefRowTall, }); } @@ -1867,7 +1861,7 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::OpenPopup(NEOPOPUP_ACTIONSERVER, NeoUI::Dim{ .x = g_uiCtx.iMouseAbsX, .y = g_uiCtx.iMouseAbsY, - .wide = NeoUI::PopupWideByStr("Add to blacklist"), + .wide = NeoUI::SuitableWideByWStr(L"Add to blacklist", NeoUI::SUITABLEWIDE_POPUP), .tall = g_uiCtx.layout.iDefRowTall * 2, }); @@ -2595,68 +2589,6 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) } } -void CNeoRoot::MainLoopPlayerList(const MainLoopParam param) -{ - const int iTallTotal = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 2); - g_uiCtx.dPanel.wide = g_iRootSubPanelWide; - g_uiCtx.dPanel.x = (param.wide / 2) - (g_iRootSubPanelWide / 2); - g_uiCtx.dPanel.y = (param.tall / 2) - (iTallTotal / 2); - g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 1); - NeoUI::BeginContext(&g_uiCtx, param.eMode, L"Player list", "CtxPlayerList"); - if (IsInGame()) - { - g_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; - { - NeoUI::BeginSection(NeoUI::SECTIONFLAG_DEFAULTFOCUS); - { - g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; - for (int i = 1; i <= gpGlobals->maxClients; i++) - { - if (!g_PR->IsConnected(i) || g_PR->IsHLTV(i) || g_PR->IsFakePlayer(i)) - { - continue; - } - - const bool bOwnLocalPlayer = g_PR->IsLocalPlayer(i); - const bool bPlayerMuted = GetClientVoiceMgr()->IsPlayerBlocked(i); - const char *szPlayerName = g_PR->GetPlayerName(i); - wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH + 1]; - g_pVGuiLocalize->ConvertANSIToUnicode(szPlayerName, wszPlayerName, sizeof(wszPlayerName)); - - wchar_t wszInfo[256]; - V_swprintf_safe(wszInfo, L"%ls%ls", bOwnLocalPlayer ? L"[LOCAL] " : bPlayerMuted ? L"[MUTED] " : L"[VOICE] ", wszPlayerName); - if (NeoUI::Button(wszInfo).bPressed) - { - if (!bOwnLocalPlayer) GetClientVoiceMgr()->SetPlayerBlockedState(i, !bPlayerMuted); - } - } - g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; - } - NeoUI::EndSection(); - g_uiCtx.dPanel.y += g_uiCtx.dPanel.tall; - g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall; - NeoUI::BeginSection(NeoUI::SECTIONFLAG_ROWWIDGETS | NeoUI::SECTIONFLAG_EXCLUDECONTROLLER); - { - NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); - g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; - NeoUI::SetPerRowLayout(5); - { - if (NeoUI::Button(NeoUI::HintAlt(L"Back (ESC)", L"Back (B)")).bPressed || NeoUI::BindKeyBack()) - { - m_state = STATE_ROOT; - } - } - NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); - } - NeoUI::EndSection(); - } - } - else - { - m_state = STATE_ROOT; - } -} - void CNeoRoot::MainLoopPopup(const MainLoopParam param) { surface()->DrawSetColor(COLOR_BLACK_TRANSPARENT); diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index faa81859e..db4ff40d3 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -77,7 +77,6 @@ enum RootState STATE__SUBSTATES, STATE_MAPLIST = STATE__SUBSTATES, STATE_SERVERDETAILS, - STATE_PLAYERLIST, STATE_SPRAYPICKER, STATE_SPRAYDELETER, @@ -100,7 +99,6 @@ enum MainMenuButtons MMBTN_FINDSERVER, MMBTN_CREATESERVER, MMBTN_DISCONNECT, - MMBTN_PLAYERLIST, MMBTN_TUTORIAL, MMBTN_FIRINGRANGE, MMBTN_OPTIONS, @@ -167,7 +165,6 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener void MainLoopCredits(const MainLoopParam param); void MainLoopMapList(const MainLoopParam param); void MainLoopServerDetails(const MainLoopParam param); - void MainLoopPlayerList(const MainLoopParam param); void MainLoopSprayPicker(const MainLoopParam param); void MainLoopPopup(const MainLoopParam param); diff --git a/src/game/client/neo/ui/neo_scoreboard.cpp b/src/game/client/neo/ui/neo_scoreboard.cpp index 9067fb88b..2358bafc6 100644 --- a/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/src/game/client/neo/ui/neo_scoreboard.cpp @@ -48,8 +48,7 @@ using namespace vgui; #define SHOW_ENEMY_STATUS true -ConVar cl_neo_hud_scoreboard_hide_others("cl_neo_hud_scoreboard_hide_others", "1", FCVAR_ARCHIVE, "Hide some other HUD elements when the scoreboard is displayed to prevent overlap", true, 0.0, true, 1.0); -ConVar neo_show_scoreboard_avatars("neo_show_scoreboard_avatars", "1", FCVAR_ARCHIVE, "Show avatars on scoreboard.", true, 0.0, true, 1.0 ); +extern ConVar neo_show_scoreboard_avatars; extern ConVar cl_neo_streamermode; extern ConVar cl_neo_hud_team_swap_sides; @@ -930,4 +929,4 @@ void CNEOScoreBoard::UpdateTeamColumnsPosition(int team) m_pJinraiPlayerList->SetPos(m_iLeftTeamXPos, m_iLeftTeamYPos); m_pNSFPlayerList->SetPos(m_iRightTeamXPos, m_iRightTeamYPos); } -} \ No newline at end of file +} diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index a06b4baf0..d2a233f02 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -294,6 +294,7 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar c->eLabelTextStyle = TEXTSTYLE_LEFT; c->ibfSectionCanActive = 0; c->ibfSectionCanController = 0; + c->popupFlags &= ~(POPUPFLAG__CTXDONEPOPUP); // Different pointer, change context c->bFirstCtxUse = (c->pSzCurCtxName != pSzCtxName); if (c->bFirstCtxUse) @@ -498,17 +499,20 @@ void EndContext() void BeginSection(const ISectionFlags iSectionFlags) { - // Previous frame(s) known this section does scroll - if (c->ibfSectionHasYScroll & (1ULL << c->iSection)) + if (!(iSectionFlags & SECTIONFLAG_DISABLEOFFSETS)) { - // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection - // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" - // without extra variable - c->dPanel.wide -= NEOUI_SCROLL_THICKNESS(); - } - if (c->ibfSectionHasXScroll & (1ULL << c->iSection)) - { - c->dPanel.tall -= NEOUI_SCROLL_THICKNESS(); + // Previous frame(s) known this section does scroll + if (c->ibfSectionHasYScroll & (1ULL << c->iSection)) + { + // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection + // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" + // without extra variable + c->dPanel.wide -= NEOUI_SCROLL_THICKNESS(); + } + if (c->ibfSectionHasXScroll & (1ULL << c->iSection)) + { + c->dPanel.tall -= NEOUI_SCROLL_THICKNESS(); + } } c->iLayoutX = -c->iXOffset[c->iSection]; @@ -583,6 +587,13 @@ void EndSection() c->iActive = FOCUSOFF_NUM; c->iActiveSection = -1; } + const bool bOffsetDisabled = (c->iSectionFlags & SECTIONFLAG_DISABLEOFFSETS); + if (bOffsetDisabled) + { + c->iXOffset[c->iSection] = 0; + c->iYOffset[c->iSection] = 0; + } + const int iAbsLayoutX = c->irWidgetMaxX + c->iXOffset[c->iSection]; const int iAbsLayoutY = c->irWidgetLayoutY + c->irWidgetTall + c->iYOffset[c->iSection]; c->wdgInfos[c->iWidget].iXOffsets = iAbsLayoutX; @@ -590,8 +601,8 @@ void EndSection() // Scroll handling const int iMWheelJump = c->layout.iDefRowTall; - const bool bHasXScroll = (iAbsLayoutX > c->dPanel.wide); - const bool bHasYScroll = (iAbsLayoutY > c->dPanel.tall); + const bool bHasXScroll = (false == bOffsetDisabled) && (iAbsLayoutX > c->dPanel.wide); + const bool bHasYScroll = (false == bOffsetDisabled) && (iAbsLayoutY > c->dPanel.tall); const int iScrollThick = NEOUI_SCROLL_THICKNESS(); const bool bResetXScrollPanelWide = (c->ibfSectionHasXScroll & (1ULL << c->iSection)); const bool bResetYScrollPanelTall = (c->ibfSectionHasYScroll & (1ULL << c->iSection)); @@ -836,16 +847,19 @@ void EndSection() } } - // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection - // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" - // without extra variable - if (bResetXScrollPanelWide) - { - c->dPanel.tall += iScrollThick; - } - if (bResetYScrollPanelTall) + if (false == bOffsetDisabled) { - c->dPanel.wide += iScrollThick; + // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection + // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" + // without extra variable + if (bResetXScrollPanelWide) + { + c->dPanel.tall += iScrollThick; + } + if (bResetYScrollPanelTall) + { + c->dPanel.wide += iScrollThick; + } } ++c->iSection; @@ -885,7 +899,7 @@ void ClosePopup() bool BeginPopup(const int iPopupId, const PopupFlags flags) { - if (iPopupId != c->iCurPopupId) + if (iPopupId != c->iCurPopupId || (c->popupFlags & POPUPFLAG__CTXDONEPOPUP)) { return false; } @@ -902,7 +916,7 @@ bool BeginPopup(const int iPopupId, const PopupFlags flags) } c->popupFlags &= ~(POPUPFLAG__EXTERNAL); - c->popupFlags |= (flags & POPUPFLAG__EXTERNAL); + c->popupFlags |= (flags & POPUPFLAG__EXTERNAL) | POPUPFLAG__CTXDONEPOPUP; V_memcpy(&c->dPanel, &dim, sizeof(Dim)); BeginSection(SECTIONFLAG_POPUP); @@ -920,21 +934,28 @@ int CurrentPopup() return c->iCurPopupId; } -static int BasePopupWideByStr(const int iSzSize) +int SuitableWideByWStr(const wchar_t *pwszStr, const ESuitableWide eWideType) { const auto *pFontI = &c->fonts[c->eFont]; - const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); - return (c->iMarginX * 2) + (iSzSize * iChWidth); -} - -int PopupWideByStr(const char *pszStr) -{ - return BasePopupWideByStr(V_strlen(pszStr)); -} - -int PopupWideByStr(const wchar_t *pwszStr) -{ - return BasePopupWideByStr(V_wcslen(pwszStr)); + switch (eWideType) + { + case SUITABLEWIDE_POPUP: + { + // Rough-estimate, suitable for popup menus + const int iWszSize = V_wcslen(pwszStr); + const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); + return (c->iMarginX * 2) + (iWszSize * iChWidth); + } break; + case SUITABLEWIDE_TABLE: + { + // More precise wide, suitable for fixed-tables + [[maybe_unused]] int iWide = 0, iTall = 0; + vgui::surface()->GetTextSize(pFontI->hdl, pwszStr, iWide, iTall); + return (c->iMarginX * 2) + iWide; + } break; + } + Assert(false); // Should not be able to get here + return 0; } void SetPerRowLayout(const int iColTotal, const int *iColProportions, const int iRowTall) @@ -1445,7 +1466,9 @@ void Label(const wchar_t *wszLabel, const wchar_t *wszText) Label(wszText); } -NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const EBaseButtonType eType, const bool bVal, const ButtonFlags flags, const float flScrollStart) +NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, + const char *szTextureGroup, const EBaseButtonType eType, const bool bVal, + const ButtonFlags flags, const float flScrollStart) { const auto wdgState = BeginWidget(WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE); @@ -1532,8 +1555,28 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c { vgui::surface()->DrawFilledRect(c->rWidgetArea.x0, c->rWidgetArea.y0, c->rWidgetArea.x1, c->rWidgetArea.y1); - Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, - c->irWidgetWide, c->irWidgetTall); + if (wszText && wszText[0]) + { + // NEO TODO (nullsystem): Currently only top-center texture, + // bottom-center label style but if wanted can tweak texture-label styling + const int iTexYTall = c->irWidgetTall * 0.75f; + Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, + c->irWidgetWide, iTexYTall, + szTextureGroup); + + const auto *pFontI = &c->fonts[c->eFont]; + const int x = XPosFromText(wszText, pFontI, TEXTSTYLE_CENTER); + vgui::surface()->DrawSetTextPos( + c->rWidgetArea.x0 + x, + c->rWidgetArea.y0 + iTexYTall - pFontI->iYFontOffset); + vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + } + else + { + Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, + c->irWidgetWide, c->irWidgetTall, + szTextureGroup); + } } break; case BASEBUTTONTYPE_CHECKBOX: { @@ -1609,7 +1652,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c NeoUI::RetButton Button(const wchar_t *wszText) { - return BaseButton(wszText, "", BASEBUTTONTYPE_TEXT); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_TEXT); } NeoUI::RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText) @@ -1651,7 +1694,22 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt { // General images decoded via stb_image int width, height, channels; - uint8 *data = stbi_load(szTexturePath, &width, &height, &channels, 0); + char szFullTexturePath[MAX_PATH] = {}; +#ifdef _WIN32 + if (V_isalpha(szTexturePath[0]) + && szTexturePath[1] == ':' + && (szTexturePath[2] == '\\' || szTexturePath[2] == '/')) +#else + if (szTexturePath[0] == '/') +#endif + { + V_strcpy_safe(szFullTexturePath, szTexturePath); + } + else + { + filesystem->RelativePathToFullPath_safe(szTexturePath, szTextureGroup, szFullTexturePath); + } + uint8 *data = stbi_load(szFullTexturePath, &width, &height, &channels, 0); if (data) { if (channels == 3) @@ -1775,19 +1833,20 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt return false; } -NeoUI::RetButton ButtonTexture(const char *szTexturePath) +NeoUI::RetButton ButtonTexture(const char *szTexturePath, const char *szTextureGroup, + const wchar_t *wszText) { - return BaseButton(L"", szTexturePath, BASEBUTTONTYPE_IMAGE); + return BaseButton(wszText, szTexturePath, szTextureGroup, BASEBUTTONTYPE_IMAGE); } NeoUI::RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal) { - return BaseButton(wszText, "", BASEBUTTONTYPE_CHECKBOX, bVal); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_CHECKBOX, bVal); } NeoUI::RetButton ButtonToggle(const wchar_t *wszText, const bool bVal, const ButtonFlags flags, const float flScrollStart) { - return BaseButton(wszText, "", BASEBUTTONTYPE_TOGGLE, bVal, flags, flScrollStart); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_TOGGLE, bVal, flags, flScrollStart); } void ResetTextures() @@ -2814,7 +2873,7 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f OpenPopup(INTERNALPOPUP_COPYMENU, Dim{ .x = c->iMouseAbsX, .y = c->iMouseAbsY, - .wide = PopupWideByStr("Paste"), + .wide = SuitableWideByWStr(L"Paste", SUITABLEWIDE_POPUP), .tall = c->layout.iDefRowTall * 3, }); c->eRightClickCopyMenuRet = COPYMENU_NIL; @@ -3268,8 +3327,8 @@ TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iCols OpenPopup(INTERNALPOPUP_TABLEHEADER, Dim{ .x = c->iMouseAbsX, .y = c->iMouseAbsY, - .wide = NeoUI::PopupWideByStr("__") // Offset by checkmark - + NeoUI::PopupWideByStr(wszColNamesList[iWidestIdx]), + .wide = NeoUI::SuitableWideByWStr(L"__", NeoUI::SUITABLEWIDE_POPUP) // Offset by checkmark + + NeoUI::SuitableWideByWStr(wszColNamesList[iWidestIdx], NeoUI::SUITABLEWIDE_POPUP), .tall = c->layout.iDefRowTall * iColsTotal, }); } @@ -3327,6 +3386,27 @@ TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iCols return modFlags; } +// NEO NOTE (nullsystem): It's done like this so that the highlighter +// border goes over rather than under the content of the row +static void TableHighlightPrevRow() +{ + if (c->curRowFlags & NEXTTABLEROWFLAG__HOT && MODE_PAINT == c->eMode) + { + vgui::IntRect rRowArea = { + .x0 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection], + .y0 = c->dPanel.y + c->iLayoutY - c->layout.iRowTall, + .x1 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection] + c->dPanel.wide, + .y1 = c->dPanel.y + c->iLayoutY, + }; + const bool bFullyVisible = (rRowArea.y0 >= c->dPanel.y) + && (rRowArea.y1 <= (c->dPanel.y + c->dPanel.tall)); + if (bFullyVisible) + { + DrawBorder(rRowArea); + } + } +} + void BeginTable(const int *piColsWide, const int iLabelsSize) { // Bump y-axis with previous row layout before applying table layout @@ -3353,6 +3433,14 @@ void BeginTable(const int *piColsWide, const int iLabelsSize) NeoUI::RetButton EndTable() { + if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) + { + c->iLayoutX = -c->iXOffset[c->iSection]; + c->iLayoutY += c->layout.iRowTall; + c->irWidgetLayoutY = c->iLayoutY; + } + TableHighlightPrevRow(); + RetButton ret = {}; if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) @@ -3393,6 +3481,7 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) c->iLayoutY += c->layout.iRowTall; c->irWidgetLayoutY = c->iLayoutY; } + TableHighlightPrevRow(); RetButton ret = {}; c->curRowFlags = ((flags & NEXTTABLEROWFLAG__EXTERNAL) | (c->curRowFlags & NEXTTABLEROWFLAG__PERSISTS)); @@ -3419,6 +3508,13 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) { bMouseIn = IN_BETWEEN_EQ(rRowArea.x0, c->iMouseAbsX, rRowArea.x1 - 1) && IN_BETWEEN_EQ(rRowArea.y0, c->iMouseAbsY, rRowArea.y1 - 1); + if (bMouseIn && (c->dimPopup.wide > 0 && c->dimPopup.tall > 0) && + !(c->popupFlags & POPUPFLAG__INPOPUPSECTION)) + { + const Dim &dim = c->dimPopup; + bMouseIn = !(IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) && + IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall)); + } if (bMouseIn) { c->curRowFlags |= NEXTTABLEROWFLAG__HOT; @@ -3447,11 +3543,6 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) vgui::surface()->DrawSetColor(color); vgui::surface()->DrawFilledRectArray(&rRowArea, 1); } - - if (c->curRowFlags & NEXTTABLEROWFLAG__HOT) - { - DrawBorder(rRowArea); - } } } break; diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index caf6188a6..2f25cf0eb 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -192,6 +192,8 @@ enum ESectionFlag SECTIONFLAG_POPUP = 1 << 4, // Don't restrict viewport to only label's area SECTIONFLAG_LABELPANELVIEWPORT = 1 << 5, + // Disable X and Y axis offsets + SECTIONFLAG_DISABLEOFFSETS = 1 << 6, }; typedef int ISectionFlags; @@ -221,6 +223,7 @@ enum PopupFlag_ POPUPFLAG__EXTERNAL = ((1 << 8) - 1), // Mask of all external/options flags below start of internal POPUPFLAG__INPOPUPSECTION = 1 << 8, // Inside a Begin/EndPopup section POPUPFLAG__NEWOPENPOPUP = 1 << 9, // The popup just initialized + POPUPFLAG__CTXDONEPOPUP = 1 << 10, // Popup have been shown in this context, it's so OpenPopup to another one won't just immediately close }; typedef int PopupFlags; @@ -517,8 +520,12 @@ void EndPopup(); [[nodiscard]] int CurrentPopup(); // Get a suitable wide size for a popup by the longest text in the popup -int PopupWideByStr(const char *pszStr); -int PopupWideByStr(const wchar_t *pwszStr); +enum ESuitableWide +{ + SUITABLEWIDE_POPUP = 0, + SUITABLEWIDE_TABLE, +}; +int SuitableWideByWStr(const wchar_t *pwszStr, const ESuitableWide eWideType); [[nodiscard]] CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag = WIDGETFLAG_NONE); void EndWidget(const CurrentWidgetState &wdgState); @@ -562,11 +569,12 @@ struct TabsState /*1W*/ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, const TabsFlags flags = TABFLAG_DEFAULT, TabsState *pState = nullptr); -/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, +/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const char *szTextureGroup, const EBaseButtonType eType, const bool bVal = false, const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f); /*1W*/ RetButton Button(const wchar_t *wszText); /*2W*/ RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText); -/*1W*/ RetButton ButtonTexture(const char *szTexturePath); +/*1W*/ RetButton ButtonTexture(const char *szTexturePath, const char *szTextureGroup = "", + const wchar_t *wszText = L""); /*1W*/ RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal); /*1W*/ RetButton ButtonToggle(const wchar_t *wszText, const bool bVal, const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f); /*1W*/ void RingBoxFlag(const int iToggleFlag, int *iFlags, const wchar_t **wszLabelsCustomList = nullptr); diff --git a/src/game/client/neo/ui/neoui_scoreboard.cpp b/src/game/client/neo/ui/neoui_scoreboard.cpp new file mode 100644 index 000000000..381963230 --- /dev/null +++ b/src/game/client/neo/ui/neoui_scoreboard.cpp @@ -0,0 +1,1177 @@ +#include "neoui_scoreboard.h" + +#include "c_neo_player.h" +#include "neo_gamerules.h" +#include "neo_theme.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar cl_neo_hud_scoreboard_hide_others("cl_neo_hud_scoreboard_hide_others", "1", FCVAR_ARCHIVE, "Hide some other HUD elements when the scoreboard is displayed to prevent overlap", true, 0.0, true, 1.0); +ConVar neo_show_scoreboard_avatars("neo_show_scoreboard_avatars", "1", FCVAR_ARCHIVE, "Show avatars on scoreboard.", true, 0.0, true, 1.0 ); +extern ConVar cl_neo_streamermode; +extern ConVar cl_neo_squad_hud_original; +extern ConVar sv_neo_readyup_lobby; +extern ConVar cl_neo_hud_team_swap_sides; + +// NEO TODO (nullsystem): +// - [TODO] (As separate PR) Replaces current damage info HUD +// - [CURRENT] Right-click popup to look acceptable to use +// - [DONE] cl_neo_hud_team_swap_sides +// - [DONE] Complements readyup +// - [DONE] Replaces "player-list" mute/unmute toggle + indicator +// - [DONE] Right-click popup steam community profile link + +enum ENeoScoreBoardPopup +{ + NEOSCOREBOARDPOPUP_CARD = NeoUI::INTERNALPOPUP_NIL + 1, + NEOSCOREBOARDPOPUP_COPYCROSSHAIR, +}; + +CNEOUIScoreBoard *g_pNeoUIScoreBoard = nullptr; + +CNEOUIScoreBoard::CNEOUIScoreBoard(IViewPort *pViewPort) + : Panel(nullptr, PANEL_SCOREBOARD) +{ + SetupNTRETheme(&m_uiCtx); + SetProportional(true); + SetKeyBoardInputEnabled(false); + SetMouseInputEnabled(false); + vgui::surface()->CreatePopup(GetVPanel(), false, false, false, true, false); + + SetScheme("ClientScheme"); + + ListenForGameEvent("server_spawn"); + ListenForGameEvent("game_newmap"); + ListenForGameEvent("hltv_status"); + + m_mapAvatarsToImageList.SetLessFunc(DefLessFunc(CSteamID)); + m_mapAvatarsToImageList.RemoveAll(); + + g_pNeoUIScoreBoard = this; +} + +CNEOUIScoreBoard::~CNEOUIScoreBoard() +{ +} + +const char *CNEOUIScoreBoard::GetName() +{ + return PANEL_SCOREBOARD; +} + +void CNEOUIScoreBoard::OnPollHideCode(int code) +{ + m_nCloseKey = static_cast(code); +} + +void CNEOUIScoreBoard::OnThink() +{ + BaseClass::OnThink(); + + // NOTE: this is necessary because of the way input works. + // If a key down message is sent to vgui, then it will get the key up message + // Sometimes the scoreboard is activated by other vgui menus, + // sometimes by console commands. In the case where it's activated by + // other vgui menus, we lose the key up message because this panel + // doesn't accept keyboard input. It *can't* accept keyboard input + // because another feature of the dialog is that if it's triggered + // from within the game, you should be able to still run around while + // the scoreboard is up. That feature is impossible if this panel accepts input. + // because if a vgui panel is up that accepts input, it prevents the engine from + // receiving that input. So, I'm stuck with a polling solution. + // + // Close key is set to non-invalid when something other than a keybind + // brings the scoreboard up, and it's set to invalid as soon as the + // dialog becomes hidden. + if (m_nCloseKey != BUTTON_CODE_INVALID + && !g_pInputSystem->IsButtonDown(m_nCloseKey)) + { + m_nCloseKey = BUTTON_CODE_INVALID; + gViewPortInterface->ShowPanel(PANEL_SCOREBOARD, false); + GetClientVoiceMgr()->StopSquelchMode(); + } +} + +void CNEOUIScoreBoard::ShowPanel(bool bShow) +{ + if (NEORules() && NEORules()->GetHiddenHudElements() & NEO_HUD_ELEMENT_SCOREBOARD) + { + return; + } + // Catch the case where we call ShowPanel before ApplySchemeSettings, eg when + // going from windowed <-> fullscreen + if (m_pImageList == NULL) + { + InvalidateLayout(true, true); + } + + if (!bShow) + { + m_nCloseKey = BUTTON_CODE_INVALID; + } + + if (BaseClass::IsVisible() == bShow) + { + return; + } + + SetMouseInputEnabled(false); + SetKeyBoardInputEnabled(false); + if (bShow) + { + Reset(); + Update(); + SetVisible(true); + MoveToFront(); + } + else + { + BaseClass::SetVisible(false); + SetMouseInputEnabled(false); + } +} + +void CNEOUIScoreBoard::FireGameEvent(IGameEvent *event) +{ + const char *type = event->GetName(); + if (0 == V_strcmp(type, "hltv_status")) + { + // NEO TODO (nullsystem): Show HLTV spec count? + // spectators = clients - proxies + m_HLTVSpectators = event->GetInt("clients"); + m_HLTVSpectators -= event->GetInt("proxies"); + } + + if (IsVisible()) + { + Update(); + } +} + +void CNEOUIScoreBoard::ApplySchemeSettings(vgui::IScheme *pScheme) +{ + BaseClass::ApplySchemeSettings(pScheme); + + int wide, tall; + vgui::surface()->GetScreenSize(wide, tall); + SetSize(wide, tall); + SetFgColor(COLOR_TRANSPARENT); + SetBgColor(COLOR_TRANSPARENT); + + static constexpr const char *FONT_NAMES[NeoUI::FONT__TOTAL] = { + "HudSelectionText", //"NeoUIScoreboard", + "NHudOCR", + "NHudOCRSmallNoAdditive", + "ClientTitleFont", + "ClientTitleFontSmall", + "NeoUINormal", + }; + for (int i = 0; i < NeoUI::FONT__TOTAL; ++i) + { + m_uiCtx.fonts[i].hdl = pScheme->GetFont(FONT_NAMES[i], true); + } + + m_playerPopup = {}; + m_iTotalPlayers = 0; + if (m_pImageList) + { + delete m_pImageList; + } + m_pImageList = new vgui::ImageList(false); + m_mapAvatarsToImageList.RemoveAll(); + + V_memset(m_iColsWidePlayersList, 0, sizeof(m_iColsWidePlayersList)); + V_memset(m_iColsWideNonPlayersList, 0, sizeof(m_iColsWideNonPlayersList)); + + m_flNextUpdateTime = gpGlobals->curtime + 0.1f; + + NeoUI::ClosePopup(); +} + +void CNEOUIScoreBoard::Paint() +{ + OnMainLoop(NeoUI::MODE_PAINT); +} + +void CNEOUIScoreBoard::OnMousePressed(vgui::MouseCode code) +{ + if (IsMouseInputEnabled()) + { + m_uiCtx.eCode = code; + OnMainLoop(NeoUI::MODE_MOUSEPRESSED); + } +} + +void CNEOUIScoreBoard::OnMouseReleased(vgui::MouseCode code) +{ + if (IsMouseInputEnabled()) + { + m_uiCtx.eCode = code; + OnMainLoop(NeoUI::MODE_MOUSERELEASED); + } +} + +void CNEOUIScoreBoard::OnMouseDoublePressed(vgui::MouseCode code) +{ + if (IsMouseInputEnabled()) + { + m_uiCtx.eCode = code; + OnMainLoop(NeoUI::MODE_MOUSEDOUBLEPRESSED); + } +} + +void CNEOUIScoreBoard::OnCursorMoved(int x, int y) +{ + if (IsMouseInputEnabled()) + { + m_uiCtx.iMouseAbsX = x; + m_uiCtx.iMouseAbsY = y; + OnMainLoop(NeoUI::MODE_MOUSEMOVED); + } +} + +void CNEOUIScoreBoard::OnKeyCodePressed([[maybe_unused]] vgui::KeyCode code) +{ + // no-op +} + +void CNEOUIScoreBoard::OnKeyCodeReleased([[maybe_unused]] vgui::KeyCode code) +{ + // no-op +} + +void CNEOUIScoreBoard::Reset() +{ + m_iTotalPlayers = 0; + V_memset(m_playersInfo, 0, sizeof(m_playersInfo)); + m_flNextUpdateTime = 0; + m_mapAvatarsToImageList.RemoveAll(); +} + +void CNEOUIScoreBoard::ToggleMouseCapture(const bool bUseMouse) +{ + SetMouseInputEnabled(bUseMouse); + m_uiCtx.iHotPersist = m_uiCtx.iActive = m_uiCtx.iHot = NeoUI::FOCUSOFF_NUM; + m_uiCtx.iHotPersistSection = m_uiCtx.iActiveSection = m_uiCtx.iHotSection = -1; + NeoUI::ClosePopup(); +} + +bool CNEOUIScoreBoard::NeedsUpdate() +{ + return (m_flNextUpdateTime < gpGlobals->curtime); +} + +void CNEOUIScoreBoard::Update() +{ + if (!g_PR || !NEORules()) + { + return; + } + + C_NEO_Player *pLocalPlayer = C_NEO_Player::GetLocalNEOPlayer(); + const int iLocalPlayerTeam = pLocalPlayer->GetTeamNumber(); + const bool bLocalPlaying = (TEAM_JINRAI == iLocalPlayerTeam || TEAM_NSF == iLocalPlayerTeam); + const bool bNotTeamplay = !NEORules()->IsTeamplay(); + + m_iTotalPlayers = 0; + for (int i = 1; i <= gpGlobals->maxClients; ++i) + { + if (false == g_PR->IsConnected(i)) + { + continue; + } + + C_NEO_Player *pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + C_NEO_Player *pNeoImpersonator = pNeoPlayer + ? ToNEOPlayer(pNeoPlayer->m_hSpectatorTakeoverPlayerImpersonatingMe.Get()) + : nullptr; + + const bool bIsImpersonating = pNeoPlayer + && pNeoPlayer->m_hSpectatorTakeoverPlayerTarget.Get(); + + CNEOUIScoreBoardPlayer *pPlayerInfo = &m_playersInfo[m_iTotalPlayers]; + pPlayerInfo->iUserID = g_PR->GetUserID(i); + pPlayerInfo->iTeam = g_PR->GetTeam(i); + pPlayerInfo->iPing = (g_PR->IsFakePlayer(i)) + ? -1 + : g_PR->GetPing(i); + pPlayerInfo->bBot = g_PR->IsFakePlayer(i); + pPlayerInfo->bMuted = GetClientVoiceMgr()->IsPlayerBlocked(i); + V_strcpy_safe(pPlayerInfo->szCrosshair, g_PR->GetNeoCrosshair(i)); + pPlayerInfo->bReady = g_PR->IsReady(i); + + // pPlayerInfo->steamID + player_info_t pi; + if (SteamUtils() && engine->GetPlayerInfo(i, &pi) && pi.friendsID) + { + pPlayerInfo->steamID = CSteamID{ + pi.friendsID, + 1, + SteamUtils()->GetConnectedUniverse(), + k_EAccountTypeIndividual}; + } + else + { + pPlayerInfo->steamID.Clear(); + } + + const bool bIsPlaying = (TEAM_JINRAI == pPlayerInfo->iTeam || TEAM_NSF == pPlayerInfo->iTeam); + pPlayerInfo->iXP = bIsPlaying ? g_PR->GetXP(i) : 0; + pPlayerInfo->iDeaths = bIsPlaying ? g_PR->GetDeaths(i) : 0; + + // pPlayerInfo->iClass + const bool bShowClass = + (false == bLocalPlaying) // Spectating + || (bNotTeamplay && g_PR->IsLocalPlayer(i)) // DM - Only see own class + || (false == bNotTeamplay && iLocalPlayerTeam == pPlayerInfo->iTeam); // Team - See own team's classes + if (bShowClass) + { + pPlayerInfo->iClass = (bIsImpersonating) + ? pNeoPlayer->m_iClassBeforeTakeover + : g_PR->GetClass(i); + } + else + { + pPlayerInfo->iClass = -1; + } + + // pPlayerInfo->wszName and pPlayerInfo->wszClantag + char szName[MAX_PLAYER_NAME_LENGTH] = {}; + char szClantag[NEO_MAX_CLANTAG_LENGTH] = {}; + if (pNeoImpersonator) + { + UTIL_MakeSafeName( + pNeoImpersonator->GetPlayerNameWithTakeoverContext( + pNeoImpersonator->entindex()) + , szName, ARRAYSIZE(szName)); + } + else + { + const char *pClantag = g_PR->GetClanTag(i); + if (pClantag && pClantag[0] + && (!cl_neo_streamermode.GetBool() || g_PR->IsLocalPlayer(i))) + { + UTIL_MakeSafeName(pClantag, szClantag, ARRAYSIZE(szClantag)); + } + UTIL_MakeSafeName(g_PR->GetPlayerName(i), szName, ARRAYSIZE(szName)); + } + g_pVGuiLocalize->ConvertANSIToUnicode( + szName, pPlayerInfo->wszName, sizeof(pPlayerInfo->wszName)); + g_pVGuiLocalize->ConvertANSIToUnicode( + szClantag, pPlayerInfo->wszClantag, sizeof(pPlayerInfo->wszClantag)); + + // pPlayerInfo->bDead + pPlayerInfo->bDead = (false == g_PR->IsAlive(i)); + if (pPlayerInfo->iTeam == TEAM_JINRAI || pPlayerInfo->iTeam == TEAM_NSF) + { + if (bIsImpersonating) + { + // Former spectators impersonating other players are (un)dead + pPlayerInfo->bDead = true; + } + else if (pNeoImpersonator) + { + // Do not show death icon for players being impersonated + pPlayerInfo->bDead = false; + } + } + + // pPlayerInfo->avatar + if (UpdateAvatars() && pPlayerInfo->steamID.IsValid()) + { + // See if we already have that avatar in our list + int iMapIndex = m_mapAvatarsToImageList.Find(pPlayerInfo->steamID); + if (iMapIndex == m_mapAvatarsToImageList.InvalidIndex()) + { + auto *pImage32 = new CAvatarImage; + auto *pImage64 = new CAvatarImage; + auto *pImage184 = new CAvatarImage; + + pImage32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); + pImage64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); + pImage184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); + + pPlayerInfo->avatar = { + .i32Idx = m_pImageList->AddImage(pImage32), + .i64Idx = m_pImageList->AddImage(pImage64), + .i184Idx = m_pImageList->AddImage(pImage184), + }; + + m_mapAvatarsToImageList.Insert(pPlayerInfo->steamID, pPlayerInfo->avatar); + } + else + { + pPlayerInfo->avatar = m_mapAvatarsToImageList[iMapIndex]; + } + } + else + { + pPlayerInfo->avatar.i32Idx = -1; + pPlayerInfo->avatar.i64Idx = -1; + pPlayerInfo->avatar.i184Idx = -1; + } + + ++m_iTotalPlayers; + } + + // Sort players by XP, if equal by death + V_qsort_s(m_playersInfo, m_iTotalPlayers, sizeof(CNEOUIScoreBoardPlayer), + []([[maybe_unused]] void *vpCtx, const void *vpLeft, const void *vpRight) -> int { + auto *pLeft = static_cast(vpLeft); + auto *pRight = static_cast(vpRight); + if (pLeft->iXP == pRight->iXP) + { + if (pLeft->iDeaths == pRight->iDeaths) + { + // Alphabetical order + return V_wcscmp(pLeft->wszName, pRight->wszName); + } + // More deaths = lower + return pLeft->iDeaths - pRight->iDeaths; + } + // More XP = higher + return pRight->iXP - pLeft->iXP; + }, nullptr); + + // NEO JANK (nullsystem): FireGameEvent is unreliable for fetching + // hostname and current map so just poll it from ConVar/NEORules instead + const ConVarRef cvr_hostname("hostname"); + g_pVGuiLocalize->ConvertANSIToUnicode(cvr_hostname.GetString(), + m_wszHostname, sizeof(m_wszHostname)); + g_pVGuiLocalize->ConvertANSIToUnicode(NEORules()->MapName(), + m_wszMap, sizeof(m_wszMap)); + + m_flNextUpdateTime = gpGlobals->curtime + 1.0f; +} + +bool CNEOUIScoreBoard::ShowAvatars() +{ + return neo_show_scoreboard_avatars.GetBool() && !cl_neo_streamermode.GetBool(); +} + +bool CNEOUIScoreBoard::UpdateAvatars() +{ + return !cl_neo_streamermode.GetBool() && (neo_show_scoreboard_avatars.GetBool() || !cl_neo_squad_hud_original.GetBool()); +} + +void CNEOUIScoreBoard::OnMainLoop(const NeoUI::Mode eMode) +{ + if (!NEORules()) + { + return; + } + + int wide, tall; + vgui::surface()->GetScreenSize(wide, tall); + + // other resolution scales up/down from it + m_uiCtx.layout.iRowTall = m_uiCtx.layout.iDefRowTall = tall / 35; + m_uiCtx.iMarginX = wide / 192 / 2; + m_uiCtx.iMarginY = tall / 108 / 2; + int iWideAs43 = static_cast(tall) * (4.0f / 3.0f); + if (iWideAs43 > wide) iWideAs43 = wide; + const int iRootSubPanelWide = static_cast(iWideAs43 * 0.975f); + const int iPopupCardPerRowTallAvatarName = m_uiCtx.layout.iDefRowTall * 3; + const int iPopupCardPerRowTallButtons = m_uiCtx.layout.iDefRowTall * 2.25f; + const bool bShowReadyUp = sv_neo_readyup_lobby.GetBool() + && NEORules()->m_nRoundStatus == NeoRoundStatus::Idle; + + bool bHasRanklessDog = false; + int iaTeamTally[TEAM__TOTAL] = {}; + int iaTeamReadyTally[TEAM__TOTAL] = {}; + for (int i = 0; i < m_iTotalPlayers; ++i) + { + const int iTeam = m_playersInfo[i].iTeam; + if (IN_BETWEEN_AR(0, iTeam, TEAM__TOTAL)) + { + ++iaTeamTally[iTeam]; + iaTeamReadyTally[iTeam] += m_playersInfo[i].bReady; + + if (iTeam >= FIRST_GAME_TEAM && + m_playersInfo[i].iXP < 0) + { + bHasRanklessDog = true; + } + } + } + + // Reset ranked column size depending if there's + // rankless dog or not + // + // Reset column sizes if wide/tall differs + static bool bPrevHasRanklessDog = false, bPrevShowReadyUp = false; + static int prevWide = 0, prevTall = 0; + if (bPrevHasRanklessDog != bHasRanklessDog + || bPrevShowReadyUp != bShowReadyUp + || wide != prevWide + || tall != prevTall) + { + V_memset(m_iColsWidePlayersList, 0, sizeof(m_iColsWidePlayersList)); + V_memset(m_iColsWideNonPlayersList, 0, sizeof(m_iColsWideNonPlayersList)); + } + bPrevHasRanklessDog = bHasRanklessDog; + bPrevShowReadyUp = bShowReadyUp; + prevWide = wide; + prevTall = tall; + + const int iGap = m_uiCtx.iMarginX; + const bool bNotTeamplay = !NEORules()->IsTeamplay(); + const int iMaxSidePlayers = (bNotTeamplay) + ? Ceil2Int((iaTeamTally[TEAM_JINRAI] + iaTeamTally[TEAM_NSF]) / 2.0f) + : Max(iaTeamTally[TEAM_JINRAI], iaTeamTally[TEAM_NSF]); + C_NEO_Player *pLocalPlayer = C_NEO_Player::GetLocalNEOPlayer(); + const int iLocalUserID = pLocalPlayer->GetUserID(); + const int iLocalPlayerTeam = pLocalPlayer->GetTeamNumber(); + + int iTies = 0; + if (false == bNotTeamplay) + { + auto pTeamJinrai = GetGlobalTeam(TEAM_JINRAI); + auto pTeamNSF = GetGlobalTeam(TEAM_NSF); + if (pTeamJinrai && pTeamNSF) + { + // NEO NOTE (nullsystem): PostRound have GetRoundsWon updated, but not + // roundNumber, so assume ties like next round + iTies = Max(0, + NEORules()->roundNumber() + + ((NEORules()->GetRoundStatus() == PostRound) ? +0 : -1) + - pTeamJinrai->GetRoundsWon() + - pTeamNSF->GetRoundsWon()); + } + } + + NeoUI::BeginContext(&m_uiCtx, eMode, nullptr, "ScoreboardCtx"); + { + // Figure out tall length of the scoreboard + int iTallTotal = m_uiCtx.layout.iRowTall * (1 + 1 + iMaxSidePlayers); + if (iaTeamTally[TEAM_UNASSIGNED] > 0) iTallTotal += m_uiCtx.layout.iRowTall * (1 + iaTeamTally[TEAM_UNASSIGNED]); + if (iaTeamTally[TEAM_SPECTATOR] > 0) iTallTotal += m_uiCtx.layout.iRowTall * (1 + iaTeamTally[TEAM_SPECTATOR]); + + // Output server's name left-aligned, map's name right-aligned + m_uiCtx.dPanel.x = (wide / 2) - (iRootSubPanelWide / 2); + m_uiCtx.dPanel.y = (tall / 2) - (iTallTotal / 2); + m_uiCtx.dPanel.wide = iRootSubPanelWide; + m_uiCtx.dPanel.tall = m_uiCtx.layout.iRowTall; + m_uiCtx.colors.normalFg = COLOR_NEO_WHITE; + m_uiCtx.colors.sectionBg = COLOR_TRANSPARENT; + m_uiCtx.colors.divider = COLOR_TRANSPARENT; + + NeoUI::BeginSection(NeoUI::SECTIONFLAG_DISABLEOFFSETS); + { + NeoUI::SetPerRowLayout(3); + + NeoUI::LabelExOpt opt = { + .eTextStyle = NeoUI::TEXTSTYLE_LEFT, + .eFont = m_uiCtx.eFont, + }; + + // Server's name + NeoUI::Label(m_wszHostname, opt); + + // Tie counter + if (iTies > 0) + { + opt.eTextStyle = NeoUI::TEXTSTYLE_CENTER; + wchar_t wszText[32]; + V_swprintf_safe(wszText, L"Ties: %d", iTies); + NeoUI::Label(wszText, opt); + } + else + { + NeoUI::Pad(); + } + + // Map's name + opt.eTextStyle = NeoUI::TEXTSTYLE_RIGHT; + NeoUI::Label(m_wszMap, opt); + } + NeoUI::EndSection(); + + m_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; + + // Output all the players in the server + for (int iCurTeam = TEAM_UNASSIGNED; iCurTeam < TEAM__TOTAL; ++iCurTeam) + { + if (iaTeamTally[iCurTeam] <= 0 && iCurTeam <= TEAM_SPECTATOR) + { + continue; + } + m_uiCtx.dPanel.wide = (iCurTeam <= TEAM_SPECTATOR) + ? iRootSubPanelWide + : iRootSubPanelWide / 2; + if (iCurTeam >= FIRST_GAME_TEAM) + { + m_uiCtx.dPanel.wide -= (iGap / 2); + } + + const bool bNSFFirst = cl_neo_hud_team_swap_sides.GetBool() && TEAM_NSF == iLocalPlayerTeam; + + m_uiCtx.dPanel.x = (wide / 2) - (iRootSubPanelWide / 2); + if ((bNSFFirst && TEAM_JINRAI == iCurTeam) + || (false == bNSFFirst && TEAM_NSF == iCurTeam)) + { + m_uiCtx.dPanel.x += (iRootSubPanelWide / 2) + (iGap / 2); + } + + m_uiCtx.dPanel.y = (tall / 2) - (iTallTotal / 2) + m_uiCtx.layout.iRowTall; + if (iCurTeam <= TEAM_SPECTATOR) + { + m_uiCtx.dPanel.y += (m_uiCtx.layout.iRowTall * (1 + iMaxSidePlayers)) + iGap; + if (TEAM_SPECTATOR == iCurTeam && iaTeamTally[TEAM_UNASSIGNED] > 0) + { + m_uiCtx.dPanel.y += (m_uiCtx.layout.iRowTall * (1 + iaTeamTally[TEAM_UNASSIGNED])); + } + } + // One for the heading + m_uiCtx.dPanel.tall = m_uiCtx.layout.iRowTall * (1 + ((iCurTeam >= FIRST_GAME_TEAM) ? iMaxSidePlayers : iaTeamTally[iCurTeam])); + + NeoUI::BeginSection(NeoUI::SECTIONFLAG_DISABLEOFFSETS); + { + if (0 == m_iColsWidePlayersList[0]) + { + m_iColsWidePlayersList[COLSPLAYERS_PING] = NeoUI::SuitableWideByWStr(L"BOT", NeoUI::SUITABLEWIDE_TABLE); + m_iColsWidePlayersList[COLSPLAYERS_AVATAR] = m_uiCtx.layout.iRowTall; + m_iColsWidePlayersList[COLSPLAYERS_NAME] = 0; + m_iColsWidePlayersList[COLSPLAYERS_READYUP] = bShowReadyUp ? NeoUI::SuitableWideByWStr(L"NOT READY", NeoUI::SUITABLEWIDE_TABLE) : 0; + m_iColsWidePlayersList[COLSPLAYERS_CLASS] = NeoUI::SuitableWideByWStr(L"Support", NeoUI::SUITABLEWIDE_TABLE); + m_iColsWidePlayersList[COLSPLAYERS_RANK] = NeoUI::SuitableWideByWStr(bHasRanklessDog ? L"Rankless Dog" : L"Lieutenant", NeoUI::SUITABLEWIDE_TABLE); + m_iColsWidePlayersList[COLSPLAYERS_XP] = NeoUI::SuitableWideByWStr(L"-99", NeoUI::SUITABLEWIDE_TABLE); + m_iColsWidePlayersList[COLSPLAYERS_DEATH] = NeoUI::SuitableWideByWStr(L"Deaths", NeoUI::SUITABLEWIDE_TABLE); + + int iTotalColsUsed = 0; + for (int i = 0; i < COLSPLAYERS__TOTAL; ++i) + { + iTotalColsUsed += m_iColsWidePlayersList[i]; + } + m_iColsWidePlayersList[COLSPLAYERS_NAME] = m_uiCtx.dPanel.wide - iTotalColsUsed; + } + if (0 == m_iColsWideNonPlayersList[0]) + { + m_iColsWideNonPlayersList[COLSNONPLAYERS_PING] = NeoUI::SuitableWideByWStr(L"BOT", NeoUI::SUITABLEWIDE_TABLE); + m_iColsWideNonPlayersList[COLSNONPLAYERS_AVATAR] = m_uiCtx.layout.iRowTall; + m_iColsWideNonPlayersList[COLSNONPLAYERS_NAME] = 0; + + int iTotalColsUsed = 0; + for (int i = 0; i < COLSNONPLAYERS__TOTAL; ++i) + { + iTotalColsUsed += m_iColsWideNonPlayersList[i]; + } + m_iColsWideNonPlayersList[COLSNONPLAYERS_NAME] = m_uiCtx.dPanel.wide - iTotalColsUsed; + } + + NeoUI::BeginTable( + (iCurTeam >= FIRST_GAME_TEAM) + ? m_iColsWidePlayersList + : m_iColsWideNonPlayersList, + (iCurTeam >= FIRST_GAME_TEAM) + ? ARRAYSIZE(m_iColsWidePlayersList) + : ARRAYSIZE(m_iColsWideNonPlayersList)); + + m_uiCtx.colors.normalFg = (bNotTeamplay && iCurTeam >= FIRST_GAME_TEAM) + ? COLOR_NEO_ORANGE + : g_PR->GetTeamColor(iCurTeam); + + wchar_t wszText[NEO_MAX_DISPLAYNAME]; + NeoUI::NextTableRow(); + + NeoUI::Pad(); // Ping/BOT column - Show team rounds won + if (iCurTeam >= FIRST_GAME_TEAM) + { + if (bNotTeamplay) + { + // DM - Print players total on the left side only + if (iCurTeam == TEAM_JINRAI) + { + V_swprintf_safe(wszText, L"Players: %d", + iaTeamTally[TEAM_JINRAI] + iaTeamTally[TEAM_NSF]); + } + else + { + wszText[0] = L'\0'; + } + } + else + { + C_Team *pTeam = GetGlobalTeam(iCurTeam); + Assert(pTeam); + if (pTeam) + { + wchar_t wszTeamtag[NEO_MAX_CLANTAG_LENGTH] = {}; + const char *pSzClantag = NEORules()->GetTeamClantag(iCurTeam); + if (pSzClantag && pSzClantag[0]) + { + g_pVGuiLocalize->ConvertANSIToUnicode(pSzClantag, wszTeamtag, sizeof(wszTeamtag)); + } + else + { + V_wcscpy_safe(wszTeamtag, SZWSZ_NEO_TEAM_STRS[iCurTeam].wszStr); + } + if (bShowReadyUp) + { + V_swprintf_safe(wszText, L"%ls: %d (%d players - %d ready)", + wszTeamtag, pTeam->GetRoundsWon(), iaTeamTally[iCurTeam], + iaTeamReadyTally[iCurTeam]); + } + else + { + V_swprintf_safe(wszText, L"%ls: %d (%d players)", + wszTeamtag, pTeam->GetRoundsWon(), iaTeamTally[iCurTeam]); + } + } + } + } + else + { + V_wcscpy_safe(wszText, SZWSZ_NEO_TEAM_STRS[iCurTeam].wszStr); + } + NeoUI::Label(wszText, true); + + NeoUI::Pad(); // Avatar/Dead-indicator + NeoUI::Pad(); // Name column + if (iCurTeam >= FIRST_GAME_TEAM) + { + NeoUI::Label(L"Ready"); // Hidden when not used + NeoUI::Label(L"Class"); + NeoUI::Label(L"Rank"); + NeoUI::Label(L"XP"); + NeoUI::Label(L"Deaths"); + } + + if (NeoUI::MODE_PAINT == m_uiCtx.eMode) + { + const int iMargin = Max(static_cast(0.25f * m_uiCtx.iMarginY), 1); + vgui::surface()->DrawSetColor(m_uiCtx.colors.normalFg); + vgui::surface()->DrawFilledRect( + m_uiCtx.dPanel.x, + m_uiCtx.dPanel.y + m_uiCtx.layout.iRowTall - iMargin, + m_uiCtx.dPanel.x + m_uiCtx.dPanel.wide, + m_uiCtx.dPanel.y + m_uiCtx.layout.iRowTall); + } + + if (iCurTeam < FIRST_GAME_TEAM) + { + m_uiCtx.colors.normalFg = COLOR_NEO_WHITE; + } + + Color colorAliveFg = m_uiCtx.colors.normalFg; + Color colorDeadFg = colorAliveFg; + colorDeadFg[0] *= 0.75f; + colorDeadFg[1] *= 0.75f; + colorDeadFg[2] *= 0.75f; + Color colorLocalBg = colorAliveFg; + colorLocalBg[0] *= 0.33f; + colorLocalBg[1] *= 0.33f; + colorLocalBg[2] *= 0.33f; + colorLocalBg[3] = 75; + + int iInPlaying = 0; + for (int i = 0; i < m_iTotalPlayers; ++i) + { + const CNEOUIScoreBoardPlayer *pPlayerInfo = &m_playersInfo[i]; + const bool bIsPlaying = (TEAM_JINRAI == pPlayerInfo->iTeam + || TEAM_NSF == pPlayerInfo->iTeam); + const bool bIsDMPlaying = bNotTeamplay && bIsPlaying; + iInPlaying += (bIsPlaying); + + const bool bDMNotThisSide = bIsDMPlaying && + ((iCurTeam >= FIRST_GAME_TEAM && (static_cast(iInPlaying % 2) == static_cast(TEAM_NSF == iCurTeam))) + || (iCurTeam <= TEAM_SPECTATOR && pPlayerInfo->iTeam != iCurTeam)); + if (bDMNotThisSide || (false == bIsDMPlaying && pPlayerInfo->iTeam != iCurTeam)) + { + continue; + } + + if (NeoUI::NextTableRow(NeoUI::NEXTTABLEROWFLAG_SELECTABLE).bPressed + && false == pPlayerInfo->bBot) + { + m_playerPopup = *pPlayerInfo; + const bool bHaveFriendReq = (SteamFriends() + && k_EFriendRelationshipRequestInitiator == SteamFriends()->GetFriendRelationship(m_playerPopup.steamID)); + NeoUI::OpenPopup(NEOSCOREBOARDPOPUP_CARD, NeoUI::Dim{ + .x = m_uiCtx.iMouseAbsX, + .y = m_uiCtx.iMouseAbsY, + .wide = 5 * iPopupCardPerRowTallButtons + + (bHaveFriendReq ? iPopupCardPerRowTallButtons : 0), + .tall = iPopupCardPerRowTallAvatarName + iPopupCardPerRowTallButtons, + }); + } + + if (iCurTeam >= FIRST_GAME_TEAM) + { + if (bShowReadyUp) + { + m_uiCtx.colors.normalFg = (pPlayerInfo->bReady) ? colorAliveFg : colorDeadFg; + } + else + { + m_uiCtx.colors.normalFg = (pPlayerInfo->bDead) ? colorDeadFg : colorAliveFg; + } + } + if (pPlayerInfo->iUserID == iLocalUserID) + { + vgui::surface()->DrawSetColor(colorLocalBg); + vgui::surface()->DrawFilledRect( + m_uiCtx.dPanel.x, + m_uiCtx.dPanel.y + m_uiCtx.iLayoutY, + m_uiCtx.dPanel.x + m_uiCtx.dPanel.wide, + m_uiCtx.dPanel.y + m_uiCtx.iLayoutY + m_uiCtx.layout.iRowTall); + } + + // Ping/BOT + if (pPlayerInfo->iPing >= 0) + { + V_swprintf_safe(wszText, L"%d", pPlayerInfo->iPing); + NeoUI::Label(wszText); + } + else if (pPlayerInfo->bBot) + { + NeoUI::Label(L"BOT"); + } + else + { + NeoUI::Pad(); + } + + // Avatar/Dead-indicator + NeoUI::Pad(); + CAvatarImage *pAvatarImg = nullptr; + if (ShowAvatars() && pPlayerInfo->avatar.i32Idx >= 0) + { + // Use higher px image if wanted, otherwise fallback to i32Idx + if (pPlayerInfo->avatar.i64Idx >= 0 && IN_BETWEEN_EQ(32, m_uiCtx.irWidgetTall, 64)) + { + pAvatarImg = (CAvatarImage *)(m_pImageList->GetImage(pPlayerInfo->avatar.i64Idx)); + } + else if (pPlayerInfo->avatar.i184Idx >= 0 && m_uiCtx.irWidgetTall > 64) + { + pAvatarImg = (CAvatarImage *)(m_pImageList->GetImage(pPlayerInfo->avatar.i184Idx)); + } + else + { + pAvatarImg = (CAvatarImage *)(m_pImageList->GetImage(pPlayerInfo->avatar.i32Idx)); + } + + if (pAvatarImg) + { + pAvatarImg->SetPos(m_uiCtx.rWidgetArea.x0, m_uiCtx.rWidgetArea.y0); + pAvatarImg->SetSize(m_uiCtx.irWidgetTall, m_uiCtx.irWidgetTall); + pAvatarImg->Paint(); + } + } + // Voice mute icon - layered over avatar, only do for actual players + if (pPlayerInfo->bMuted && false == pPlayerInfo->bBot) + { + // Slightly redden the avatar + if (pAvatarImg) + { + vgui::surface()->DrawSetColor(100, 0, 0, 75); + vgui::surface()->DrawFilledRect( + m_uiCtx.rWidgetArea.x0, + m_uiCtx.rWidgetArea.y0, + m_uiCtx.rWidgetArea.x0 + m_uiCtx.irWidgetTall, + m_uiCtx.rWidgetArea.y0 + m_uiCtx.irWidgetTall); + } + NeoUI::Texture("vgui/hud/voice_mute", + m_uiCtx.rWidgetArea.x0, + m_uiCtx.rWidgetArea.y0, + m_uiCtx.irWidgetTall, + m_uiCtx.irWidgetTall, + "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL); + } + if (iCurTeam >= FIRST_GAME_TEAM) + { + // Darken the avatar if dead or not ready + if (pAvatarImg + && (pPlayerInfo->bDead + || (bShowReadyUp && false == pPlayerInfo->bReady))) + { + vgui::surface()->DrawSetColor(0, 0, 0, 200); + vgui::surface()->DrawFilledRect( + m_uiCtx.rWidgetArea.x0, + m_uiCtx.rWidgetArea.y0, + m_uiCtx.rWidgetArea.x0 + m_uiCtx.irWidgetTall, + m_uiCtx.rWidgetArea.y0 + m_uiCtx.irWidgetTall); + } + + // Dead icon - layered over avatar + if (pPlayerInfo->bDead) + { + NeoUI::Texture("vgui/hud/kill_kill", + m_uiCtx.rWidgetArea.x0, + m_uiCtx.rWidgetArea.y0, + m_uiCtx.irWidgetTall, + m_uiCtx.irWidgetTall, + "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL); + } + } + + // Player's name + if (pPlayerInfo->wszClantag[0]) + { + V_swprintf_safe(wszText, L"[%ls] %ls", + pPlayerInfo->wszClantag, pPlayerInfo->wszName); + } + else + { + V_wcscpy_safe(wszText, pPlayerInfo->wszName); + } + NeoUI::Label(wszText); + + if (iCurTeam >= FIRST_GAME_TEAM) + { + // Ready-up (Hidden when not used) + NeoUI::Label(pPlayerInfo->bReady ? L"READY" : L"NOT READY"); + + // Class + NeoUI::Label(GetNeoClassNameW(pPlayerInfo->iClass)); + + // Rank + NeoUI::Label(GetRankNameW(pPlayerInfo->iXP)); + + // XP + V_swprintf_safe(wszText, L"%d", pPlayerInfo->iXP); + NeoUI::Label(wszText); + + // Deaths count + V_swprintf_safe(wszText, L"%d", pPlayerInfo->iDeaths); + NeoUI::Label(wszText); + } + } + NeoUI::EndTable(); + } + NeoUI::EndSection(); + } + + m_uiCtx.colors.normalFg = COLOR_NEO_WHITE; + + if (NeoUI::BeginPopup(NEOSCOREBOARDPOPUP_CARD)) + { + // NEO TODO (nullsystem): If name longer than popup box, paint over the remaining + // area + + const int iAvatarOffset = m_uiCtx.iMarginX; + const int iAvatarWT = iPopupCardPerRowTallAvatarName - (iAvatarOffset * 2); + + CAvatarImage *pAvatarImg = nullptr; + if (m_playerPopup.avatar.i184Idx >= 0) + { + pAvatarImg = (CAvatarImage *)(m_pImageList->GetImage(m_playerPopup.avatar.i184Idx)); + } + if (pAvatarImg) + { + pAvatarImg->SetPos(m_uiCtx.dPanel.x + iAvatarOffset, + m_uiCtx.dPanel.y + iAvatarOffset); + pAvatarImg->SetSize(iAvatarWT, iAvatarWT); + pAvatarImg->Paint(); + } + + NeoUI::SetPerRowLayout(1, nullptr, iPopupCardPerRowTallAvatarName); + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + NeoUI::Pad(); + + vgui::surface()->DrawSetTextPos( + m_uiCtx.dPanel.x + iAvatarOffset + iAvatarWT + iAvatarOffset, + m_uiCtx.dPanel.y + iAvatarOffset); + if (m_playerPopup.wszClantag[0]) + { + vgui::surface()->DrawSetTextColor(m_uiCtx.colors.normalFg); + vgui::surface()->DrawPrintText(m_playerPopup.wszClantag, + V_wcslen(m_playerPopup.wszClantag)); + + const auto *pFontI = &m_uiCtx.fonts[m_uiCtx.eFont]; + const int iClantagTall = vgui::surface()->GetFontTall(pFontI->hdl); + vgui::surface()->DrawSetTextPos( + m_uiCtx.dPanel.x + iAvatarOffset + iAvatarWT + iAvatarOffset, + m_uiCtx.dPanel.y + iAvatarOffset + iClantagTall + iAvatarOffset); + } + NeoUI::SwapFont(NeoUI::FONT_NTLARGE); + vgui::surface()->DrawSetTextColor(m_uiCtx.colors.hotFg); + vgui::surface()->DrawPrintText(m_playerPopup.wszName, + V_wcslen(m_playerPopup.wszName)); + + NeoUI::SetPerRowLayout(5, nullptr, iPopupCardPerRowTallButtons); + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + + // NEO NOTE (nullsystem): Currently bots won't have a popup card + Assert(false == m_playerPopup.bBot); + if (false == m_playerPopup.bBot) + { + // NEO NOTE (nullsystem): CVoiceBanMgr can mute (and unmute) yourself, so no checks + // for local player here + if (m_playerPopup.iUserID > 0) + { + if (NeoUI::ButtonTexture( + m_playerPopup.bMuted + ? "vgui/hud/voice_transmit" + : "vgui/hud/voice_mute" + , "GAME" + , m_playerPopup.bMuted + ? L"Unmute" + : L"Mute").bPressed) + { + int iPlayerIdx = 0; + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + if (g_PR->GetUserID(i) == m_playerPopup.iUserID) + { + iPlayerIdx = i; + break; + } + } + if (iPlayerIdx > 0) + { + GetClientVoiceMgr()->SetPlayerBlockedState(iPlayerIdx, + !m_playerPopup.bMuted); + } + NeoUI::ClosePopup(); + } + } + + if (SteamFriends()) + { + CSteamID &steamID = m_playerPopup.steamID; + + // NEO TODO (nullsystem): Turn to proper texture when done + // Replace every .png with actual vmt/vtf + if (NeoUI::ButtonTexture("materials/vgui/hud/player_profile.png", "GAME", + L"Profile").bPressed) + { + SteamFriends()->ActivateGameOverlayToUser("steamid", steamID); + NeoUI::ClosePopup(); + } + if (m_playerPopup.iUserID != iLocalUserID) + { + const char *pszOverlay = nullptr; + // NEO TODO (nullsystem) png -> vtf/vmt + if (NeoUI::ButtonTexture("materials/vgui/hud/player_message.png", "GAME", + L"Message").bPressed) + { + pszOverlay = "chat"; + } + + const EFriendRelationship eFriendRel = + SteamFriends()->GetFriendRelationship(steamID); + switch (eFriendRel) + { + case k_EFriendRelationshipNone: + if (NeoUI::ButtonTexture("", "GAME", + L"Send").bPressed) + { + pszOverlay = "friendadd"; + } + break; + case k_EFriendRelationshipRequestRecipient: + if (NeoUI::ButtonTexture("", "GAME", + L"Cancel").bPressed) + { + pszOverlay = "friendremove"; + } + break; + case k_EFriendRelationshipRequestInitiator: + if (NeoUI::ButtonTexture("", "GAME", + L"Accept").bPressed) + { + pszOverlay = "friendrequestaccept"; + } + if (NeoUI::ButtonTexture("", "GAME", + L"Ignore").bPressed) + { + pszOverlay = "friendrequestignore"; + } + break; + default: + break; + } + + if (pszOverlay) + { + SteamFriends()->ActivateGameOverlayToUser(pszOverlay, steamID); + NeoUI::ClosePopup(); + } + } + } + + if (m_playerPopup.iUserID == iLocalUserID) + { + if (bShowReadyUp) + { + if (NeoUI::ButtonTexture("", "GAME", + m_playerPopup.bReady ? L"Unready" : L"Ready").bPressed) + { + engine->ClientCmd(m_playerPopup.bReady + ? "readytoggle unready" + : "readytoggle ready"); + NeoUI::ClosePopup(); + } + } + } + else + { + // NEO TODO (nullsystem) png -> vtf/vmt + if (NeoUI::ButtonTexture("materials/vgui/hud/player_crosshair_icon.png", "GAME", + L"Crosshair").bPressed) + { + NeoUI::ClosePopup(); + if (m_playerPopup.szCrosshair[0]) + { + NeoUI::OpenPopup(NEOSCOREBOARDPOPUP_COPYCROSSHAIR, + NeoUI::Dim{ + .x = wide / 2 - wide / 4, + .y = tall / 2 - ((m_uiCtx.layout.iRowTall * 3) / 2), + .wide = wide / 2, + .tall = m_uiCtx.layout.iRowTall * 3, + }); + } + } + } + } + + NeoUI::EndPopup(); + } + + // NEO TODO (nullsystem): Probably another separate dialog without having to + // hold scoreboard bind instead? + if (NeoUI::BeginPopup(NEOSCOREBOARDPOPUP_COPYCROSSHAIR)) + { + NeoUI::SwapFont(NeoUI::FONT_NTLARGE); + const auto tmpButtonTextStyle = m_uiCtx.eButtonTextStyle; + m_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; + + NeoUI::SetPerRowLayout(1); + NeoUI::HeadingLabel(L"Replace your crosshair?"); + NeoUI::Pad(); + + NeoUI::SetPerRowLayout(3); + if (NeoUI::Button(L"Yes").bPressed) + { + ConVarRef cvr_cl_neo_crosshair("cl_neo_crosshair"); + cvr_cl_neo_crosshair.SetValue(m_playerPopup.szCrosshair); + NeoUI::ClosePopup(); + } + NeoUI::Pad(); + if (NeoUI::Button(L"No").bPressed) + { + NeoUI::ClosePopup(); + } + + m_uiCtx.eButtonTextStyle = tmpButtonTextStyle; + + NeoUI::EndPopup(); + } + } + NeoUI::EndContext(); +} + diff --git a/src/game/client/neo/ui/neoui_scoreboard.h b/src/game/client/neo/ui/neoui_scoreboard.h new file mode 100644 index 000000000..2dcc775ec --- /dev/null +++ b/src/game/client/neo/ui/neoui_scoreboard.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +#include "neo_player_shared.h" +#include "ui/neo_ui.h" +#include "neo_crosshair.h" + +struct MapAvatarValue +{ + int i32Idx; + int i64Idx; + int i184Idx; +}; + +struct CNEOUIScoreBoardPlayer +{ + int iUserID; // More reliable than player index + int iTeam; + int iDeaths; + int iXP; + int iClass; + int iPing; + // NEO TODO (nullsystem): Turn to flags? Add local state? + bool bDead; + bool bBot; + bool bMuted; + bool bReady; + CSteamID steamID; + MapAvatarValue avatar; + wchar_t wszName[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszClantag[NEO_MAX_CLANTAG_LENGTH]; + char szCrosshair[NEO_XHAIR_SEQMAX]; +}; + +class CNEOUIScoreBoard : public vgui::Panel, public IViewPortPanel, public CGameEventListener +{ + DECLARE_CLASS_SIMPLE(CNEOUIScoreBoard, vgui::Panel); + +public: + CNEOUIScoreBoard(IViewPort *pViewPort); + ~CNEOUIScoreBoard(); + + const char *GetName() final; + void ApplySchemeSettings(vgui::IScheme *pScheme) final; + void Paint() final; + void OnMousePressed(vgui::MouseCode code) final; + void OnMouseReleased(vgui::MouseCode code) final; + void OnMouseDoublePressed(vgui::MouseCode code) final; + void OnCursorMoved(int x, int y) final; + void OnKeyCodePressed(vgui::KeyCode code) final; + void OnKeyCodeReleased(vgui::KeyCode code) final; + void Reset() final; + bool NeedsUpdate() final; + void Update() final; + bool IsVisible() final { return BaseClass::IsVisible(); } + void ToggleMouseCapture(const bool bUseMouse); + + void SetData([[maybe_unused]] KeyValues *data) final {} + bool HasInputElements() final { return true; } + void ShowPanel(bool state) final; + GameActionSet_t GetPreferredActionSet() final { return GAME_ACTION_SET_IN_GAME_HUD; } + vgui::VPANEL GetVPanel() final { return BaseClass::GetVPanel(); } + void SetParent(vgui::VPANEL parent) final { BaseClass::SetParent( parent ); } + void FireGameEvent(IGameEvent *event) final; + + bool ShowAvatars(); + bool UpdateAvatars(); + + void OnMainLoop(const NeoUI::Mode eMode); + + MESSAGE_FUNC_INT(OnPollHideCode, "PollHideCode", code); + void OnThink() final; + + NeoUI::Context m_uiCtx; + + int m_HLTVSpectators = 0; + int m_iTotalPlayers = 0; + CNEOUIScoreBoardPlayer m_playersInfo[MAX_PLAYERS] = {}; + CNEOUIScoreBoardPlayer m_playerPopup = {}; + + wchar_t m_wszHostname[128] = {}; + wchar_t m_wszMap[128] = {}; + + vgui::ImageList *m_pImageList = nullptr; + CUtlMap m_mapAvatarsToImageList; + float m_flNextUpdateTime = 0.0f; + ButtonCode_t m_nCloseKey = BUTTON_CODE_INVALID; + + enum EColsPlayers + { + COLSPLAYERS_PING = 0, + COLSPLAYERS_AVATAR, + COLSPLAYERS_NAME, + COLSPLAYERS_READYUP, + COLSPLAYERS_CLASS, + COLSPLAYERS_RANK, + COLSPLAYERS_XP, + COLSPLAYERS_DEATH, + + COLSPLAYERS__TOTAL, + }; + int m_iColsWidePlayersList[COLSPLAYERS__TOTAL] = {}; + + enum EColsNonPlayers + { + COLSNONPLAYERS_PING = 0, + COLSNONPLAYERS_AVATAR, + COLSNONPLAYERS_NAME, + + COLSNONPLAYERS__TOTAL, + }; + int m_iColsWideNonPlayersList[COLSNONPLAYERS__TOTAL] = {}; +}; + +extern CNEOUIScoreBoard *g_pNeoUIScoreBoard; + diff --git a/src/game/server/player_resource.cpp b/src/game/server/player_resource.cpp index 1a5225889..a65bd47c1 100644 --- a/src/game/server/player_resource.cpp +++ b/src/game/server/player_resource.cpp @@ -11,6 +11,7 @@ #ifdef NEO #include "neo_player.h" +#include "neo_gamerules.h" #endif // memdbgon must be the last include file in a .cpp file!!! @@ -31,6 +32,8 @@ IMPLEMENT_SERVERCLASS_ST_NOBASE(CPlayerResource, DT_PlayerResource) SendPropArray3(SENDINFO_ARRAY3(m_iStar), SendPropInt(SENDINFO_ARRAY(m_iStar), 12)), SendPropArray3(SENDINFO_ARRAY3(m_szNeoClantag), SendPropString(SENDINFO_ARRAY(m_szNeoClantag), 0, SendProxy_StringT_To_String)), SendPropArray3(SENDINFO_ARRAY3(m_iMaxHealth), SendPropInt(SENDINFO_ARRAY(m_iMaxHealth), -1, SPROP_VARINT | SPROP_UNSIGNED)), + SendPropArray3(SENDINFO_ARRAY3(m_szNeoCrosshair), SendPropString(SENDINFO_ARRAY(m_szNeoCrosshair), 0, SendProxy_StringT_To_String)), + SendPropArray3(SENDINFO_ARRAY3(m_bReady), SendPropInt(SENDINFO_ARRAY(m_bReady), 1, SPROP_UNSIGNED)), #endif SendPropArray3( SENDINFO_ARRAY3(m_iScore), SendPropInt( SENDINFO_ARRAY(m_iScore), 12 ) ), SendPropArray3( SENDINFO_ARRAY3(m_iDeaths), SendPropInt( SENDINFO_ARRAY(m_iDeaths), 12 ) ), @@ -96,6 +99,8 @@ void CPlayerResource::Init( int iIndex ) m_iStar.Set(iIndex, 0); m_szNeoClantag.Set(iIndex, m_szNeoNameNone); m_iMaxHealth.Set(iIndex, 1); + m_szNeoCrosshair.Set(iIndex, m_szNeoNameNone); + m_bReady.Set(iIndex, 0); #endif m_iPing.Set( iIndex, 0 ); m_iScore.Set( iIndex, 0 ); @@ -176,6 +181,20 @@ void CPlayerResource::UpdatePlayerData( void ) m_szNeoClantag.Set(i, strt); } m_iNeoNameDupeIdx.Set(i, neoPlayer->NameDupePos()); + { + const char *neoCrosshair = neoPlayer->m_szNeoCrosshair.Get(); + string_t strt; + if (neoCrosshair && neoCrosshair[0] != '\0') + { + strt = AllocPooledString(neoCrosshair); + } + else + { + strt = m_szNeoNameNone; + } + m_szNeoCrosshair.Set(i, strt); + } + m_bReady.Set(i, NEORules() ? NEORules()->ReadyUpPlayerIsReady(neoPlayer) : false); #endif UpdateConnectedPlayer( i, pPlayer ); } diff --git a/src/game/server/player_resource.h b/src/game/server/player_resource.h index 02f001954..0c8395800 100644 --- a/src/game/server/player_resource.h +++ b/src/game/server/player_resource.h @@ -57,6 +57,8 @@ class CPlayerResource : public CBaseEntity CNetworkArray(int, m_iStar, MAX_PLAYERS_ARRAY_SAFE); CNetworkArray(string_t, m_szNeoClantag, MAX_PLAYERS_ARRAY_SAFE); CNetworkArray(int, m_iMaxHealth, MAX_PLAYERS_ARRAY_SAFE); + CNetworkArray(string_t, m_szNeoCrosshair, MAX_PLAYERS_ARRAY_SAFE); + CNetworkArray(int, m_bReady, MAX_PLAYERS_ARRAY_SAFE); #endif CNetworkArray( int, m_iScore, MAX_PLAYERS_ARRAY_SAFE ); CNetworkArray( int, m_iDeaths, MAX_PLAYERS_ARRAY_SAFE ); diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 0ddbbb19a..98e35a4f4 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -72,9 +72,27 @@ ConVar neo_vip_eligible("cl_neo_vip_eligible", "1", FCVAR_ARCHIVE, "Eligible for #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); ConVar sv_neo_jgr_max_points("sv_neo_jgr_max_points", "20", FCVAR_GAMEDLL, "Maximum points required for a team to win in JGR", true, 1, false, 0); -#endif -#ifdef GAME_DLL +void ReadyToggleCB(const CCommand &command) +{ + if (auto pNeoPlayer = static_cast(UTIL_GetCommandClient())) + { + if (2 != command.ArgC()) + { + Msg("Usage: readytoggle [ready|unready]\n"); + return; + } + CNEORules::ReadyToggleFlags flags = CNEORules::READYTOGGLEFLAG_PRINTCHANGE; + if (0 == V_strcmp(command[1], "unready")) + { + flags |= CNEORules::READYTOGGLEFLAG_UNREADY; + } + NEORules()->ReadyToggle(pNeoPlayer, flags); + } +} + +ConCommand readytoggle("readytoggle", ReadyToggleCB, "Toggle ready state", FCVAR_USERINFO); + // NEO TODO (nullsystem): Change how voting done from convar to menu selection enum eGamemodeEnforcement { @@ -2359,31 +2377,15 @@ void CNEORules::CheckChatCommand(CNEO_Player *pNeoCmdPlayer, const char *pSzChat if (steamID.IsValid()) { const int iThres = sv_neo_readyup_teamplayersthres.GetInt(); - if (V_strcmp(pSzChat, "ready") == 0) + const bool bIsSetUnReady = (0 == V_strcmp(pSzChat, "unready")); + if (bIsSetUnReady || 0 == V_strcmp(pSzChat, "ready")) { - 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 + ReadyToggleFlags flags = READYTOGGLEFLAG_NIL; + if (bIsSetUnReady) { - char szReadyText[32]; - V_sprintf_safe(szReadyText, "%d/%d players are ready.", readyPlayers.array[TEAM_JINRAI] + readyPlayers.array[TEAM_NSF], iThres * 2); - UTIL_ClientPrintAll(HUD_PRINTTALK, szReadyText); + flags |= READYTOGGLEFLAG_UNREADY; } - } - else if (V_strcmp(pSzChat, "unready") == 0) - { - m_readyAccIDs.Remove(steamID.GetAccountID()); - ClientPrint(pNeoCmdPlayer, HUD_PRINTTALK, "You are now marked as unready."); - - char szReadyText[32]; - const auto readyPlayers = FetchReadyPlayers(); - V_sprintf_safe(szReadyText, "%d/%d players are ready.", readyPlayers.array[TEAM_JINRAI] + readyPlayers.array[TEAM_NSF], iThres * 2); - UTIL_ClientPrintAll(HUD_PRINTTALK, szReadyText); + ReadyToggle(pNeoCmdPlayer, flags); } else if (V_strcmp(pSzChat, "start") == 0) { @@ -4976,6 +4978,58 @@ void CNEORules::InitDefaultAIRelationships( void ) CBaseCombatCharacter::SetDefaultRelationship(CLASS_EARTH_FAUNA, CLASS_PLAYER_ALLY, D_HT, 0); CBaseCombatCharacter::SetDefaultRelationship(CLASS_EARTH_FAUNA, CLASS_PLAYER_ALLY_VITAL,D_HT, 0); } + +void CNEORules::ReadyToggle(CNEO_Player *pNeoPlayer, const ReadyToggleFlags flags) +{ + const int iThres = sv_neo_readyup_teamplayersthres.GetInt(); + const CSteamID steamID = GetSteamIDForPlayerIndex(pNeoPlayer->entindex()); + + if (flags & READYTOGGLEFLAG_PRINTCHANGE) + { + // Have to go one by one due to streamer-mode + char szReadyPrint[MAX_PLAYER_NAME_LENGTH + 32 + 1]; + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + auto *pNeoOtherPlayer = static_cast(UTIL_PlayerByIndex(i)); + if (!pNeoOtherPlayer) + { + continue; + } + + V_sprintf_safe(szReadyPrint, "Player %s now %s" + , pNeoPlayer->GetNeoPlayerName(pNeoOtherPlayer) + , (flags & READYTOGGLEFLAG_UNREADY) ? "unready" : "ready"); + ClientPrint(pNeoOtherPlayer, HUD_PRINTTALK, szReadyPrint); + } + } + + if (flags & READYTOGGLEFLAG_UNREADY) + { + m_readyAccIDs.Remove(steamID.GetAccountID()); + ClientPrint(pNeoPlayer, HUD_PRINTTALK, "You are now marked as unready."); + char szReadyText[32]; + const auto readyPlayers = FetchReadyPlayers(); + V_sprintf_safe(szReadyText, "%d/%d players are ready.", readyPlayers.array[TEAM_JINRAI] + readyPlayers.array[TEAM_NSF], iThres * 2); + UTIL_ClientPrintAll(HUD_PRINTTALK, szReadyText); + } + else + { + m_readyAccIDs.Insert(steamID.GetAccountID()); + ClientPrint(pNeoPlayer, 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 + { + char szReadyText[32]; + V_sprintf_safe(szReadyText, "%d/%d players are ready.", readyPlayers.array[TEAM_JINRAI] + readyPlayers.array[TEAM_NSF], iThres * 2); + UTIL_ClientPrintAll(HUD_PRINTTALK, szReadyText); + } + } +} + #endif #ifdef CLIENT_DLL diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 5602e0b26..3e13f4c43 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -453,6 +453,15 @@ class CNEORules : public CHL2MPRules, public CGameEventListener void SetLastAttacker(const int index) { m_iLastAttacker = m_iLastEvent = index; } void SetLastKiller(const int index) { m_iLastKiller = m_iLastEvent = index; } void SetLastGhoster(const int index) { m_iLastGhoster = m_iLastEvent = index; } + + enum ReadyToggleFlag_ + { + READYTOGGLEFLAG_NIL = 0, + READYTOGGLEFLAG_UNREADY = 1 << 0, + READYTOGGLEFLAG_PRINTCHANGE = 1 << 1, + }; + typedef int ReadyToggleFlags; + void ReadyToggle(CNEO_Player *pNeoPlayer, const ReadyToggleFlags flags); #endif // GAME_DLL public: const int GetLastHurt() const { return m_iLastHurt; } diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 7de2bb4de..f85228463 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -349,22 +349,41 @@ int GetRank(const int xp) return iRank + 1; } -const char *GetRankName(const int xp, const bool shortened) +static constexpr const SZWSZTexts RANK_NAME_LONG[] = { + SZWSZ_INIT("Rankless Dog"), + SZWSZ_INIT("Private"), + SZWSZ_INIT("Corporal"), + SZWSZ_INIT("Sergeant"), + SZWSZ_INIT("Lieutenant"), +}; +static constexpr const SZWSZTexts RANK_NAME_SHORT[] = { + SZWSZ_INIT("Dog"), + SZWSZ_INIT("Pvt"), + SZWSZ_INIT("Cpl"), + SZWSZ_INIT("Sgt"), + SZWSZ_INIT("Lt"), +}; +static_assert(ARRAYSIZE(RANK_NAME_LONG) == ARRAYSIZE(RANK_NAME_SHORT)); + +static const SZWSZTexts &GetRankNameBase(const int xp, const bool shortened) { - static constexpr const char *RANK_NAME_LONG[] = { - "Rankless Dog", "Private", "Corporal", "Sergeant", "Lieutenant" - }; - static constexpr const char *RANK_NAME_SHORT[] = { - "Dog", "Pvt", "Cpl", "Sgt", "Lt" - }; - static_assert(ARRAYSIZE(RANK_NAME_LONG) == ARRAYSIZE(RANK_NAME_SHORT)); - + static const SZWSZTexts EMPTY{"", L""}; const int iRank = GetRank(xp); if (IN_BETWEEN_AR(0, iRank, ARRAYSIZE(RANK_NAME_LONG))) { return (shortened ? RANK_NAME_SHORT : RANK_NAME_LONG)[iRank]; } - return ""; + return EMPTY; +} + +const char *GetRankName(const int xp, const bool shortened) +{ + return GetRankNameBase(xp, shortened).szStr; +} + +const wchar_t *GetRankNameW(const int xp, const bool shortened) +{ + return GetRankNameBase(xp, shortened).wszStr; } void CNEO_Player::CheckAimButtons() diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 48aa6ac09..192c19449 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -288,6 +288,7 @@ inline const wchar_t *GetNeoClassNameW(const int neoClassIdx) int GetRank(const int xp); const char *GetRankName(const int xp, const bool shortened = false); +const wchar_t *GetRankNameW(const int xp, const bool shortened = false); CBaseCombatWeapon* GetNeoWepWithBits(const CNEO_Player* player, const NEO_WEP_BITS_UNDERLYING_TYPE& neoWepBits);