From 4fcd44464bb3618af66c5a8b61a1bbbb33e7701a Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:45:04 +0100 Subject: [PATCH 1/4] Spectator overlay - Team top area info ConVar set team top area infos with ability in live game to update match scores on match end. Team logo can be set with absolute/relative path PNG/JPG files. Also add new main menu settings UI that'll show up if overlay's enabled to edit those team name/logo settings also. --- src/game/client/neo/ui/neo_hud_round_state.h | 4 + .../neo/ui/neo_hud_spectator_overlay.cpp | 244 +++++++++++++++++- .../client/neo/ui/neo_hud_spectator_overlay.h | 12 + src/game/client/neo/ui/neo_root.cpp | 177 ++++++++++++- src/game/client/neo/ui/neo_root.h | 11 + src/game/client/neo/ui/neo_ui.cpp | 156 ++++++----- src/game/client/neo/ui/neo_ui.h | 27 +- 7 files changed, 550 insertions(+), 81 deletions(-) diff --git a/src/game/client/neo/ui/neo_hud_round_state.h b/src/game/client/neo/ui/neo_hud_round_state.h index adcd05601..e8a2ad75f 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.h +++ b/src/game/client/neo/ui/neo_hud_round_state.h @@ -17,6 +17,8 @@ class ImagePanel; class ImageList; } +class CNEOHud_SpectatorOverlay; + constexpr int MAX_GAME_TYPE_OBJECTIVE_LENGTH = 33; class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, public vgui::Panel @@ -64,6 +66,8 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ virtual void LevelShutdown(void) override; private: + friend class CNEOHud_SpectatorOverlay; + vgui::HFont m_hOCRLargeFont = 0UL; vgui::HFont m_hOCRFont = 0UL; vgui::HFont m_hOCRSmallFont = 0UL; diff --git a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp index 0aabbe72c..2767bc764 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -10,6 +10,10 @@ #include "neo_weapon_loadout.h" #include "neo_hud_deathnotice.h" #include "neo_misc.h" +#include "neo_hud_round_state.h" +#include "neo_theme.h" +#include "neo_ui.h" +#include "neo_gamerules.h" #include #include "iclientmode.h" @@ -23,9 +27,55 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" +static void cvCallbackOverlayLogoJinrai( + [[maybe_unused]] IConVar *cvar, + [[maybe_unused]] const char *pOldVal, + [[maybe_unused]] float flOldVal) +{ + if (g_pNeoHudSpecOverlay && + vgui::surface()->IsTextureIDValid(g_pNeoHudSpecOverlay->m_iTexLogoJinrai)) + { + vgui::surface()->DeleteTextureByID(g_pNeoHudSpecOverlay->m_iTexLogoJinrai); + g_pNeoHudSpecOverlay->m_iTexLogoJinrai = -1; + } +} + +static void cvCallbackOverlayLogoNSF( + [[maybe_unused]] IConVar *cvar, + [[maybe_unused]] const char *pOldVal, + [[maybe_unused]] float flOldVal) +{ + if (g_pNeoHudSpecOverlay && + vgui::surface()->IsTextureIDValid(g_pNeoHudSpecOverlay->m_iTexLogoNSF)) + { + vgui::surface()->DeleteTextureByID(g_pNeoHudSpecOverlay->m_iTexLogoNSF); + g_pNeoHudSpecOverlay->m_iTexLogoNSF = -1; + } +} + ConVar cl_neo_hud_spectator_overlay_enabled("cl_neo_hud_spectator_overlay_enabled", "0", FCVAR_ARCHIVE | FCVAR_DONTRECORD, "Whether the spectator overlay HUD is enabled", true, 0.0f, true, 1.0f); +ConVar cl_neo_hud_spectator_overlay_jinrai_name("cl_neo_hud_spectator_overlay_jinrai_name", "", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The name of the Jinrai team"); +ConVar cl_neo_hud_spectator_overlay_nsf_name("cl_neo_hud_spectator_overlay_nsf_name", "", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The name of the NSF team"); +ConVar cl_neo_hud_spectator_overlay_jinrai_logo("cl_neo_hud_spectator_overlay_jinrai_logo", "", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The logo of the Jinrai team, have to be png or jpg", false, 0.0f, false, 0.0f, + cvCallbackOverlayLogoJinrai); +ConVar cl_neo_hud_spectator_overlay_nsf_logo("cl_neo_hud_spectator_overlay_nsf_logo", "", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The logo of the NSF team, have to be png or jpg", false, 0.0f, false, 0.0f, + cvCallbackOverlayLogoNSF); +ConVar cl_neo_hud_spectator_overlay_jinrai_matches_won("cl_neo_hud_spectator_overlay_jinrai_matches_won", "0", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The amount of matches Jinrai team won", true, 0.0f, true, static_cast(COMP_MATCHES_WON_MAX)); +ConVar cl_neo_hud_spectator_overlay_nsf_matches_won("cl_neo_hud_spectator_overlay_nsf_matches_won", "0", + FCVAR_ARCHIVE | FCVAR_DONTRECORD, + "The amount of matches NSF team won", true, 0.0f, true, static_cast(COMP_MATCHES_WON_MAX)); DECLARE_NAMED_HUDELEMENT(CNEOHud_SpectatorOverlay, neo_spectator_overlay); @@ -98,11 +148,14 @@ void CNEOHud_SpectatorOverlay::ClearAll() V_memset(m_cards, 0, sizeof(m_cards)); V_memset(m_iTeamPlayersCount, 0, sizeof(m_iTeamPlayersCount)); m_iEntIndexSelect = 0; + m_iTexLogoJinrai = -1; + m_iTexLogoNSF = -1; } void CNEOHud_SpectatorOverlay::Init() { ListenForGameEvent("round_start"); + ListenForGameEvent("game_end"); ClearAll(); } @@ -126,6 +179,30 @@ void CNEOHud_SpectatorOverlay::FireGameEvent(IGameEvent *event) pCard->iLastHP = 0; } } + else if (0 == V_strcmp(type, "game_end")) + { + // NEO NOTE (nullsystem): Match winner auto update only works on live + // games, not demos + if (NEORules() + && cl_neo_hud_spectator_overlay_enabled.GetBool() + && NEO_GAME_TYPE_SETTINGS[NEORules()->GetGameType()].comp) + { + const int iMatchWinner = event->GetInt("winner"); + switch (iMatchWinner) + { + case TEAM_JINRAI: + cl_neo_hud_spectator_overlay_jinrai_matches_won.SetValue( + cl_neo_hud_spectator_overlay_jinrai_matches_won.GetInt() + 1); + break; + case TEAM_NSF: + cl_neo_hud_spectator_overlay_nsf_matches_won.SetValue( + cl_neo_hud_spectator_overlay_nsf_matches_won.GetInt() + 1); + break; + default: + break; + } + } + } } void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) @@ -195,7 +272,7 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() if (avatar.i184Idx >= 0) { pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); - bAvatarNeedUpdate = false; + bAvatarNeedUpdate = (!pCard->pAvatar || !pCard->pAvatar->IsValid()); } if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i64Idx >= 0) { pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); @@ -760,12 +837,175 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() float flWideAs43 = static_cast(iScrTall) * (4.0f / 3.0f); if (flWideAs43 > flWide) flWideAs43 = flWide; + // Draw team's name and logo on top + const auto *pRoundStateHUD = GET_NAMED_HUDELEMENT(CNEOHud_RoundState, NRoundState); + if (pRoundStateHUD) + { + wchar_t wszJinName[64] = {}; + wchar_t wszNSFName[64] = {}; + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_jinrai_name.GetString(), wszJinName, sizeof(wszJinName)); + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_nsf_name.GetString(), wszNSFName, sizeof(wszNSFName)); + + // 14px [16px] 14px [80px] 14px [name] + // Team logo: 1440/20=72px + // Gaps: 1440/100~=14.4 -> 14px + const int iLogoWH = flWideAs43 / 20.0f; + const int iGapW = flWideAs43 / 100.0f; + + // Won indicators: + // Box WH: 1440/90=16px + // Starting y: 1080/108=10px + // Gap y: 1080/180=6px + const int iIndWinWH = flWideAs43 / 90.0f; + const int iYIndStart = iScrTall / 108.0f; + const int iYIndGap = iScrTall / 180.0f; + // Team logo and name starting y: 1080/270=4px + const int iYNameLogoStart = iScrTall / 270.0f; + // Name: + // Pad wide: 1440/72=20px + // Tall: 1080/24=45px + const int iNamePadW = flWideAs43 / 72.0f; + const int iNameT = iScrTall / 24.0f; + + Color rsColor = pRoundStateHUD->box_color; + + // Jinrai - Left team + { + int x = pRoundStateHUD->m_iLeftOffset; + x -= (iGapW + iIndWinWH); + + // Matches won indicators + { + int y = iYIndStart; + const int iJinWin = cl_neo_hud_spectator_overlay_jinrai_matches_won.GetInt(); + for (int i = 0; i < COMP_MATCHES_WON_MAX; ++i) + { + vgui::surface()->DrawSetColor((iJinWin > i) ? COLOR_NEO_WHITE : rsColor); + vgui::surface()->DrawFilledRect(x, y, x + iIndWinWH, y + iIndWinWH); + y += iIndWinWH + iYIndGap; + } + } + + x -= (iGapW + iLogoWH); + const int y = iYNameLogoStart; + + // Team logo + const char *pszJinLogo = cl_neo_hud_spectator_overlay_jinrai_logo.GetString(); + if (pszJinLogo[0]) + { + if (!vgui::surface()->IsTextureIDValid(m_iTexLogoJinrai) || m_iTexLogoJinrai == -1) + { + m_iTexLogoJinrai = NeoUI::TextureFromFile( + pszJinLogo, "", NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + } + if (vgui::surface()->IsTextureIDValid(m_iTexLogoJinrai) && m_iTexLogoJinrai != -1) + { + vgui::surface()->DrawSetColor(COLOR_WHITE); + vgui::surface()->DrawSetTexture(m_iTexLogoJinrai); + vgui::surface()->DrawTexturedRect( + x, + y, + x + iLogoWH, + y + iLogoWH); + } + } + else + { + vgui::surface()->DrawSetColor(rsColor); + vgui::surface()->DrawFilledRect(x, y, x + iLogoWH, y + iLogoWH); + } + + // NEO TODO (nullsystem): Deal with very long team name that needs multiline? + // Team name + if (wszJinName[0]) + { + int iJinNameWide = 0, iJinNameTall = 0; + vgui::surface()->GetTextSize(m_hTeamNameFont, wszJinName, iJinNameWide, iJinNameTall); + + const int iNameBoxW = iNamePadW + iJinNameWide + iNamePadW; + x -= (iGapW + iNameBoxW); + + DrawNeoHudRoundedBox(x, y, x + iNameBoxW, y + iNameT, COLOR_BLACK_TRANSPARENT); + + vgui::surface()->DrawSetTextFont(m_hTeamNameFont); + vgui::surface()->DrawSetTextColor(COLOR_WHITE); + vgui::surface()->DrawSetTextPos(x + ((iNameBoxW - iJinNameWide) / 2.0f), y); + vgui::surface()->DrawPrintText(wszJinName, V_wcslen(wszJinName)); + } + } + + // NSF - Right team + { + int x = pRoundStateHUD->m_iRightOffset; + x += iGapW; + + // Matches won indicators + { + int y = iYIndStart; + const int iNSFWin = cl_neo_hud_spectator_overlay_nsf_matches_won.GetInt(); + for (int i = 0; i < COMP_MATCHES_WON_MAX; ++i) + { + vgui::surface()->DrawSetColor((iNSFWin > i) ? COLOR_NEO_WHITE : rsColor); + vgui::surface()->DrawFilledRect(x, y, x + iIndWinWH, y + iIndWinWH); + y += iIndWinWH + iYIndGap; + } + } + + x += (iGapW + iIndWinWH); + const int y = iYNameLogoStart; + + // Team logo + const char *pszNSFLogo = cl_neo_hud_spectator_overlay_nsf_logo.GetString(); + if (pszNSFLogo[0]) + { + if (!vgui::surface()->IsTextureIDValid(m_iTexLogoNSF) || m_iTexLogoNSF == -1) + { + m_iTexLogoNSF = NeoUI::TextureFromFile( + pszNSFLogo, "", NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + } + if (vgui::surface()->IsTextureIDValid(m_iTexLogoNSF) && m_iTexLogoNSF != -1) + { + vgui::surface()->DrawSetColor(COLOR_WHITE); + vgui::surface()->DrawSetTexture(m_iTexLogoNSF); + vgui::surface()->DrawTexturedRect( + x, + y, + x + iLogoWH, + y + iLogoWH); + } + } + else + { + vgui::surface()->DrawSetColor(rsColor); + vgui::surface()->DrawFilledRect(x, y, x + iLogoWH, y + iLogoWH); + } + + // NEO TODO (nullsystem): Deal with very long team name that needs multiline? + // Team name + if (wszNSFName[0]) + { + int iNSFNameWide = 0, iNSFNameTall = 0; + vgui::surface()->GetTextSize(m_hTeamNameFont, wszNSFName, iNSFNameWide, iNSFNameTall); + + const int iNameBoxW = iNamePadW + iNSFNameWide + iNamePadW; + x += (iGapW + iLogoWH); + + DrawNeoHudRoundedBox(x, y, x + iNameBoxW, y + iNameT, COLOR_BLACK_TRANSPARENT); + + vgui::surface()->DrawSetTextFont(m_hTeamNameFont); + vgui::surface()->DrawSetTextColor(COLOR_WHITE); + vgui::surface()->DrawSetTextPos(x + ((iNameBoxW - iNSFNameWide) / 2.0f), y); + vgui::surface()->DrawPrintText(wszNSFName, V_wcslen(wszNSFName)); + } + } + } + const int iTotalCardWide = flWideAs43 / 4.1f; const int iAvatarWH = flWideAs43 / 22.5f; const int iGapWide = flWideAs43 / 120.0f; const int iGapBetween = flWideAs43 / 360.0f; - // NEO TODO (nullsystem): Draw team's name and logo on top + // Draw player cards on the side for (const auto iTeam : {TEAM_JINRAI, TEAM_NSF}) { const bool bIsLeftSide = (iTeam == TEAM_JINRAI); diff --git a/src/game/client/neo/ui/neo_hud_spectator_overlay.h b/src/game/client/neo/ui/neo_hud_spectator_overlay.h index 1d60cfcff..6fc9d7c89 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -7,6 +7,14 @@ class CAvatarImage; extern ConVar cl_neo_hud_spectator_overlay_enabled; +extern ConVar cl_neo_hud_spectator_overlay_jinrai_name; +extern ConVar cl_neo_hud_spectator_overlay_nsf_name; +extern ConVar cl_neo_hud_spectator_overlay_jinrai_logo; +extern ConVar cl_neo_hud_spectator_overlay_nsf_logo; +extern ConVar cl_neo_hud_spectator_overlay_jinrai_matches_won; +extern ConVar cl_neo_hud_spectator_overlay_nsf_matches_won; + +static constexpr const int COMP_MATCHES_WON_MAX = 2; struct SpectatorPlayerCard { @@ -69,6 +77,9 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement int SpecTargetNextEntIdx(const ESpecType eSpecType, EDirection eDirection) const; int m_iEntIndexSelect = 0; + int m_iTexLogoJinrai = -1; + int m_iTexLogoNSF = -1; + protected: void UpdateStateForNeoHudElementDraw() final; void DrawNeoHudElement() final; @@ -88,6 +99,7 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement CPanelAnimationVar(vgui::HFont, m_hRKHPBackFont, "RKHPBackFont", "NHudSpectatorOverlayRoundKillHPBack"); CPanelAnimationVar(vgui::HFont, m_hGhostFont, "GhostFont", "NHudSpectatorOverlayGhost"); CPanelAnimationVar(vgui::HFont, m_hSmallWeaponsFont, "SmallWeaponsFont", "NHudSpectatorOverlaySmallWeapons"); + CPanelAnimationVar(vgui::HFont, m_hTeamNameFont, "TeamNameFont", "HudHintTextLarge"); CPanelAnimationVarAliasType(int, m_iDeadTexture, "DeadTexture", "vgui/hud/kill_kill", "textureid"); int m_iTeamPlayersCount[TEAM__TOTAL] = {}; diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index 4d4ece983..66a3aff6e 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -28,6 +28,7 @@ #include "neo_player_shared.h" #include "steamnetworkingtypes.h" #include "neo_mp3player.h" +#include "neo_hud_spectator_overlay.h" #include #include @@ -777,6 +778,7 @@ void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) &CNeoRoot::MainLoopNewGame, // STATE_NEWGAME &CNeoRoot::MainLoopServerBrowser, // STATE_SERVERBROWSER &CNeoRoot::MainLoopCredits, // STATE_CREDITS + &CNeoRoot::MainLoopOverlay, // STATE_OVERLAY &CNeoRoot::MainLoopMapList, // STATE_MAPLIST &CNeoRoot::MainLoopServerDetails, // STATE_SERVERDETAILS @@ -994,6 +996,22 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) m_state = STATE_SETTINGS; NeoSettingsRestore(&m_ns); } + if (cl_neo_hud_spectator_overlay_enabled.GetBool() + && NeoUI::Button(L"OVERLAY").bPressed) + { + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_jinrai_name.GetString(), + m_wszTeamNameJinrai, sizeof(m_wszTeamNameJinrai)); + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_nsf_name.GetString(), + m_wszTeamNameNSF, sizeof(m_wszTeamNameNSF)); + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_jinrai_logo.GetString(), + m_wszLogoPathJinrai, sizeof(m_wszLogoPathJinrai)); + Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_nsf_logo.GetString(), + m_wszLogoPathNSF, sizeof(m_wszLogoPathNSF)); + m_iMatchesWonJinrai = cl_neo_hud_spectator_overlay_jinrai_matches_won.GetInt(); + m_iMatchesWonNSF = cl_neo_hud_spectator_overlay_nsf_matches_won.GetInt(); + + m_state = STATE_OVERLAY; + } if (NeoUI::Button(m_wszCachedTexts[MMBTN_QUIT]).bPressed || (!IsInGame() && NeoUI::Bind(KEY_ESCAPE))) { m_state = STATE_QUIT; @@ -2250,6 +2268,142 @@ void CNeoRoot::MainLoopCredits(const MainLoopParam param) } } +void CNeoRoot::MainLoopOverlay(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 - g_iRootSubPanelWide) / 2); + g_uiCtx.dPanel.y = ((param.tall - iTallTotal) / 2); + g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 1); + g_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; + NeoUI::BeginContext(&g_uiCtx, param.eMode, L"OVERLAY", "CtxOverlay"); + { + const int iThisRowTall = g_uiCtx.layout.iRowTall; + NeoUI::BeginSection(NeoUI::SECTIONFLAG_DEFAULTFOCUS); + { + FileIODialogMode eNextFileIOMode = FILEIODLGMODE__TOTAL; + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + + NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT, iThisRowTall); + NeoUI::Divider(L"JINRAI"); + NeoUI::SliderInt(L"Matches won:", &m_iMatchesWonJinrai, 0, COMP_MATCHES_WON_MAX); + NeoUI::TextEdit(L"Team name:", m_wszTeamNameJinrai, SZWSZ_LEN(m_wszTeamNameJinrai)); + NeoUI::SetPerRowLayout(2, nullptr, iThisRowTall); + { + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; + if (NeoUI::Button(L"Pick logo").bPressed) + { + eNextFileIOMode = FILEIODLGMODE_TEAMLOGO_JINRAI; + } + if (NeoUI::Button(L"Clear logo").bPressed) + { + m_wszLogoPathJinrai[0] = L'\0'; + } + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; + } + NeoUI::SetPerRowLayout(1, nullptr, iThisRowTall * 4); + static char szLogoPathJinrai[MAX_PATH] = {}; + Q_UnicodeToUTF8(m_wszLogoPathJinrai, szLogoPathJinrai, sizeof(szLogoPathJinrai)); + NeoUI::ImageTexture(szLogoPathJinrai, L"No logo applied", "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL + | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID + | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + + NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT, iThisRowTall); + NeoUI::Divider(L"NSF"); + NeoUI::SliderInt(L"Matches won:", &m_iMatchesWonNSF, 0, COMP_MATCHES_WON_MAX); + NeoUI::TextEdit(L"Team name:", m_wszTeamNameNSF, SZWSZ_LEN(m_wszTeamNameNSF)); + NeoUI::SetPerRowLayout(2, nullptr, iThisRowTall); + { + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; + if (NeoUI::Button(L"Pick logo").bPressed) + { + eNextFileIOMode = FILEIODLGMODE_TEAMLOGO_NSF; + } + if (NeoUI::Button(L"Clear logo").bPressed) + { + m_wszLogoPathNSF[0] = L'\0'; + } + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; + } + NeoUI::SetPerRowLayout(1, nullptr, iThisRowTall * 4); + static char szLogoPathNSF[MAX_PATH] = {}; + Q_UnicodeToUTF8(m_wszLogoPathNSF, szLogoPathNSF, sizeof(szLogoPathNSF)); + NeoUI::ImageTexture(szLogoPathNSF, L"No logo applied", "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL + | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID + | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + + if (FILEIODLGMODE_TEAMLOGO_JINRAI == eNextFileIOMode + || FILEIODLGMODE_TEAMLOGO_NSF == eNextFileIOMode) + { + if (m_pFileIODialog) + { + m_pFileIODialog->MarkForDeletion(); + } + m_pFileIODialog = new vgui::FileOpenDialog(this, + (FILEIODLGMODE_TEAMLOGO_JINRAI == eNextFileIOMode) + ? "Import Jinrai team logo" + : "Import NSF team logo", + vgui::FOD_OPEN); + m_eFileIOMode = eNextFileIOMode; + m_pFileIODialog->AddFilter("*.jpg;*.jpeg;*.png;*.vtf", "Images (JPEG, PNG, VTF)", true); + m_pFileIODialog->AddFilter("*.jpg;*.jpeg", "JPEG Image", false); + m_pFileIODialog->AddFilter("*.png", "PNG Image", false); + m_pFileIODialog->AddFilter("*.vtf", "VTF Image", false); + m_pFileIODialog->DoModal(); + } + } + NeoUI::EndSection(); + g_uiCtx.dPanel.y += g_uiCtx.dPanel.tall; + g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall; + NeoUI::BeginSection(NeoUI::SECTIONFLAG_EXCLUDECONTROLLER); + { + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; + NeoUI::SetPerRowLayout(5, nullptr, iThisRowTall); + { + if (NeoUI::Button(L"Back (ESC)").bPressed || NeoUI::BindKeyBack()) + { + m_state = STATE_ROOT; + } + NeoUI::Pad(); + NeoUI::Pad(); + NeoUI::Pad(); + if (NeoUI::Button(L"Save (F8)").bPressed || NeoUI::Bind(KEY_F8)) + { + { + char szTeamNameJinrai[MAX_PLAYER_NAME_LENGTH] = {}; + Q_UnicodeToUTF8(m_wszTeamNameJinrai, szTeamNameJinrai, sizeof(szTeamNameJinrai)); + cl_neo_hud_spectator_overlay_jinrai_name.SetValue(szTeamNameJinrai); + } + { + char szTeamNameNSF[MAX_PLAYER_NAME_LENGTH] = {}; + Q_UnicodeToUTF8(m_wszTeamNameNSF, szTeamNameNSF, sizeof(szTeamNameNSF)); + cl_neo_hud_spectator_overlay_nsf_name.SetValue(szTeamNameNSF); + } + { + char szLogoPathJinrai[MAX_PATH] = {}; + Q_UnicodeToUTF8(m_wszLogoPathJinrai, szLogoPathJinrai, sizeof(szLogoPathJinrai)); + cl_neo_hud_spectator_overlay_jinrai_logo.SetValue(szLogoPathJinrai); + } + { + char szLogoPathNSF[MAX_PATH] = {}; + Q_UnicodeToUTF8(m_wszLogoPathNSF, szLogoPathNSF, sizeof(szLogoPathNSF)); + cl_neo_hud_spectator_overlay_nsf_logo.SetValue(szLogoPathNSF); + } + cl_neo_hud_spectator_overlay_jinrai_matches_won.SetValue(m_iMatchesWonJinrai); + cl_neo_hud_spectator_overlay_nsf_matches_won.SetValue(m_iMatchesWonNSF); + + m_state = STATE_ROOT; + } + } + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + } + NeoUI::EndSection(); + } +} + void CNeoRoot::MainLoopMapList(const MainLoopParam param) { const int iTallTotal = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 2); @@ -2986,6 +3140,23 @@ void CNeoRoot::ReadNewsFile(CUtlBuffer &buf) } } +static void OnFileSelectedTeamLogo(const char *szFullpath) +{ + switch (g_pNeoRoot->m_eFileIOMode) + { + case CNeoRoot::FILEIODLGMODE_TEAMLOGO_JINRAI: + Q_UTF8ToUnicode(szFullpath, + g_pNeoRoot->m_wszLogoPathJinrai, sizeof(g_pNeoRoot->m_wszLogoPathJinrai)); + break; + case CNeoRoot::FILEIODLGMODE_TEAMLOGO_NSF: + Q_UTF8ToUnicode(szFullpath, + g_pNeoRoot->m_wszLogoPathNSF, sizeof(g_pNeoRoot->m_wszLogoPathNSF)); + break; + default: + break; + } +} + static void OnFileSelectedMode_Spray(const char *szFullpath) { // Ensure the directories are there to write to @@ -3177,8 +3348,10 @@ void CNeoRoot::OnFileSelected(const char *szFullpath) { static void (*FILESELMODEFNS[FILEIODLGMODE__TOTAL])(const char *) = { &OnFileSelectedMode_Spray, // FILEIODLGMODE_SPRAY - &ServerBlacklistRead, // FILEIODLGMODE_BLACKLIST_IMPORT - &ServerBlacklistWrite, // FILEIODLGMODE_BLACKLIST_EXPORT + &ServerBlacklistRead, // FILEIODLGMODE_BLACKLIST_IMPORT + &ServerBlacklistWrite, // FILEIODLGMODE_BLACKLIST_EXPORT + &OnFileSelectedTeamLogo, // FILEIODLGMODE_TEAMLOGO_JINRAI + &OnFileSelectedTeamLogo, // FILEIODLGMODE_TEAMLOGO_NSF }; (*FILESELMODEFNS[m_eFileIOMode])(szFullpath); } diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index 97dddccb8..6af64679f 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -72,6 +72,7 @@ enum RootState STATE_NEWGAME, STATE_SERVERBROWSER, STATE_CREDITS, + STATE_OVERLAY, // Those that are not the main states goes under here STATE__SUBSTATES, @@ -163,6 +164,7 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener void MainLoopNewGame(const MainLoopParam param); void MainLoopServerBrowser(const MainLoopParam param); void MainLoopCredits(const MainLoopParam param); + void MainLoopOverlay(const MainLoopParam param); void MainLoopMapList(const MainLoopParam param); void MainLoopServerDetails(const MainLoopParam param); void MainLoopSprayPicker(const MainLoopParam param); @@ -220,6 +222,8 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener FILEIODLGMODE_SPRAY = 0, FILEIODLGMODE_BLACKLIST_IMPORT, FILEIODLGMODE_BLACKLIST_EXPORT, + FILEIODLGMODE_TEAMLOGO_JINRAI, + FILEIODLGMODE_TEAMLOGO_NSF, FILEIODLGMODE__TOTAL, }; @@ -274,6 +278,13 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener float m_flHtBtnCodeUpdate = 0.0f; CUtlHashtable m_htButtonCodeToAction; + + wchar_t m_wszTeamNameJinrai[MAX_PLAYER_NAME_LENGTH] = {}; + wchar_t m_wszTeamNameNSF[MAX_PLAYER_NAME_LENGTH] = {}; + wchar_t m_wszLogoPathJinrai[MAX_PATH] = {}; + wchar_t m_wszLogoPathNSF[MAX_PATH] = {}; + int m_iMatchesWonJinrai = 0; + int m_iMatchesWonNSF = 0; }; extern CNeoRoot *g_pNeoRoot; diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index a190ae0ea..66bba454a 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -10,6 +10,7 @@ #include #include "neo_misc.h" +#include "neo_utils.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -1484,7 +1485,7 @@ void Label(const wchar_t *wszLabel, const wchar_t *wszText) 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 ButtonFlags flags, const float flScrollStart, const TextureOptFlags texFlags) { const auto wdgState = BeginWidget(WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE); @@ -1578,7 +1579,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const int iTexYTall = c->irWidgetTall * 0.75f; Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, c->irWidgetWide, iTexYTall, - szTextureGroup); + szTextureGroup, texFlags); const auto *pFontI = &c->fonts[c->eFont]; const int x = XPosFromText(wszText, pFontI, TEXTSTYLE_CENTER); @@ -1591,7 +1592,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, { Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, c->irWidgetWide, c->irWidgetTall, - szTextureGroup); + szTextureGroup, texFlags); } } break; case BASEBUTTONTYPE_CHECKBOX: @@ -1680,7 +1681,8 @@ NeoUI::RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText) return ret; } -void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg, const char *szTextureGroup) +void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg, + const char *szTextureGroup, const TextureOptFlags flags) { const auto wdgState = BeginWidget(WIDGETFLAG_SKIPACTIVE); if (wdgState.bInView && c->eMode == MODE_PAINT) @@ -1688,7 +1690,8 @@ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg, const c const bool bHasTexture = Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, c->irWidgetWide, c->irWidgetTall, - szTextureGroup); + szTextureGroup, + flags); if (!bHasTexture) { Label(wszErrorMsg, true); @@ -1697,89 +1700,103 @@ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg, const c EndWidget(wdgState); } -bool Texture(const char *szTexturePath, const int x, const int y, const int width, const int height, - const char *szTextureGroup, const TextureOptFlags texFlags) +int TextureFromFile(const char *szTexturePath, const char *szTextureGroup, + const TextureOptFlags texFlags) { - auto hdl = c->htTexMap.Find(szTexturePath); - if (hdl == c->htTexMap.InvalidHandle() && c->eMode == MODE_PAINT) + int iTex = -1; + if (V_striEndsWith(szTexturePath, ".png") || V_striEndsWith(szTexturePath, ".jpg") || + V_striEndsWith(szTexturePath, ".jpeg")) { - bool bApplied = false; - int iTex = -1; - if (V_striEndsWith(szTexturePath, ".png") || V_striEndsWith(szTexturePath, ".jpg") || - V_striEndsWith(szTexturePath, ".jpeg")) - { - // General images decoded via stb_image - int width, height, channels; - char szFullTexturePath[MAX_PATH] = {}; + // General images decoded via stb_image + int width, height, channels; + char szFullTexturePath[MAX_PATH] = {}; #ifdef _WIN32 - if (V_isalpha(szTexturePath[0]) - && szTexturePath[1] == ':' - && (szTexturePath[2] == '\\' || szTexturePath[2] == '/')) + if (V_isalpha(szTexturePath[0]) + && szTexturePath[1] == ':' + && (szTexturePath[2] == '\\' || szTexturePath[2] == '/')) #else - if (szTexturePath[0] == '/') + 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) { - V_strcpy_safe(szFullTexturePath, szTexturePath); + uint8 *rgbaData = reinterpret_cast(calloc(width * height, sizeof(uint8) * 4)); + ImageLoader::ConvertImageFormat(data, IMAGE_FORMAT_RGB888, + rgbaData, IMAGE_FORMAT_RGBA8888, + width, height); + stbi_image_free(data); + data = rgbaData; + channels = 4; } - else + else if (channels != 4) { - filesystem->RelativePathToFullPath_safe(szTexturePath, szTextureGroup, szFullTexturePath); + Assert(false); } - uint8 *data = stbi_load(szFullTexturePath, &width, &height, &channels, 0); - if (data) + if (texFlags & NeoUI::TEXTUREOPTFLAGS_RESIZETO256) { - if (channels == 3) - { - uint8 *rgbaData = reinterpret_cast(calloc(width * height, sizeof(uint8) * 4)); - ImageLoader::ConvertImageFormat(data, IMAGE_FORMAT_RGB888, - rgbaData, IMAGE_FORMAT_RGBA8888, - width, height); - stbi_image_free(data); - data = rgbaData; - channels = 4; - } - else if (channels != 4) - { - Assert(false); - } - iTex = vgui::surface()->CreateNewTextureID(true); - vgui::surface()->DrawSetTextureRGBAEx(iTex, data, width, height, IMAGE_FORMAT_RGBA8888); - stbi_image_free(data); - bApplied = true; + uint8 *newData = NeoUtils::CropScaleTo256(data, width, height); + free(data); + data = newData; + width = NeoUtils::SPRAY_WH; + height = NeoUtils::SPRAY_WH; } + iTex = vgui::surface()->CreateNewTextureID(true); + vgui::surface()->DrawSetTextureRGBAEx(iTex, data, width, height, IMAGE_FORMAT_RGBA8888); + stbi_image_free(data); } - else if (V_striEndsWith(szTexturePath, ".vtf")) + } + else if (V_striEndsWith(szTexturePath, ".vtf")) + { + // Direct vtf file + CUtlBuffer buf(0, 0, CUtlBuffer::READ_ONLY); + if (filesystem->ReadFile(szTexturePath, nullptr, buf)) { - // Direct vtf file - CUtlBuffer buf(0, 0, CUtlBuffer::READ_ONLY); - if (filesystem->ReadFile(szTexturePath, nullptr, buf)) + IVTFTexture *pVTFTexture = CreateVTFTexture(); + if (pVTFTexture) { - IVTFTexture *pVTFTexture = CreateVTFTexture(); - if (pVTFTexture) + if (pVTFTexture->Unserialize(buf)) { - if (pVTFTexture->Unserialize(buf)) - { - pVTFTexture->ConvertImageFormat(IMAGE_FORMAT_RGBA8888, false); - iTex = vgui::surface()->CreateNewTextureID(true); - vgui::surface()->DrawSetTextureRGBAEx(iTex, pVTFTexture->ImageData(0, 0, 0), - pVTFTexture->Width(), pVTFTexture->Height(), - IMAGE_FORMAT_RGBA8888); - bApplied = true; - } - DestroyVTFTexture(pVTFTexture); + pVTFTexture->ConvertImageFormat(IMAGE_FORMAT_RGBA8888, false); + iTex = vgui::surface()->CreateNewTextureID(true); + vgui::surface()->DrawSetTextureRGBAEx(iTex, pVTFTexture->ImageData(0, 0, 0), + pVTFTexture->Width(), pVTFTexture->Height(), + IMAGE_FORMAT_RGBA8888); } + DestroyVTFTexture(pVTFTexture); } } - else if (!IsErrorMaterial(materials->FindMaterial(szTexturePath, szTextureGroup))) + } + else if (!IsErrorMaterial(materials->FindMaterial(szTexturePath, szTextureGroup))) + { + // Direct texture determined by vmt (without extension) + iTex = vgui::surface()->CreateNewTextureID(true); + vgui::surface()->DrawSetTextureFile(iTex, szTexturePath, false, true); + } + return iTex; +} + +bool Texture(const char *szTexturePath, const int x, const int y, const int width, const int height, + const char *szTextureGroup, const TextureOptFlags texFlags) +{ + auto hdl = c->htTexMap.Find(szTexturePath); + if (hdl == c->htTexMap.InvalidHandle() && c->eMode == MODE_PAINT) + { + const int iTex = TextureFromFile(szTexturePath, szTextureGroup, texFlags); + if ((vgui::surface()->IsTextureIDValid(iTex) && iTex != -1) + || !(texFlags & TEXTUREOPTFLAGS_DONOTCACHEIFINVALID)) { - // Direct texture determined by vmt (without extension) - iTex = vgui::surface()->CreateNewTextureID(true); - vgui::surface()->DrawSetTextureFile(iTex, szTexturePath, false, true); - bApplied = true; + hdl = c->htTexMap.Insert(szTexturePath, iTex); } - hdl = c->htTexMap.Insert(szTexturePath, (bApplied) ? iTex : -1); } - if (hdl != c->htTexMap.InvalidHandle()) { const int iTex = c->htTexMap.Element(hdl); @@ -1850,9 +1867,10 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt } NeoUI::RetButton ButtonTexture(const char *szTexturePath, const char *szTextureGroup, - const wchar_t *wszText) + const wchar_t *wszText, const TextureOptFlags flags) { - return BaseButton(wszText, szTexturePath, szTextureGroup, BASEBUTTONTYPE_IMAGE); + return BaseButton(wszText, szTexturePath, szTextureGroup, BASEBUTTONTYPE_IMAGE, + false, BUTTONFLAG_NONE, 0.0f, flags); } NeoUI::RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal) diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index f7dd16769..e0753b220 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -573,15 +573,27 @@ struct TabsState int iOffset; bool bInYScroll; }; + +enum TextureOptFlag_ +{ + TEXTUREOPTFLAGS_NONE = 0, + TEXTUREOPTFLAGS_DONOTCROPTOPANEL = 1 << 0, // Disable cropping based on panel's dimension + TEXTUREOPTFLAGS_DONOTCACHEIFINVALID = 1 << 1, // Don't insert to htTexMap cache if invalid + TEXTUREOPTFLAGS_RESIZETO256 = 1 << 2, // Resize texture to 256px square using NeoUtils::CropScaleTo256 +}; +typedef int TextureOptFlags; + /*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, const char *szTextureGroup, - const EBaseButtonType eType, const bool bVal = false, const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f); + const EBaseButtonType eType, const bool bVal = false, + const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f, + const TextureOptFlags texFlags = TEXTUREOPTFLAGS_NONE); /*1W*/ RetButton Button(const wchar_t *wszText); /*2W*/ RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText); /*1W*/ RetButton ButtonTexture(const char *szTexturePath, const char *szTextureGroup = "", - const wchar_t *wszText = L""); + const wchar_t *wszText = L"", const TextureOptFlags flags = TEXTUREOPTFLAGS_NONE); /*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); @@ -611,7 +623,7 @@ enum TextEditFlag_ typedef int TextEditFlags; /*1W*/ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); /*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); -/*SW*/ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg = L"", const char *szTextureGroup = ""); +/*SW*/ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg = L"", const char *szTextureGroup = "", const TextureOptFlags flags = TEXTUREOPTFLAGS_NONE); // Table widgets + functionalities // NEO TODO (nullsystem): iColProportions non-const, resizable within TableHeader @@ -650,15 +662,14 @@ void BeginIgnoreXOffset(); void EndIgnoreXOffset(); // NeoUI::Texture is non-widget, but utilizes NeoUI's image/texture handling -enum TextureOptFlags -{ - TEXTUREOPTFLAGS_NONE = 0, - TEXTUREOPTFLAGS_DONOTCROPTOPANEL = 1, // Disable cropping based on panel's dimension -}; bool Texture(const char *szTexturePath, const int x, const int y, const int width, const int height, const char *szTextureGroup = "", const TextureOptFlags texFlags = TEXTUREOPTFLAGS_NONE); void ResetTextures(); +// NeoUI::TextureFromFile does not relies on NeoUI context and can be used anywhere +int TextureFromFile(const char *szTexturePath, const char *szTextureGroup = "", + const TextureOptFlags texFlags = TEXTUREOPTFLAGS_NONE); + // Non-widgets/convenience functions bool Bind(const ButtonCode_t eCode); bool Bind(const ButtonCode_t *peCode, const int ieCodeSize); From ff62829e8f9414ebf1af9797b9c7a8b3cb1323af Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:23:25 +0100 Subject: [PATCH 2/4] Add swap teams button --- src/game/client/neo/ui/neo_root.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index 66a3aff6e..283f14d63 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -2369,7 +2369,26 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) } NeoUI::Pad(); NeoUI::Pad(); - NeoUI::Pad(); + if (NeoUI::Button(L"Swap Teams").bPressed) + { + wchar_t wszTeamNameJinraiTemp[MAX_PLAYER_NAME_LENGTH] = {}; + wchar_t wszLogoPathJinraiTemp[MAX_PATH] = {}; + + // Jinrai -> Jinrai-Temp + V_wcscpy_safe(wszTeamNameJinraiTemp, m_wszTeamNameJinrai); + V_wcscpy_safe(wszLogoPathJinraiTemp, m_wszLogoPathJinrai); + const int iMatchesWonJinraiTemp = m_iMatchesWonJinrai; + + // NSF -> Jinrai + V_wcscpy_safe(m_wszTeamNameJinrai, m_wszTeamNameNSF); + V_wcscpy_safe(m_wszLogoPathJinrai, m_wszLogoPathNSF); + m_iMatchesWonJinrai = m_iMatchesWonNSF; + + // Jinrai-Temp -> NSF + V_wcscpy_safe(m_wszTeamNameNSF, wszTeamNameJinraiTemp); + V_wcscpy_safe(m_wszLogoPathNSF, wszLogoPathJinraiTemp); + m_iMatchesWonNSF = iMatchesWonJinraiTemp; + } if (NeoUI::Button(L"Save (F8)").bPressed || NeoUI::Bind(KEY_F8)) { { From 3636d6d49abd0114c23d9d121daf47f2033aa985 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:39:15 +0100 Subject: [PATCH 3/4] Use sz instead of wsz for logo path. No point in using wchar_t when not displaying as string. --- src/game/client/neo/ui/neo_root.cpp | 44 +++++++++-------------------- src/game/client/neo/ui/neo_root.h | 4 +-- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index 283f14d63..721ed7d82 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -1003,10 +1003,8 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) m_wszTeamNameJinrai, sizeof(m_wszTeamNameJinrai)); Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_nsf_name.GetString(), m_wszTeamNameNSF, sizeof(m_wszTeamNameNSF)); - Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_jinrai_logo.GetString(), - m_wszLogoPathJinrai, sizeof(m_wszLogoPathJinrai)); - Q_UTF8ToUnicode(cl_neo_hud_spectator_overlay_nsf_logo.GetString(), - m_wszLogoPathNSF, sizeof(m_wszLogoPathNSF)); + V_strcpy_safe(m_szLogoPathJinrai, cl_neo_hud_spectator_overlay_jinrai_logo.GetString()); + V_strcpy_safe(m_szLogoPathNSF, cl_neo_hud_spectator_overlay_nsf_logo.GetString()); m_iMatchesWonJinrai = cl_neo_hud_spectator_overlay_jinrai_matches_won.GetInt(); m_iMatchesWonNSF = cl_neo_hud_spectator_overlay_nsf_matches_won.GetInt(); @@ -2297,14 +2295,12 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) } if (NeoUI::Button(L"Clear logo").bPressed) { - m_wszLogoPathJinrai[0] = L'\0'; + m_szLogoPathJinrai[0] = '\0'; } g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; } NeoUI::SetPerRowLayout(1, nullptr, iThisRowTall * 4); - static char szLogoPathJinrai[MAX_PATH] = {}; - Q_UnicodeToUTF8(m_wszLogoPathJinrai, szLogoPathJinrai, sizeof(szLogoPathJinrai)); - NeoUI::ImageTexture(szLogoPathJinrai, L"No logo applied", "", + NeoUI::ImageTexture(m_szLogoPathJinrai, L"No logo applied", "", NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); @@ -2322,14 +2318,12 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) } if (NeoUI::Button(L"Clear logo").bPressed) { - m_wszLogoPathNSF[0] = L'\0'; + m_szLogoPathNSF[0] = '\0'; } g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; } NeoUI::SetPerRowLayout(1, nullptr, iThisRowTall * 4); - static char szLogoPathNSF[MAX_PATH] = {}; - Q_UnicodeToUTF8(m_wszLogoPathNSF, szLogoPathNSF, sizeof(szLogoPathNSF)); - NeoUI::ImageTexture(szLogoPathNSF, L"No logo applied", "", + NeoUI::ImageTexture(m_szLogoPathNSF, L"No logo applied", "", NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); @@ -2372,21 +2366,21 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) if (NeoUI::Button(L"Swap Teams").bPressed) { wchar_t wszTeamNameJinraiTemp[MAX_PLAYER_NAME_LENGTH] = {}; - wchar_t wszLogoPathJinraiTemp[MAX_PATH] = {}; + char szLogoPathJinraiTemp[MAX_PATH] = {}; // Jinrai -> Jinrai-Temp V_wcscpy_safe(wszTeamNameJinraiTemp, m_wszTeamNameJinrai); - V_wcscpy_safe(wszLogoPathJinraiTemp, m_wszLogoPathJinrai); + V_strcpy_safe(szLogoPathJinraiTemp, m_szLogoPathJinrai); const int iMatchesWonJinraiTemp = m_iMatchesWonJinrai; // NSF -> Jinrai V_wcscpy_safe(m_wszTeamNameJinrai, m_wszTeamNameNSF); - V_wcscpy_safe(m_wszLogoPathJinrai, m_wszLogoPathNSF); + V_strcpy_safe(m_szLogoPathJinrai, m_szLogoPathNSF); m_iMatchesWonJinrai = m_iMatchesWonNSF; // Jinrai-Temp -> NSF V_wcscpy_safe(m_wszTeamNameNSF, wszTeamNameJinraiTemp); - V_wcscpy_safe(m_wszLogoPathNSF, wszLogoPathJinraiTemp); + V_strcpy_safe(m_szLogoPathNSF, szLogoPathJinraiTemp); m_iMatchesWonNSF = iMatchesWonJinraiTemp; } if (NeoUI::Button(L"Save (F8)").bPressed || NeoUI::Bind(KEY_F8)) @@ -2401,16 +2395,8 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) Q_UnicodeToUTF8(m_wszTeamNameNSF, szTeamNameNSF, sizeof(szTeamNameNSF)); cl_neo_hud_spectator_overlay_nsf_name.SetValue(szTeamNameNSF); } - { - char szLogoPathJinrai[MAX_PATH] = {}; - Q_UnicodeToUTF8(m_wszLogoPathJinrai, szLogoPathJinrai, sizeof(szLogoPathJinrai)); - cl_neo_hud_spectator_overlay_jinrai_logo.SetValue(szLogoPathJinrai); - } - { - char szLogoPathNSF[MAX_PATH] = {}; - Q_UnicodeToUTF8(m_wszLogoPathNSF, szLogoPathNSF, sizeof(szLogoPathNSF)); - cl_neo_hud_spectator_overlay_nsf_logo.SetValue(szLogoPathNSF); - } + cl_neo_hud_spectator_overlay_jinrai_logo.SetValue(m_szLogoPathJinrai); + cl_neo_hud_spectator_overlay_nsf_logo.SetValue(m_szLogoPathNSF); cl_neo_hud_spectator_overlay_jinrai_matches_won.SetValue(m_iMatchesWonJinrai); cl_neo_hud_spectator_overlay_nsf_matches_won.SetValue(m_iMatchesWonNSF); @@ -3164,12 +3150,10 @@ static void OnFileSelectedTeamLogo(const char *szFullpath) switch (g_pNeoRoot->m_eFileIOMode) { case CNeoRoot::FILEIODLGMODE_TEAMLOGO_JINRAI: - Q_UTF8ToUnicode(szFullpath, - g_pNeoRoot->m_wszLogoPathJinrai, sizeof(g_pNeoRoot->m_wszLogoPathJinrai)); + V_strcpy_safe(g_pNeoRoot->m_szLogoPathJinrai, szFullpath); break; case CNeoRoot::FILEIODLGMODE_TEAMLOGO_NSF: - Q_UTF8ToUnicode(szFullpath, - g_pNeoRoot->m_wszLogoPathNSF, sizeof(g_pNeoRoot->m_wszLogoPathNSF)); + V_strcpy_safe(g_pNeoRoot->m_szLogoPathNSF, szFullpath); break; default: break; diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index 6af64679f..7f34da7db 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -281,8 +281,8 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener wchar_t m_wszTeamNameJinrai[MAX_PLAYER_NAME_LENGTH] = {}; wchar_t m_wszTeamNameNSF[MAX_PLAYER_NAME_LENGTH] = {}; - wchar_t m_wszLogoPathJinrai[MAX_PATH] = {}; - wchar_t m_wszLogoPathNSF[MAX_PATH] = {}; + char m_szLogoPathJinrai[MAX_PATH] = {}; + char m_szLogoPathNSF[MAX_PATH] = {}; int m_iMatchesWonJinrai = 0; int m_iMatchesWonNSF = 0; }; From 341c1a68c3be096ef0d487510974d6985af9be3d Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:03:43 +0100 Subject: [PATCH 4/4] Add Jinrai/NSF default logo, remember last path --- src/game/client/neo/ui/neo_hud_spectator_overlay.cpp | 12 ++++++++++++ src/game/client/neo/ui/neo_root.cpp | 8 ++++++++ src/game/client/neo/ui/neo_root.h | 1 + 3 files changed, 21 insertions(+) diff --git a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp index 2767bc764..d0d1ced99 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -913,6 +913,12 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() { vgui::surface()->DrawSetColor(rsColor); vgui::surface()->DrawFilledRect(x, y, x + iLogoWH, y + iLogoWH); + if (pRoundStateHUD) + { + vgui::surface()->DrawSetColor(COLOR_FADED_DARK); + vgui::surface()->DrawSetTexture(pRoundStateHUD->m_teamLogoColors[TEAM_JINRAI].logo); + vgui::surface()->DrawTexturedRect(x, y, x + iLogoWH, y + iLogoWH); + } } // NEO TODO (nullsystem): Deal with very long team name that needs multiline? @@ -978,6 +984,12 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() { vgui::surface()->DrawSetColor(rsColor); vgui::surface()->DrawFilledRect(x, y, x + iLogoWH, y + iLogoWH); + if (pRoundStateHUD) + { + vgui::surface()->DrawSetColor(COLOR_FADED_DARK); + vgui::surface()->DrawSetTexture(pRoundStateHUD->m_teamLogoColors[TEAM_NSF].logo); + vgui::surface()->DrawTexturedRect(x, y, x + iLogoWH, y + iLogoWH); + } } // NEO TODO (nullsystem): Deal with very long team name that needs multiline? diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index 721ed7d82..1682d3a13 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -2340,6 +2340,10 @@ void CNeoRoot::MainLoopOverlay(const MainLoopParam param) ? "Import Jinrai team logo" : "Import NSF team logo", vgui::FOD_OPEN); + if (m_szNextLogoPathStartDir[0]) + { + m_pFileIODialog->SetStartDirectory(m_szNextLogoPathStartDir); + } m_eFileIOMode = eNextFileIOMode; m_pFileIODialog->AddFilter("*.jpg;*.jpeg;*.png;*.vtf", "Images (JPEG, PNG, VTF)", true); m_pFileIODialog->AddFilter("*.jpg;*.jpeg", "JPEG Image", false); @@ -3156,8 +3160,12 @@ static void OnFileSelectedTeamLogo(const char *szFullpath) V_strcpy_safe(g_pNeoRoot->m_szLogoPathNSF, szFullpath); break; default: + Assert(false); break; } + + V_strcpy_safe(g_pNeoRoot->m_szNextLogoPathStartDir, szFullpath); + V_StripFilename(g_pNeoRoot->m_szNextLogoPathStartDir); } static void OnFileSelectedMode_Spray(const char *szFullpath) diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index 7f34da7db..6da806571 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -285,6 +285,7 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener char m_szLogoPathNSF[MAX_PATH] = {}; int m_iMatchesWonJinrai = 0; int m_iMatchesWonNSF = 0; + char m_szNextLogoPathStartDir[MAX_PATH] = {}; }; extern CNeoRoot *g_pNeoRoot;