From 3803da0508232a0a3d95153dce35a4add6d64f53 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:06:21 +0100 Subject: [PATCH 01/17] Native spectator overlay Works with demos and live, native implementation of the competitve spectator overlay which have player cards side-to-side on screen. Features, coloring, and fades/animations as close to the likeness of the websocket overlay, but not completely exact. * TODO: Show full equipments (like in OG:NT websocket overlay) * TODO: Check Windows (fonts) * TODO: Check 720p, 1440p, 4k. Use x0.75/x0.5 from 1080p * TODO: Separate PR: Top area team name + logos * TODO: Separate PR: NeoUI setting UI to toggle + setup team name + logos --- game/neo/resource/ClientScheme.res | 297 +++++++ game/neo/scripts/HudLayout.res | 5 + src/game/client/CMakeLists.txt | 3 + src/game/client/hud.cpp | 2 +- .../client/neo/c_neo_killer_damage_infos.h | 2 - .../client/neo/ui/neo_hud_deathnotice.cpp | 55 +- src/game/client/neo/ui/neo_hud_deathnotice.h | 51 ++ .../neo/ui/neo_hud_spectator_overlay.cpp | 725 ++++++++++++++++++ .../client/neo/ui/neo_hud_spectator_overlay.h | 71 ++ src/game/shared/neo/neo_gamerules.cpp | 23 +- src/game/shared/neo/neo_gamerules.h | 1 + src/game/shared/neo/neo_player_shared.h | 2 + .../weapons/weapon_neobasecombatweapon.cpp | 14 + .../neo/weapons/weapon_neobasecombatweapon.h | 2 + 14 files changed, 1186 insertions(+), 67 deletions(-) create mode 100644 src/game/client/neo/ui/neo_hud_deathnotice.h create mode 100644 src/game/client/neo/ui/neo_hud_spectator_overlay.cpp create mode 100644 src/game/client/neo/ui/neo_hud_spectator_overlay.h diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index c3f9b7d57e..7002518cb4 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -594,6 +594,303 @@ Scheme } } + NHudSpectatorOverlayName + { + "1" + { + "name" "Verdana" + "tall" "9" [!$LINUX] + "tall" "12" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "1 599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "2" + { + "name" "Verdana" + "tall" "11" [!$LINUX] + "tall" "14" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "600 767" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "3" + { + "name" "Verdana" + "tall" "14" [!$LINUX] + "tall" "18" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "4" + { + "name" "Verdana" + "tall" "16" [!$LINUX] + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "5" + { + "name" "Verdana" + "tall" "18" [!$LINUX] + "tall" "24" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1200 10000" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + } + + NHudSpectatorOverlayInfo + { + "1" + { + "name" "Verdana" + "tall" "8" [!$LINUX] + "tall" "10" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "1 599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "2" + { + "name" "Verdana" + "tall" "10" [!$LINUX] + "tall" "13" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "600 767" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "3" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "15" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "4" + { + "name" "Verdana" + "tall" "16" [!$LINUX] + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "5" + { + "name" "Verdana" + "tall" "17" [!$LINUX] + "tall" "22" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1200 10000" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + } + + NHudSpectatorOverlayClass + { + "1" + { + "name" "Verdana" + "tall" "8" [!$LINUX] + "tall" "10" [$LINUX] + "weight" "500" + "antialias" "1" + "yres" "1 599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "2" + { + "name" "Verdana" + "tall" "9" [!$LINUX] + "tall" "12" [$LINUX] + "weight" "500" + "antialias" "1" + "yres" "600 767" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "3" + { + "name" "Verdana" + "tall" "11" [!$LINUX] + "tall" "14" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "768 1023" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "4" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "16" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "1024 1199" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "5" + { + "name" "Verdana" + "tall" "16" [!$LINUX] + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1200 10000" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + } + + NHudSpectatorOverlayHP + { + "1" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "14" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "1 599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "2" + { + "name" "Verdana" + "tall" "14" [!$LINUX] + "tall" "16" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "600 767" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "additive" "1" + } + "3" + { + "name" "Verdana" + "tall" "16" [!$LINUX] + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "4" + { + "name" "Verdana" + "tall" "20" [!$LINUX] + "tall" "24" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "5" + { + "name" "Verdana" + "tall" "22" [!$LINUX] + "tall" "26" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1200 10000" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + } + + NHudSpectatorOverlayGhost + { + "1" + { + "name" "killfeedicons" + "tall" "28" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "480 599" + "additive" "0" + } + "2" + { + "name" "killfeedicons" + "tall" "32" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "600 767" + "additive" "0" + } + "3" + { + "name" "killfeedicons" + "tall" "36" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "768 1023" + "antialias" "1" + "additive" "0" + } + "4" + { + "name" "killfeedicons" + "tall" "40" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1024 1199" + "antialias" "1" + "additive" "0" + } + "5" + { + "name" "killfeedicons" + "tall" "44" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1200 1440" + "antialias" "1" + "additive" "0" + } + "6" + { + "name" "killfeedicons" + "tall" "48" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1441 1599" + "antialias" "1" + "additive" "0" + } + "7" + { + "name" "killfeedicons" + "tall" "52" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1600 6000" + "antialias" "1" + "additive" "0" + } + } + + BudgetLabel { "1" diff --git a/game/neo/scripts/HudLayout.res b/game/neo/scripts/HudLayout.res index d692ff1c01..4f409b7356 100644 --- a/game/neo/scripts/HudLayout.res +++ b/game/neo/scripts/HudLayout.res @@ -1013,4 +1013,9 @@ "box_color" "20 20 20 0" "text_color" "255 255 255 255" } + + neo_spectator_overlay + { + "fieldName" "neo_spectator_overlay" + } } diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt index 7fbb698e43..947d08280f 100644 --- a/src/game/client/CMakeLists.txt +++ b/src/game/client/CMakeLists.txt @@ -1667,6 +1667,7 @@ set(UNITY_SOURCE_NEO_UI neo/ui/neo_hud_message.cpp neo/ui/neo_hud_player_ping.cpp neo/ui/neo_hud_round_state.cpp + neo/ui/neo_hud_spectator_overlay.cpp neo/ui/neo_hud_startup_sequence.cpp neo/ui/neo_hud_worldpos_marker.cpp neo/ui/neo_hud_worldpos_marker_generic.cpp @@ -1694,6 +1695,7 @@ target_sources_grouped( neo/ui/neo_hud_ammo.h neo/ui/neo_hud_childelement.h neo/ui/neo_hud_compass.h + neo/ui/neo_hud_deathnotice.h neo/ui/neo_hud_friendly_marker.h neo/ui/neo_hud_game_event.h neo/ui/neo_hud_ghost_beacons.h @@ -1706,6 +1708,7 @@ target_sources_grouped( neo/ui/neo_hud_message.h neo/ui/neo_hud_player_ping.h neo/ui/neo_hud_round_state.h + neo/ui/neo_hud_spectator_overlay.h neo/ui/neo_hud_startup_sequence.h neo/ui/neo_hud_worldpos_marker.h neo/ui/neo_hud_worldpos_marker_generic.h diff --git a/src/game/client/hud.cpp b/src/game/client/hud.cpp index 6269bd848e..824b35c2a8 100644 --- a/src/game/client/hud.cpp +++ b/src/game/client/hud.cpp @@ -500,7 +500,7 @@ void CHud::InitFonts() #ifdef NEO g_hFontKillfeed = pScheme->GetFont("NHudOCRSmallNoAdditive", true); - g_hFontKillfeedIcons = pScheme->GetFont("NHudKillFeedIcons", true); + g_hFontKillfeedIcons = pScheme->GetFont("NHudKillfeedIcons", true); if (g_hFontKillfeed == vgui::INVALID_FONT) { diff --git a/src/game/client/neo/c_neo_killer_damage_infos.h b/src/game/client/neo/c_neo_killer_damage_infos.h index cbabf40dec..6bd7a984b4 100644 --- a/src/game/client/neo/c_neo_killer_damage_infos.h +++ b/src/game/client/neo/c_neo_killer_damage_infos.h @@ -3,8 +3,6 @@ #include "shareddefs.h" #include "neo_player_shared.h" -static constexpr const int WEP_NAME_MAXSTRLEN = 32; - struct CNEOKillerInfos { bool bHasDmgInfos = false; diff --git a/src/game/client/neo/ui/neo_hud_deathnotice.cpp b/src/game/client/neo/ui/neo_hud_deathnotice.cpp index 463bcb68ce..a4dc4a1034 100644 --- a/src/game/client/neo/ui/neo_hud_deathnotice.cpp +++ b/src/game/client/neo/ui/neo_hud_deathnotice.cpp @@ -24,6 +24,7 @@ #include "takedamageinfo.h" #include "c_neo_killer_damage_infos.h" #include "neo_scoreboard.h" +#include "neo_hud_deathnotice.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -31,6 +32,9 @@ static ConVar hud_deathnotice_time( "hud_deathnotice_time", "20", 0 ); extern ConVar cl_neo_hud_scoreboard_hide_others; +inline int gRoundKillerUserIDs[MAX_PLAYERS_ARRAY_SAFE]; +inline int gRoundKillerUserIDsSize; + // Player entries in a death notice struct DeathNoticePlayer { @@ -145,52 +149,6 @@ void CNEOHud_DeathNotice::UpdateStateForNeoHudElementDraw() } -enum NeoHudDeathNoticeIcon : wchar_t -{ - NEO_HUD_DEATHNOTICEICON_EXPLODE = '!', - NEO_HUD_DEATHNOTICEICON_GUN, - NEO_HUD_DEATHNOTICEICON_HEADSHOT, - NEO_HUD_DEATHNOTICEICON_KILL, - NEO_HUD_DEATHNOTICEICON_MELEE, - NEO_HUD_DEATHNOTICEICON_SHORTBUS, - - NEO_HUD_DEATHNOTICEICON_RANKUP = '(', - NEO_HUD_DEATHNOTICEICON_RANKDOWN, - NEO_HUD_DEATHNOTICEICON_RANKLESS_DOG, - NEO_HUD_DEATHNOTICEICON_PVT, - NEO_HUD_DEATHNOTICEICON_CPL, - NEO_HUD_DEATHNOTICEICON_SGT, - NEO_HUD_DEATHNOTICEICON_LT, - - NEO_HUD_DEATHNOTICEICON_AA12 = '0', - NEO_HUD_DEATHNOTICEICON_GHOST, - NEO_HUD_DEATHNOTICEICON_GRENADE, - NEO_HUD_DEATHNOTICEICON_JITTE, - NEO_HUD_DEATHNOTICEICON_JITTESCOPED, - NEO_HUD_DEATHNOTICEICON_KNIFE, - NEO_HUD_DEATHNOTICEICON_KYLA, - NEO_HUD_DEATHNOTICEICON_M41, - NEO_HUD_DEATHNOTICEICON_M41L, - NEO_HUD_DEATHNOTICEICON_M41S, - NEO_HUD_DEATHNOTICEICON_MILSO, - NEO_HUD_DEATHNOTICEICON_MPN, - NEO_HUD_DEATHNOTICEICON_MPN_UNSUPRESSED, - NEO_HUD_DEATHNOTICEICON_MX, - NEO_HUD_DEATHNOTICEICON_MX_SILENCED, - NEO_HUD_DEATHNOTICEICON_PBK56S, - NEO_HUD_DEATHNOTICEICON_PZ, - NEO_HUD_DEATHNOTICEICON_REMOTEDET, - NEO_HUD_DEATHNOTICEICON_SMAC, - NEO_HUD_DEATHNOTICEICON_SMOKEGRENADE, - NEO_HUD_DEATHNOTICEICON_SRM, - NEO_HUD_DEATHNOTICEICON_SRM_S, - NEO_HUD_DEATHNOTICEICON_SRS, - NEO_HUD_DEATHNOTICEICON_SUPA7, - NEO_HUD_DEATHNOTICEICON_ZR68C, - NEO_HUD_DEATHNOTICEICON_ZR68L, - NEO_HUD_DEATHNOTICEICON_ZR68S, -}; - //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- @@ -798,6 +756,11 @@ void CNEOHud_DeathNotice::AddPlayerDeath(IGameEvent* event) C_NEO_Player* pVictim = ToNEOPlayer(UTIL_PlayerByIndex(victim)); C_NEO_Player* pAssist = ToNEOPlayer(UTIL_PlayerByIndex(assist)); + if (pKiller && gRoundKillerUserIDsSize < MAX_PLAYERS_ARRAY_SAFE) + { + gRoundKillerUserIDs[gRoundKillerUserIDsSize++] = pKiller->GetUserID(); + } + // Special case: Spectator assisted their own bot-takeover kill // Simplify to "Bot Name + Player Name" if (pKiller && pAssist && killer == assist && killer > 0) diff --git a/src/game/client/neo/ui/neo_hud_deathnotice.h b/src/game/client/neo/ui/neo_hud_deathnotice.h new file mode 100644 index 0000000000..4f4cccbab3 --- /dev/null +++ b/src/game/client/neo/ui/neo_hud_deathnotice.h @@ -0,0 +1,51 @@ +#pragma once + +extern int gRoundKillerUserIDs[MAX_PLAYERS_ARRAY_SAFE]; +extern int gRoundKillerUserIDsSize; + +enum NeoHudDeathNoticeIcon : wchar_t +{ + NEO_HUD_DEATHNOTICEICON_EXPLODE = '!', + NEO_HUD_DEATHNOTICEICON_GUN, + NEO_HUD_DEATHNOTICEICON_HEADSHOT, + NEO_HUD_DEATHNOTICEICON_KILL, + NEO_HUD_DEATHNOTICEICON_MELEE, + NEO_HUD_DEATHNOTICEICON_SHORTBUS, + + NEO_HUD_DEATHNOTICEICON_RANKUP = '(', + NEO_HUD_DEATHNOTICEICON_RANKDOWN, + NEO_HUD_DEATHNOTICEICON_RANKLESS_DOG, + NEO_HUD_DEATHNOTICEICON_PVT, + NEO_HUD_DEATHNOTICEICON_CPL, + NEO_HUD_DEATHNOTICEICON_SGT, + NEO_HUD_DEATHNOTICEICON_LT, + + NEO_HUD_DEATHNOTICEICON_AA13 = '0', + NEO_HUD_DEATHNOTICEICON_GHOST, + NEO_HUD_DEATHNOTICEICON_GRENADE, + NEO_HUD_DEATHNOTICEICON_JITTE, + NEO_HUD_DEATHNOTICEICON_JITTESCOPED, + NEO_HUD_DEATHNOTICEICON_KNIFE, + NEO_HUD_DEATHNOTICEICON_KYLA, + NEO_HUD_DEATHNOTICEICON_M41, + NEO_HUD_DEATHNOTICEICON_M41L, + NEO_HUD_DEATHNOTICEICON_M41S, + NEO_HUD_DEATHNOTICEICON_MILSO, + NEO_HUD_DEATHNOTICEICON_MPN, + NEO_HUD_DEATHNOTICEICON_MPN_UNSUPRESSED, + NEO_HUD_DEATHNOTICEICON_MX, + NEO_HUD_DEATHNOTICEICON_MX_SILENCED, + NEO_HUD_DEATHNOTICEICON_PBK56S, + NEO_HUD_DEATHNOTICEICON_PZ, + NEO_HUD_DEATHNOTICEICON_REMOTEDET, + NEO_HUD_DEATHNOTICEICON_SMAC, + NEO_HUD_DEATHNOTICEICON_SMOKEGRENADE, + NEO_HUD_DEATHNOTICEICON_SRM, + NEO_HUD_DEATHNOTICEICON_SRM_S, + NEO_HUD_DEATHNOTICEICON_SRS, + NEO_HUD_DEATHNOTICEICON_SUPA7, + NEO_HUD_DEATHNOTICEICON_ZR68C, + NEO_HUD_DEATHNOTICEICON_ZR68L, + NEO_HUD_DEATHNOTICEICON_ZR68S, +}; + diff --git a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp new file mode 100644 index 0000000000..12dd03b1fd --- /dev/null +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -0,0 +1,725 @@ +#include "cbase.h" +#include "icliententitylist.h" +#include "cliententitylist.h" +#include "neo_hud_spectator_overlay.h" + +#include "c_neo_player.h" +#include "cdll_client_int.h" +#include "neo_player_shared.h" +#include "neo_scoreboard.h" +#include "neo_weapon_loadout.h" +#include "neo_hud_deathnotice.h" +#include + +#include "iclientmode.h" +#include +#include +#include +#include +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +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); + +DECLARE_NAMED_HUDELEMENT(CNEOHud_SpectatorOverlay, neo_spectator_overlay); + +NEO_HUD_ELEMENT_DECLARE_FREQ_CVAR(SpectatorOverlay, 0.2f); + +static const struct Theme +{ + Color hpLossBg = Color(150, 20, 20, 255); + Color cardBg = Color(20, 20, 20, 180); + Color deadBg = Color(40, 20, 20, 180); + Color textPrimary = Color(240, 240, 245, 255); + Color textSecondary = Color(180, 180, 190, 230); + Color deadAvatarTint = Color(90, 40, 40, 160); +} OVERLAY_THEME = {}; + +static constexpr const float FL_CHECK_HP_REDUCTION_DELTA_MAX = 1.5f; + +CNEOHud_SpectatorOverlay::CNEOHud_SpectatorOverlay(const char* pElementName, vgui::Panel* parent) + : CHudElement(pElementName), EditablePanel(parent, pElementName) +{ + SetAutoDelete(true); + m_iHideHudElementNumber = NEO_HUD_ELEMENT_SPECTATOR_OVERLAY; + + if (parent) + { + SetParent(parent); + } + else + { + SetParent(g_pClientMode->GetViewport()); + } + + m_iDeadTex = vgui::surface()->CreateNewTextureID(true); + vgui::surface()->DrawSetTextureFile(m_iDeadTex, "vgui/hud/kill_kill", false, true); + + int iScrWide, iScrTall; + vgui::surface()->GetScreenSize(iScrWide, iScrTall); + SetBounds(0, 0, iScrWide, iScrTall); + + SetVisible(true); +} + +CNEOHud_SpectatorOverlay::~CNEOHud_SpectatorOverlay() +{ +} + +static bool ShouldDrawHUD() +{ + return (g_PR && NEORules() + && NEORules()->IsTeamplay() + && GetLocalPlayerTeam() == TEAM_SPECTATOR + && cl_neo_hud_spectator_overlay_enabled.GetBool()); +} + +void CNEOHud_SpectatorOverlay::Paint() +{ + if (!ShouldDrawHUD()) + { + return; + } + PaintNeoElement(); + BaseClass::Paint(); +} + +void CNEOHud_SpectatorOverlay::ClearAll() +{ + m_iCardsSize = 0; + V_memset(m_cards, 0, sizeof(m_cards)); + V_memset(m_iTeamPlayersCount, 0, sizeof(m_iTeamPlayersCount)); +} + +void CNEOHud_SpectatorOverlay::Init() +{ + ListenForGameEvent("round_start"); + ClearAll(); +} + +void CNEOHud_SpectatorOverlay::LevelShutdown() +{ + ClearAll(); +} + +void CNEOHud_SpectatorOverlay::FireGameEvent(IGameEvent *event) +{ + const char *type = event->GetName(); + if (0 == V_strcmp(type, "round_start")) + { + // Clear animation/fading related variables + for (int i = 0; i < m_iCardsSize; ++i) + { + SpectatorPlayerCard *pCard = &m_cards[i]; + pCard->flLastAttackTime = 0.0f; + pCard->flLastAliveTime = 0.0f; + pCard->flLastHPChangeTime = 0.0f; + pCard->iLastHP = 0; + } + } +} + +void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) +{ + BaseClass::ApplySchemeSettings(pScheme); + + LoadControlSettings("scripts/HudLayout.res"); + + // Try to load fonts specifically for this HUD + m_hNameFont = pScheme->GetFont("NHudSpectatorOverlayName", true); + m_hInfoFont = pScheme->GetFont("NHudSpectatorOverlayInfo", true); + m_hClassFont = pScheme->GetFont("NHudSpectatorOverlayClass", true); + m_hHPFont = pScheme->GetFont("NHudSpectatorOverlayHP", true); + m_hGhostFont = pScheme->GetFont("NHudSpectatorOverlayGhost", true); + + // Fallbacks + if (!m_hNameFont) m_hNameFont = pScheme->GetFont("Default", true); + if (!m_hInfoFont) m_hInfoFont = pScheme->GetFont("DefaultSmall", true); + if (!m_hClassFont) m_hClassFont = pScheme->GetFont("DefaultSmall", true); + if (!m_hHPFont) m_hHPFont = pScheme->GetFont("Default", true); + if (!m_hGhostFont) m_hGhostFont = pScheme->GetFont("NHudKillfeedIcons", true); + + int iScrWide, iScrTall; + vgui::surface()->GetScreenSize(iScrWide, iScrTall); + SetBounds(0, 0, iScrWide, iScrTall); + + SetFgColor(COLOR_TRANSPARENT); + SetBgColor(COLOR_TRANSPARENT); +} + +void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() +{ + bool bScoreboardNeedUpdate = false; + + const int iOldCardsSize = m_iCardsSize; + SpectatorPlayerCard oldCards[MAX_PLAYERS_ARRAY_SAFE] = {}; + static_assert(sizeof(oldCards) == sizeof(m_cards)); + V_memcpy(oldCards, m_cards, sizeof(oldCards)); + + m_iCardsSize = 0; + V_memset(m_iTeamPlayersCount, 0, sizeof(m_iTeamPlayersCount)); + V_memset(m_cards, 0, sizeof(m_cards)); + + const auto *pLocalPlayer = C_NEO_Player::GetLocalNEOPlayer(); + if (!pLocalPlayer || !ShouldDrawHUD()) + { + return; + } + + for (int i = 1; i <= gpGlobals->maxClients; ++i) + { + if (i == pLocalPlayer->entindex() || !g_PR->IsValid(i) || g_PR->IsHLTV(i)) + { + continue; + } + + const int iTeam = g_PR->GetTeam(i); + if (false == IN_BETWEEN_EQ(FIRST_GAME_TEAM, iTeam, TEAM__TOTAL - 1) + || (m_iCardsSize >= MAX_PLAYERS_ARRAY_SAFE)) + { + continue; + } + + SpectatorPlayerCard *pCard = &m_cards[m_iCardsSize++]; + ++m_iTeamPlayersCount[iTeam]; + + // Reuse the avatar image already generated by the scoreboard + pCard->pAvatar = nullptr; + if (g_pNeoScoreBoard && g_pNeoScoreBoard->m_pImageList && CNEOScoreBoard::ShowAvatars()) + { + const CSteamID steamId = GetSteamIDForPlayerIndex(i); + if (steamId.IsValid()) + { + CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; + const int iMapIdx = mapAvatars.Find(steamId); + if (iMapIdx != mapAvatars.InvalidIndex()) + { + const MapAvatarValue &avatar = mapAvatars[iMapIdx]; + if (avatar.i184Idx >= 0) + { + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + } + } + + bScoreboardNeedUpdate = !pCard->pAvatar; + } + } + + Q_UTF8ToUnicode(g_PR->GetPlayerName(i), pCard->wszPlayerName, sizeof(pCard->wszPlayerName)); + + C_NEO_Player *pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + + // XP and class from player resource + pCard->iTeam = iTeam; + pCard->iXP = g_PR->GetXP(i); + pCard->iDeath = g_PR->GetDeaths(i); + pCard->iClass = g_PR->GetClass(i); + + pCard->iRoundKills = 0; + if (pNeoPlayer) + { + pCard->iUserID = pNeoPlayer->GetUserID(); + pCard->iEntIndex = pNeoPlayer->entindex(); + + for (int j = 0; j < gRoundKillerUserIDsSize; ++j) + { + pCard->iRoundKills += (gRoundKillerUserIDs[j] == pCard->iUserID); + } + } + + // Health - NEO NOTE (nullsystem): GetHealth can be non-0 even when dead + pCard->bAlive = g_PR->IsAlive(i); + pCard->iHP = pCard->bAlive ? g_PR->GetHealth(i) : 0; + + // Deal with timings to can do animations/fading + SpectatorPlayerCard *pOldCard = nullptr; + if (pCard->iUserID > 0) + { + if (oldCards[m_iCardsSize - 1].iUserID == pCard->iUserID) + { + pOldCard = &oldCards[m_iCardsSize - 1]; + } + else + { + for (int j = 0; j < iOldCardsSize; ++j) + { + if (oldCards[j].iUserID == pCard->iUserID) + { + pOldCard = &oldCards[j]; + break; + } + } + } + } + if (pOldCard) + { + // Alive -> dead + pCard->flLastAliveTime = pOldCard->flLastAliveTime; + if (pOldCard->bAlive && !pCard->bAlive) + { + pCard->flLastAliveTime = gpGlobals->curtime; + } + + // HP reduction + pCard->flLastHPChangeTime = pOldCard->flLastHPChangeTime; + pCard->iLastHP = pOldCard->iLastHP; + if ((pOldCard->iHP > pCard->iHP) + && ((gpGlobals->curtime - pCard->flLastHPChangeTime) > FL_CHECK_HP_REDUCTION_DELTA_MAX)) + { + pCard->flLastHPChangeTime = gpGlobals->curtime; + pCard->iLastHP = pOldCard->iHP; + } + } + + // Weapon + pCard->wszWeaponIcon[0] = L'\0'; + pCard->flLastAttackTime = 0.0f; + if (pNeoPlayer) + { + auto *neoWep = static_cast(pNeoPlayer->GetActiveWeapon()); + if (neoWep) + { + Q_UTF8ToUnicode(neoWep->GetDeathIcon(), + pCard->wszWeaponIcon, sizeof(pCard->wszWeaponIcon)); + pCard->flLastAttackTime = neoWep->GetLastAttackTime(); + } + } + } + + // NEO NOTE (nullsystem): This is so the spectator don't need to tab press + // on the scoreboard to update the avatar. Generally only happens in the + // beginning of match/demo + // + // NEO TODO (nullsystem): Move avatar handling off to something separate from + // the scoreboard but shared between scoreboard, overlay, and killer info + if (bScoreboardNeedUpdate) + { + g_pNeoScoreBoard->Update(); + } +} + +void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, + const bool bIsLeftSide, + const int x, const int y, + const int wide, const int tall, + const Color accentColor, const float flWideAs43) const +{ + // Card background + static constexpr const float FL_LAST_ALIVE_DELTA_MAX = 1.5f; + const float flLastAliveDelta = gpGlobals->curtime - card.flLastAliveTime; + if (card.bAlive || flLastAliveDelta > FL_LAST_ALIVE_DELTA_MAX || flLastAliveDelta < 0.0f) + { + vgui::surface()->DrawSetColor(card.bAlive ? OVERLAY_THEME.cardBg : OVERLAY_THEME.deadBg); + } + else + { + // Just died fade from brighter red to normal dead red background + Color deadFadeBg = OVERLAY_THEME.deadBg; + deadFadeBg[0] += 80.0f * ((FL_LAST_ALIVE_DELTA_MAX - flLastAliveDelta) * (1.0f / FL_LAST_ALIVE_DELTA_MAX)); + vgui::surface()->DrawSetColor(deadFadeBg); + } + vgui::surface()->DrawFilledRect(x, y, x + wide, y + tall); + + const int iAvatarSize = tall; + const int iAvatarX = bIsLeftSide ? x : x + wide - iAvatarSize; + const int iAvatarY = y; + + // Draw avatar background + vgui::surface()->DrawSetColor(OVERLAY_THEME.cardBg); + vgui::surface()->DrawOutlinedRect(iAvatarX, iAvatarY, iAvatarX + iAvatarSize, iAvatarY + iAvatarSize); + vgui::surface()->DrawFilledRect(iAvatarX + 1, iAvatarY + 1, iAvatarX + iAvatarSize - 1, iAvatarY + iAvatarSize - 1); + + // Draw the actual Steam avatar (borrowed from the scoreboard's avatar list) + if (card.pAvatar && card.pAvatar->IsValid()) + { + card.pAvatar->SetPos(iAvatarX, iAvatarY); + card.pAvatar->SetSize(iAvatarSize, iAvatarSize); + card.pAvatar->Paint(); + } + else if (card.wszPlayerName[0]) + { + // Fallback when no avatar: first character of the player name + // Usually just bots though + wchar_t wszInitial[2] = { card.wszPlayerName[0], L'\0' }; + vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textSecondary); + vgui::surface()->DrawSetTextFont(m_hInfoFont); + int iInitialWide = 0; + [[maybe_unused]] int iInitialTall = 0; + vgui::surface()->GetTextSize(m_hInfoFont, wszInitial, iInitialWide, iInitialTall); + vgui::surface()->DrawSetTextPos(iAvatarX + (iAvatarSize - iInitialWide) / 2, iAvatarY + (iAvatarSize - vgui::surface()->GetFontTall(m_hInfoFont)) / 2); + vgui::surface()->DrawPrintText(wszInitial, 1); + } + + const int iTextPad = flWideAs43 / 360.0f; + + // HP text or dead icon over avatar + if (card.bAlive) + { + wchar_t wszHP[16] = {}; + V_swprintf_safe(wszHP, L"%d", card.iHP); + + int iWideHP = 0, iTallHP = 0; + vgui::surface()->GetTextSize(m_hHPFont, wszHP, iWideHP, iTallHP); + vgui::surface()->DrawSetTextFont(m_hHPFont); + + const int iHPTextX = bIsLeftSide + ? iAvatarX + iTextPad + : iAvatarX + iAvatarSize - iWideHP - iTextPad; + const int iHPTextY = iAvatarY + iAvatarSize - iTallHP; + + // Shadow + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iHPTextX + 2, iHPTextY + 2); + vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); + + // Front + vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); + vgui::surface()->DrawSetTextPos(iHPTextX, iHPTextY); + vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); + } + else + { + // Dead red tint + vgui::surface()->DrawSetColor(OVERLAY_THEME.deadAvatarTint); + vgui::surface()->DrawFilledRect( + iAvatarX, + iAvatarY, + iAvatarX + iAvatarSize, + iAvatarY + iAvatarSize); + + // Dead icon + vgui::surface()->DrawSetColor(COLOR_WHITE); + vgui::surface()->DrawSetTexture(m_iDeadTex); + vgui::surface()->DrawTexturedRect( + iAvatarX, + iAvatarY, + iAvatarX + iAvatarSize, + iAvatarY + iAvatarSize); + } + + // HP bar(s) + { + const int hpX = bIsLeftSide ? x + iAvatarSize : x; + const int hpY = y; + const int hpW = wide - iAvatarSize; + const int hpH = static_cast(iAvatarSize / 2.0f); + const float flClassHP = static_cast(IN_BETWEEN_AR(0, card.iClass, NEO_CLASS__ENUM_COUNT) + ? MAX_HEALTH_FOR_CLASS[card.iClass] : 100); + + const float flHpPerc = clamp(card.iHP / flClassHP, 0.0f, 1.0f); + const int iFillHPWide = static_cast(hpW * flHpPerc); + + // Red HP reduction/loss bar + const float flLastHPChangeDelta = gpGlobals->curtime - card.flLastHPChangeTime; + if (card.iLastHP > card.iHP + && IN_BETWEEN_EQ(0.0f, flLastHPChangeDelta, FL_CHECK_HP_REDUCTION_DELTA_MAX)) + { + static constexpr const float FL_CHECK_HP_REDUCTION_DELTA_ANIM_START = + FL_CHECK_HP_REDUCTION_DELTA_MAX * 0.40f; + static constexpr const float FL_CHECK_HP_REDUCTION_ANIM_MAX = + FL_CHECK_HP_REDUCTION_DELTA_MAX - FL_CHECK_HP_REDUCTION_DELTA_ANIM_START; + + const float flLastHpPerc = clamp(card.iLastHP / flClassHP, 0.0f, 1.0f); + int iFillReductHP = static_cast(hpW * flLastHpPerc) - iFillHPWide; + if (iFillReductHP > 0) + { + if (flLastHPChangeDelta > FL_CHECK_HP_REDUCTION_DELTA_ANIM_START) + { + iFillReductHP = static_cast(iFillReductHP) + * static_cast( + (FL_CHECK_HP_REDUCTION_ANIM_MAX - (flLastHPChangeDelta - FL_CHECK_HP_REDUCTION_DELTA_ANIM_START)) + * (1.0f / FL_CHECK_HP_REDUCTION_ANIM_MAX)); + } + const int iHPXStart = bIsLeftSide + ? hpX + iFillHPWide + : hpX + hpW - iFillHPWide - iFillReductHP; + + vgui::surface()->DrawSetColor(OVERLAY_THEME.hpLossBg); + vgui::surface()->DrawFilledRect( + iHPXStart, + hpY, + iHPXStart + iFillReductHP, + hpY + hpH); + } + } + + // HP bar + if (card.iHP > 0) + { + Color colorHpBar = accentColor; + colorHpBar[0] *= 0.8f; + colorHpBar[1] *= 0.8f; + colorHpBar[2] *= 0.8f; + + static constexpr const int FADE_RANGE = 196; + static constexpr const int FADE_START = 255 - FADE_RANGE; + const int iMaxFade = Clamp(static_cast(flHpPerc * static_cast(FADE_RANGE)), 0, FADE_RANGE); + const int iHPXStart = bIsLeftSide ? hpX : hpX + (hpW - iFillHPWide); + vgui::surface()->DrawSetColor(colorHpBar); + vgui::surface()->DrawFilledRectFade( + iHPXStart, + hpY, + iHPXStart + iFillHPWide, + hpY + hpH, + bIsLeftSide ? FADE_START : FADE_START + iMaxFade, + bIsLeftSide ? FADE_START + iMaxFade : FADE_START, + true); + } + } + + // Player name | Class + { + vgui::surface()->DrawSetTextFont(m_hNameFont); + + // Right-side right-aligned player name + int iPlayerNameWide = 0, iPlayerNameTall = 0; + vgui::surface()->GetTextSize(m_hNameFont, card.wszPlayerName, iPlayerNameWide, iPlayerNameTall); + + const int iNameX = bIsLeftSide + ? x + iAvatarSize + iTextPad + : x + (wide - iAvatarSize) - iTextPad - iPlayerNameWide; + const int iNameY = y + (((iAvatarSize / 2.0f) - iPlayerNameTall) / 2.0f); + + // Shadow + { + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iNameX + 2, iNameY + 2); + vgui::surface()->DrawPrintText(card.wszPlayerName, V_wcslen(card.wszPlayerName)); + } + // Front + { + vgui::surface()->DrawSetTextColor(card.bAlive + ? OVERLAY_THEME.textPrimary + : Color(140, 140, 150, 200)); + vgui::surface()->DrawSetTextPos(iNameX, iNameY); + vgui::surface()->DrawPrintText(card.wszPlayerName, V_wcslen(card.wszPlayerName)); + } + + // Class name + vgui::surface()->DrawSetTextFont(m_hClassFont); + const wchar_t *pwszClassName = GetNeoClassNameW(card.iClass); + int iClassNameWide = 0, iClassNameTall = 0; + vgui::surface()->GetTextSize(m_hClassFont, pwszClassName, iClassNameWide, iClassNameTall); + const int iClNamePosX = bIsLeftSide + ? x + iAvatarSize + (wide - iAvatarSize) - iTextPad - iClassNameWide + : x + iTextPad; + const int iClNamePosY = y + (((iAvatarSize / 2.0f) - iClassNameTall) / 2.0f); + + // Shadow + { + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iClNamePosX + 1, iClNamePosY + 1); + vgui::surface()->DrawPrintText(pwszClassName, V_wcslen(pwszClassName)); + } + // Front + { + vgui::surface()->DrawSetTextColor(card.bAlive + ? OVERLAY_THEME.textPrimary + : Color(140, 140, 150, 200)); + vgui::surface()->DrawSetTextPos(iClNamePosX, iClNamePosY); + vgui::surface()->DrawPrintText(pwszClassName, V_wcslen(pwszClassName)); + } + } + + // Extra info row: XP | Death | Round kills | Weapon logo + { + const int iInfoTall = iAvatarSize / 2.0f; + const int iInfoX = bIsLeftSide ? x + iAvatarSize + iTextPad : x + iTextPad; + const int iInfoY = y + iInfoTall; + const int iTotalW = wide - iAvatarSize - (iTextPad * 2); + + // Split available width into 4 equal columns with small gaps + const int iGap = flWideAs43 / 360.0f; + const int iColW = iTotalW / 4; + + const int iTextFontTall = vgui::surface()->GetFontTall(m_hInfoFont); + const int iTextFontOffset = (iInfoTall - iTextFontTall) / 2.0f; + + int iRankIconWide = 0, iRankIconTall = 0; + { + const wchar_t wszRankIcon[] = { NEO_HUD_DEATHNOTICEICON_LT, L'\0' }; + vgui::surface()->GetTextSize(g_hFontKillfeedIcons, wszRankIcon, iRankIconWide, iRankIconTall); + } + + int iDeathIconWide = 0, iDeathIconTall = 0; + { + const wchar_t wszDeathsIcon[] = { NEO_HUD_DEATHNOTICEICON_KILL, L'\0' }; + vgui::surface()->GetTextSize(g_hFontKillfeedIcons, wszDeathsIcon, iDeathIconWide, iDeathIconTall); + } + + int iWepWide = 0, iWepTall = 0; + vgui::surface()->GetTextSize(g_hFontKillfeedIcons, card.wszWeaponIcon, iWepWide, iWepTall); + + wchar_t wszNum[8] = {}; + + // XP + const int iXPosXP = bIsLeftSide ? iInfoX + iGap : x + (wide - iAvatarSize) - iColW + iGap; + // XP - Rank Icon + { + const wchar_t wcRankIcon = NEO_HUD_DEATHNOTICEICON_RANKLESS_DOG + GetRank(card.iXP); + vgui::surface()->DrawSetTextFont(g_hFontKillfeedIcons); + vgui::surface()->DrawSetTextPos(iXPosXP + iTextPad, iInfoY + ((iInfoTall - iRankIconTall) / 2.0f)); + vgui::surface()->DrawPrintText(&wcRankIcon, 1); + } + // XP - Number + { + V_swprintf_safe(wszNum, L"%d", card.iXP); + vgui::surface()->DrawSetTextFont(m_hInfoFont); + vgui::surface()->DrawSetTextPos(iXPosXP + iTextPad + iRankIconWide + iTextPad, iInfoY + iTextFontOffset); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + } + + // Death + const int iXPosDeath = bIsLeftSide ? iXPosXP + iColW + iGap : iXPosXP - iColW + iGap; + // Death - Icon + { + static const wchar_t WC_KILLICON = NEO_HUD_DEATHNOTICEICON_KILL; + vgui::surface()->DrawSetTextFont(g_hFontKillfeedIcons); + vgui::surface()->DrawSetTextPos(iXPosDeath, iInfoY + ((iInfoTall - iDeathIconTall) / 2.0f)); + vgui::surface()->DrawPrintText(&WC_KILLICON, 1); + } + // Death - Number + { + V_swprintf_safe(wszNum, L"%d", card.iDeath); + vgui::surface()->DrawSetTextFont(m_hInfoFont); + vgui::surface()->DrawSetTextPos(iXPosDeath + iDeathIconWide + iTextPad, iInfoY + iTextFontOffset); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + } + + if (card.iRoundKills > 0) + { + const int iXPosRoundKills = bIsLeftSide + ? iXPosDeath + iColW + iGap + : iXPosDeath - iColW + iGap; + // Kills in current round + V_swprintf_safe(wszNum, L"x%d", card.iRoundKills); + vgui::surface()->DrawSetTextFont(m_hInfoFont); + vgui::surface()->DrawSetTextPos(iXPosRoundKills, iInfoY + iTextFontOffset); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + } + + if (card.bAlive) + { + const int iXPosWepIcon = bIsLeftSide ? x + wide - iTextPad - iWepWide : x + iTextPad; + vgui::surface()->DrawSetTextFont(g_hFontKillfeedIcons); + vgui::surface()->DrawSetTextPos(iXPosWepIcon, iInfoY + ((iInfoTall - iWepTall) / 2.0f)); + vgui::surface()->DrawPrintText(card.wszWeaponIcon, 1); + } + } + + // Ghoster player indicator - Jinrai right side, NSF left side + if (card.iEntIndex == NEORules()->GetGhosterPlayer()) + { + const wchar_t WSZ_DNI_GHOST[] = { NEO_HUD_DEATHNOTICEICON_GHOST, L'\0' }; + + int iGhostWide = 0, iGhostTall = 0; + vgui::surface()->GetTextSize(m_hGhostFont, WSZ_DNI_GHOST, iGhostWide, iGhostTall); + + vgui::surface()->DrawSetTextFont(m_hGhostFont); + if (bIsLeftSide) + { + vgui::surface()->DrawSetTextPos(x + wide + iTextPad, y + ((tall - iGhostTall) / 2.0f)); + } + else + { + vgui::surface()->DrawSetTextPos(x - iTextPad - iGhostWide, y + ((tall - iGhostTall) / 2.0f)); + } + vgui::surface()->DrawPrintText(WSZ_DNI_GHOST, 1); + } + + // Spectator current observing border + const auto *pLocalPlayer = C_NEO_Player::GetLocalNEOPlayer(); + if (card.iEntIndex == GetSpectatorTarget() + && pLocalPlayer + && pLocalPlayer->IsObserver() + && (pLocalPlayer->GetObserverMode() == OBS_MODE_IN_EYE + || pLocalPlayer->GetObserverMode() == OBS_MODE_CHASE)) + { + const int iMargin = flWideAs43 / 480.0f; + + vgui::surface()->DrawSetColor(COLOR_WHITE); + vgui::surface()->DrawFilledRect(x, y - iMargin, x + wide, y); // top + vgui::surface()->DrawFilledRect(x, y + tall, x + wide, y + tall + iMargin); // bottom + vgui::surface()->DrawFilledRect(x - iMargin, y - iMargin, x, y + tall + iMargin); // left + vgui::surface()->DrawFilledRect(x + wide, y - iMargin, x + wide + iMargin, y + tall + iMargin); // right + } + + // Weapon firing indicator bar - Jinrai right side, NSF left side + static constexpr const float FL_LAST_ATTACK_DELTA_MAX = 0.5f; + const float flLastAttackDelta = gpGlobals->curtime - card.flLastAttackTime; + if (IN_BETWEEN_EQ(0.0f, flLastAttackDelta, FL_LAST_ATTACK_DELTA_MAX)) + { + Color fireIndColor = COLOR_WHITE; + fireIndColor[3] *= (FL_LAST_ATTACK_DELTA_MAX - flLastAttackDelta) * (1.0f / FL_LAST_ATTACK_DELTA_MAX); + vgui::surface()->DrawSetColor(fireIndColor); + if (bIsLeftSide) + { + vgui::surface()->DrawFilledRect(x + wide + iTextPad, y, x + wide + (2 * iTextPad), y + tall); + } + else + { + vgui::surface()->DrawFilledRect(x - (2 * iTextPad), y, x - iTextPad, y + tall); + } + } +} + +void CNEOHud_SpectatorOverlay::DrawNeoHudElement() +{ + if (!ShouldDrawHUD() || m_iCardsSize <= 0) + { + return; + } + + // In 1920x1080, the player card: + // Converted to 4:3 is 1440x1080, calculate from 4:3 width regardless of + // 16:9/16:10: + // * Total width (including avatar) of 300px, 1440/4.8=300 + // * Avatar width of 64px, 1440/22.5=64 + // * Gap from left or right edge of 12px, 1440/120=12 + // * Gap between cards Y-axis of 4px, 1440/360=4 + int iScrWide, iScrTall; + vgui::surface()->GetScreenSize(iScrWide, iScrTall); + + const float flWide = static_cast(iScrWide); + float flWideAs43 = static_cast(iScrTall) * (4.0f / 3.0f); + if (flWideAs43 > flWide) flWideAs43 = flWide; + + const int iTotalCardWide = flWideAs43 / 4.8f; + 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 + for (const auto iTeam : {TEAM_JINRAI, TEAM_NSF}) + { + const bool bIsLeftSide = (iTeam == TEAM_JINRAI); + const Color accentColor = g_PR->GetTeamColor(iTeam); + + // Center the column vertically on screen + const int iTeamCount = m_iTeamPlayersCount[iTeam]; + const int iTotalHeight = (iTeamCount * iAvatarWH) + ((iTeamCount - 1) * iGapBetween); + const int iColXStart = bIsLeftSide + ? iGapWide + : static_cast(iScrWide - iTotalCardWide - iGapWide); + const int iColYStart = ((iScrTall - iTotalHeight) / 2.0f); + + int iPlayerRow = 0; + for (int i = 0; i < m_iCardsSize; ++i) + { + if (m_cards[i].iTeam == iTeam) + { + const int iRowY = iColYStart + (iPlayerRow * iAvatarWH) + + ((iPlayerRow > 0) ? (iPlayerRow * iGapBetween) : 0); + DrawPlayerCard(m_cards[i], bIsLeftSide, + iColXStart, iRowY, iTotalCardWide, iAvatarWH, + accentColor, flWideAs43); + ++iPlayerRow; + } + } + } +} + diff --git a/src/game/client/neo/ui/neo_hud_spectator_overlay.h b/src/game/client/neo/ui/neo_hud_spectator_overlay.h new file mode 100644 index 0000000000..0f247d26b8 --- /dev/null +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -0,0 +1,71 @@ +#pragma once + +#include "neo_hud_childelement.h" +#include "hudelement.h" +#include + +class CAvatarImage; + +struct SpectatorPlayerCard +{ + int iUserID = 0; + int iEntIndex = 0; + int iTeam = 0; + wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH] = {}; + wchar_t wszWeaponIcon[2] = {}; + int iXP = 0; + int iDeath = 0; + int iClass = 0; + int iHP = 0; + int iRoundKills = 0; + bool bAlive = false; + CAvatarImage *pAvatar = nullptr; + // Used for animations/fading + float flLastAttackTime = 0.0f; + float flLastAliveTime = 0.0f; + float flLastHPChangeTime = 0.0f; + int iLastHP = 0; +}; + +// Client-side spectator-only HUD overlay showing both teams players infos +// Mainly for competitve streams +class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement, public vgui::EditablePanel +{ + DECLARE_CLASS_SIMPLE(CNEOHud_SpectatorOverlay, EditablePanel); + +public: + CNEOHud_SpectatorOverlay(const char *pElementName, vgui::Panel *parent = nullptr); + CNEOHud_SpectatorOverlay(const CNEOHud_SpectatorOverlay& other) = delete; + virtual ~CNEOHud_SpectatorOverlay(); + + void ClearAll(); + void Init() final; + void LevelShutdown() final; + void FireGameEvent(IGameEvent *event) final; + void ApplySchemeSettings(vgui::IScheme *pScheme) final; + void Paint() final; + +protected: + void UpdateStateForNeoHudElementDraw() final; + void DrawNeoHudElement() final; + ConVar *GetUpdateFrequencyConVar() const final; + +private: + void DrawPlayerCard(const SpectatorPlayerCard &card, + const bool bIsLeftSide, + const int x, const int y, + const int wide, const int tall, + const Color accentColor, const float flWideAs43) const; + + vgui::HFont m_hNameFont = 0; + vgui::HFont m_hInfoFont = 0; + vgui::HFont m_hClassFont = 0; + vgui::HFont m_hHPFont = 0; + vgui::HFont m_hGhostFont = 0; + int m_iDeadTex = 0; + + SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; + int m_iCardsSize = 0; + int m_iTeamPlayersCount[TEAM__TOTAL] = {}; +}; + diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index b001b4212e..8acd112613 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -17,6 +17,7 @@ #include "filesystem.h" #include "hltvcamera.h" #include "hud_crosshair.h" +#include "ui/neo_hud_deathnotice.h" #else #include "neo_player.h" #include "team.h" @@ -1922,6 +1923,9 @@ void CNEORules::FireGameEvent(IGameEvent* event) { StartAutoRecording(); } + + V_memset(gRoundKillerUserIDs, 0, sizeof(gRoundKillerUserIDs)); + gRoundKillerUserIDsSize = 0; #endif #ifdef GAME_DLL m_flNeoRoundStartTime = gpGlobals->curtime; @@ -4347,24 +4351,7 @@ void CNEORules::DeathNotice(CBasePlayer* pVictim, const CTakeDamageInfo& info) event->SetInt("priority", 7); event->SetBool("headshot", pVictim->LastHitGroup() == HITGROUP_HEAD); event->SetBool("suicide", pKiller == pVictim || !pKiller->IsPlayer()); - - if (isGrenade) - { - event->SetString("deathIcon", "2"); // NEO TODO (Adam) get from enum - } - else if (isRemoteDetpack) - { - event->SetString("deathIcon", "A"); // NEO TODO (Adam) get from enum - } - else if (neoWep) - { - event->SetString("deathIcon", neoWep->GetNEOWpnData().szDeathIcon); - } - else - { - event->SetString("deathIcon", ""); - } - + event->SetString("deathIcon", neoWep ? neoWep->GetDeathIcon() : ""); event->SetBool("explosive", isGrenade || isRemoteDetpack); event->SetBool("ghoster", m_iGhosterPlayer == pVictim->entindex()); diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 8d811cab52..5f692470d3 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -170,6 +170,7 @@ enum NeoHudElements : NEO_HUD_BITS_UNDERLYING_TYPE { NEO_HUD_ELEMENT_SCOREBOARD = (static_cast(1) << 14), NEO_HUD_ELEMENT_PLAYER_PING = (static_cast(1) << 15), NEO_HUD_ELEMENT_WORLDPOS_MARKER_ENT = (static_cast(1) << 16), + NEO_HUD_ELEMENT_SPECTATOR_OVERLAY = (static_cast(1) << 17), }; enum NeoSpectateEvent { diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index a0368e340f..9e621e937b 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -419,4 +419,6 @@ static constexpr const SZWSZTexts SZWSZ_NEO_TEAM_STRS[TEAM__TOTAL] = { #define NEO_GAME_NAME "NEOTOKYO;REBUILD" +static constexpr const int WEP_NAME_MAXSTRLEN = 32; + #endif // NEO_PLAYER_SHARED_H diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 7727bbe1c3..40447f42d7 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -1398,3 +1398,17 @@ void CNEOBaseCombatWeapon::Use(CBaseEntity* pActivator, CBaseEntity* pCaller, US // Calling BaseClass::Use will pick the weapon up without waiting for the touch cooldown, don't see anything important there that we need to do that we aren't doing here } #endif + +const char *CNEOBaseCombatWeapon::GetDeathIcon() const +{ + if (WeaponIndex() == NEO_WIDX_FRAG_GRENADE) + { + return "2"; + } + else if (WeaponIndex() == NEO_WIDX_DETPACK) + { + return "A"; + } + return GetNEOWpnData().szDeathIcon; +} + diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index be1732aace..f209a94e21 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -259,6 +259,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon float m_flTemperature; int m_spawnflags; #endif // CLIENT_DLL + + const char *GetDeathIcon() const; protected: WeaponHandlingInfo_t m_weaponHandling; From 08e5f0142537dae0c285db74302ee7f92d3edeec Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:33:15 +0100 Subject: [PATCH 02/17] Change to more like OG:NT overlay's showing all weapons * Also moved x[round kills] to on top of avatar * Reorder XP/Death on NSF side --- game/neo/resource/ClientScheme.res | 73 +++++++- .../neo/ui/neo_hud_spectator_overlay.cpp | 163 +++++++++++++----- .../client/neo/ui/neo_hud_spectator_overlay.h | 10 +- 3 files changed, 203 insertions(+), 43 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 7002518cb4..74d17d2921 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -762,7 +762,7 @@ Scheme } } - NHudSpectatorOverlayHP + NHudSpectatorOverlayRoundKillHP { "1" { @@ -890,6 +890,77 @@ Scheme } } + NHudSpectatorOverlaySmallWeapons + { + "1" + { + "name" "killfeedicons" + "tall" "12" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "480 599" + "additive" "0" + } + "2" + { + "name" "killfeedicons" + "tall" "14" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "600 767" + "additive" "0" + } + "3" + { + "name" "killfeedicons" + "tall" "16" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "768 1023" + "antialias" "1" + "additive" "0" + } + "4" + { + "name" "killfeedicons" + "tall" "14"// TESTING + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1024 1199" + "antialias" "1" + "additive" "0" + } + "5" + { + "name" "killfeedicons" + "tall" "24" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1200 1440" + "antialias" "1" + "additive" "0" + } + "6" + { + "name" "killfeedicons" + "tall" "26" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1441 1599" + "antialias" "1" + "additive" "0" + } + "7" + { + "name" "killfeedicons" + "tall" "28" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1600 6000" + "antialias" "1" + "additive" "0" + } + } BudgetLabel { 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 12dd03b1fd..fa46ca5cd8 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -34,8 +34,9 @@ static const struct Theme Color hpLossBg = Color(150, 20, 20, 255); Color cardBg = Color(20, 20, 20, 180); Color deadBg = Color(40, 20, 20, 180); - Color textPrimary = Color(240, 240, 245, 255); - Color textSecondary = Color(180, 180, 190, 230); + Color textPrimary = Color(240, 240, 240, 255); + Color textSecondary = Color(180, 180, 180, 230); + Color wepInactive = Color(120, 120, 120, 255); Color deadAvatarTint = Color(90, 40, 40, 160); } OVERLAY_THEME = {}; @@ -133,15 +134,17 @@ void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) m_hNameFont = pScheme->GetFont("NHudSpectatorOverlayName", true); m_hInfoFont = pScheme->GetFont("NHudSpectatorOverlayInfo", true); m_hClassFont = pScheme->GetFont("NHudSpectatorOverlayClass", true); - m_hHPFont = pScheme->GetFont("NHudSpectatorOverlayHP", true); + m_hRKHPFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHP", true); m_hGhostFont = pScheme->GetFont("NHudSpectatorOverlayGhost", true); + m_hSmallWeaponsFont = pScheme->GetFont("NHudSpectatorOverlaySmallWeapons", true); // Fallbacks if (!m_hNameFont) m_hNameFont = pScheme->GetFont("Default", true); if (!m_hInfoFont) m_hInfoFont = pScheme->GetFont("DefaultSmall", true); if (!m_hClassFont) m_hClassFont = pScheme->GetFont("DefaultSmall", true); - if (!m_hHPFont) m_hHPFont = pScheme->GetFont("Default", true); + if (!m_hRKHPFont) m_hRKHPFont = pScheme->GetFont("Default", true); if (!m_hGhostFont) m_hGhostFont = pScheme->GetFont("NHudKillfeedIcons", true); + if (!m_hSmallWeaponsFont) m_hSmallWeaponsFont = pScheme->GetFont("NHudKillfeedIcons", true); int iScrWide, iScrTall; vgui::surface()->GetScreenSize(iScrWide, iScrTall); @@ -276,16 +279,41 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() } // Weapon - pCard->wszWeaponIcon[0] = L'\0'; pCard->flLastAttackTime = 0.0f; + pCard->iTotalWeaponIcons = 0; + pCard->iWeaponIdxPrimary = -1; + pCard->iWeaponIdxActive = -1; if (pNeoPlayer) { - auto *neoWep = static_cast(pNeoPlayer->GetActiveWeapon()); - if (neoWep) + const auto *pWepActive = static_cast(pNeoPlayer->GetActiveWeapon()); + if (pWepActive) { - Q_UTF8ToUnicode(neoWep->GetDeathIcon(), - pCard->wszWeaponIcon, sizeof(pCard->wszWeaponIcon)); - pCard->flLastAttackTime = neoWep->GetLastAttackTime(); + pCard->flLastAttackTime = pWepActive->GetLastAttackTime(); + } + + wchar_t wszWeaponIcon[2] = {}; + for (int j = 0; j < MAX_WEAPONS; ++j) + { + // NEO NOTE (nullsystem): Disallow knife, but keep ghost in + // array but ghost only for the outside indicator will be + // skipped over in the weapon icons list + if (const auto *pNeoWep = static_cast(pNeoPlayer->GetWeapon(j)); + pNeoWep && pNeoWep->WeaponIndex() != NEO_WIDX_KNIFE) + { + if (pNeoWep->GetSlot() == 0) + { + pCard->iWeaponIdxPrimary = pCard->iTotalWeaponIcons; + } + if (pWepActive == pNeoWep) + { + pCard->iWeaponIdxActive = pCard->iTotalWeaponIcons; + } + + // Icons are really only 1 character long + Q_UTF8ToUnicode(pNeoWep->GetDeathIcon(), + wszWeaponIcon, sizeof(wszWeaponIcon)); + pCard->wcaWeaponIcons[pCard->iTotalWeaponIcons++] = wszWeaponIcon[0]; + } } } } @@ -363,8 +391,8 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, V_swprintf_safe(wszHP, L"%d", card.iHP); int iWideHP = 0, iTallHP = 0; - vgui::surface()->GetTextSize(m_hHPFont, wszHP, iWideHP, iTallHP); - vgui::surface()->DrawSetTextFont(m_hHPFont); + vgui::surface()->GetTextSize(m_hRKHPFont, wszHP, iWideHP, iTallHP); + vgui::surface()->DrawSetTextFont(m_hRKHPFont); const int iHPTextX = bIsLeftSide ? iAvatarX + iTextPad @@ -401,6 +429,33 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, iAvatarY + iAvatarSize); } + // Round kills, over avatar, upper section + if (card.iRoundKills > 0) + { + // Kills in current round + wchar_t wszNum[8] = {}; + V_swprintf_safe(wszNum, L"x%d", card.iRoundKills); + + int iWideRK = 0, iTallRK = 0; + vgui::surface()->GetTextSize(m_hRKHPFont, wszNum, iWideRK, iTallRK); + vgui::surface()->DrawSetTextFont(m_hRKHPFont); + + const int iRKTextX = bIsLeftSide + ? iAvatarX + iTextPad + : iAvatarX + iAvatarSize - iWideRK - iTextPad; + const int iRKTextY = iAvatarY; + + // Shadow + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iRKTextX + 2, iRKTextY + 2); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + + // Front + vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); + vgui::surface()->DrawSetTextPos(iRKTextX, iRKTextY); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + } + // HP bar(s) { const int hpX = bIsLeftSide ? x + iAvatarSize : x; @@ -528,13 +583,12 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, // Extra info row: XP | Death | Round kills | Weapon logo { const int iInfoTall = iAvatarSize / 2.0f; - const int iInfoX = bIsLeftSide ? x + iAvatarSize + iTextPad : x + iTextPad; const int iInfoY = y + iInfoTall; const int iTotalW = wide - iAvatarSize - (iTextPad * 2); - // Split available width into 4 equal columns with small gaps + // Sizing split between XP and death const int iGap = flWideAs43 / 360.0f; - const int iColW = iTotalW / 4; + const int iColW = iTotalW / 5; const int iTextFontTall = vgui::surface()->GetFontTall(m_hInfoFont); const int iTextFontOffset = (iInfoTall - iTextFontTall) / 2.0f; @@ -551,13 +605,11 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->GetTextSize(g_hFontKillfeedIcons, wszDeathsIcon, iDeathIconWide, iDeathIconTall); } - int iWepWide = 0, iWepTall = 0; - vgui::surface()->GetTextSize(g_hFontKillfeedIcons, card.wszWeaponIcon, iWepWide, iWepTall); - wchar_t wszNum[8] = {}; + const int iInfoX = bIsLeftSide ? x + iAvatarSize + iTextPad : x + (wide - iAvatarSize) - iTextPad; - // XP - const int iXPosXP = bIsLeftSide ? iInfoX + iGap : x + (wide - iAvatarSize) - iColW + iGap; + // XP (left of death) + const int iXPosXP = bIsLeftSide ? iInfoX + iGap : iInfoX - (iColW * 2) + iGap; // XP - Rank Icon { const wchar_t wcRankIcon = NEO_HUD_DEATHNOTICEICON_RANKLESS_DOG + GetRank(card.iXP); @@ -573,8 +625,8 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); } - // Death - const int iXPosDeath = bIsLeftSide ? iXPosXP + iColW + iGap : iXPosXP - iColW + iGap; + // Death (right of XP) + const int iXPosDeath = bIsLeftSide ? iInfoX + iColW + iGap : iInfoX - iColW + iGap; // Death - Icon { static const wchar_t WC_KILLICON = NEO_HUD_DEATHNOTICEICON_KILL; @@ -590,24 +642,50 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); } - if (card.iRoundKills > 0) - { - const int iXPosRoundKills = bIsLeftSide - ? iXPosDeath + iColW + iGap - : iXPosDeath - iColW + iGap; - // Kills in current round - V_swprintf_safe(wszNum, L"x%d", card.iRoundKills); - vgui::surface()->DrawSetTextFont(m_hInfoFont); - vgui::surface()->DrawSetTextPos(iXPosRoundKills, iInfoY + iTextFontOffset); - vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); - } - if (card.bAlive) { - const int iXPosWepIcon = bIsLeftSide ? x + wide - iTextPad - iWepWide : x + iTextPad; - vgui::surface()->DrawSetTextFont(g_hFontKillfeedIcons); - vgui::surface()->DrawSetTextPos(iXPosWepIcon, iInfoY + ((iInfoTall - iWepTall) / 2.0f)); - vgui::surface()->DrawPrintText(card.wszWeaponIcon, 1); + int iWepPosOffsetX = iTextPad; + + // Primary (larger icon, most right in card for Jinrai, most left in card for NSF) + // If ghost is primary, this is differed to the ghoster player indicator + if (card.iWeaponIdxPrimary >= 0 + && NEO_HUD_DEATHNOTICEICON_GHOST != card.wcaWeaponIcons[card.iWeaponIdxPrimary]) + { + wchar_t wszWeaponIcon[2] = { card.wcaWeaponIcons[card.iWeaponIdxPrimary], L'\0' }; + + int iWepWide = 0, iWepTall = 0; + vgui::surface()->GetTextSize(g_hFontKillfeedIcons, wszWeaponIcon, iWepWide, iWepTall); + + const int iXPosWepIcon = bIsLeftSide ? x + wide - iTextPad - iWepWide : x + iTextPad; + iWepPosOffsetX += iWepWide + iTextPad; + vgui::surface()->DrawSetTextColor((card.iWeaponIdxPrimary == card.iWeaponIdxActive) + ? OVERLAY_THEME.textPrimary : OVERLAY_THEME.wepInactive); + vgui::surface()->DrawSetTextFont(g_hFontKillfeedIcons); + vgui::surface()->DrawSetTextPos(iXPosWepIcon, iInfoY + ((iInfoTall - iWepTall) / 2.0f)); + vgui::surface()->DrawPrintText(wszWeaponIcon, 1); + } + + // Secondary + Throwables (smaller icons) + for (int i = 0; i < card.iTotalWeaponIcons; ++i) + { + if (i == card.iWeaponIdxPrimary) + { + continue; + } + + wchar_t wszWeaponIcon[2] = { card.wcaWeaponIcons[i], L'\0' }; + + int iWepWide = 0, iWepTall = 0; + vgui::surface()->GetTextSize(m_hSmallWeaponsFont, wszWeaponIcon, iWepWide, iWepTall); + + const int iXPosWepIcon = bIsLeftSide ? x + wide - iWepPosOffsetX - iWepWide : x + iWepPosOffsetX; + iWepPosOffsetX += iWepWide + iTextPad; + vgui::surface()->DrawSetTextColor((i == card.iWeaponIdxActive) + ? OVERLAY_THEME.textPrimary : OVERLAY_THEME.wepInactive); + vgui::surface()->DrawSetTextFont(m_hSmallWeaponsFont); + vgui::surface()->DrawSetTextPos(iXPosWepIcon, iInfoY + ((iInfoTall - iWepTall) * 0.66f)); + vgui::surface()->DrawPrintText(wszWeaponIcon, 1); + } } } @@ -619,6 +697,11 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, int iGhostWide = 0, iGhostTall = 0; vgui::surface()->GetTextSize(m_hGhostFont, WSZ_DNI_GHOST, iGhostWide, iGhostTall); + vgui::surface()->DrawSetTextColor( + (card.iWeaponIdxActive >= 0 + && NEO_HUD_DEATHNOTICEICON_GHOST == card.wcaWeaponIcons[card.iWeaponIdxActive]) + ? OVERLAY_THEME.textPrimary + : OVERLAY_THEME.wepInactive); vgui::surface()->DrawSetTextFont(m_hGhostFont); if (bIsLeftSide) { @@ -677,7 +760,7 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() // In 1920x1080, the player card: // Converted to 4:3 is 1440x1080, calculate from 4:3 width regardless of // 16:9/16:10: - // * Total width (including avatar) of 300px, 1440/4.8=300 + // * Total width (including avatar) of 351px, 1440/4.1=~351 // * Avatar width of 64px, 1440/22.5=64 // * Gap from left or right edge of 12px, 1440/120=12 // * Gap between cards Y-axis of 4px, 1440/360=4 @@ -688,7 +771,7 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() float flWideAs43 = static_cast(iScrTall) * (4.0f / 3.0f); if (flWideAs43 > flWide) flWideAs43 = flWide; - const int iTotalCardWide = flWideAs43 / 4.8f; + const int iTotalCardWide = flWideAs43 / 4.1f; const int iAvatarWH = flWideAs43 / 22.5f; const int iGapWide = flWideAs43 / 120.0f; const int iGapBetween = flWideAs43 / 360.0f; 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 0f247d26b8..e65a308d5b 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -12,7 +12,12 @@ struct SpectatorPlayerCard int iEntIndex = 0; int iTeam = 0; wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH] = {}; - wchar_t wszWeaponIcon[2] = {}; + // NEO NOTE (nullsystem): wcaWeaponIcons is NOT used as a string + // but individual characters! + wchar_t wcaWeaponIcons[MAX_WEAPONS] = {}; + int iTotalWeaponIcons = 0; + int iWeaponIdxActive = 0; + int iWeaponIdxPrimary = 0; int iXP = 0; int iDeath = 0; int iClass = 0; @@ -60,8 +65,9 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement vgui::HFont m_hNameFont = 0; vgui::HFont m_hInfoFont = 0; vgui::HFont m_hClassFont = 0; - vgui::HFont m_hHPFont = 0; + vgui::HFont m_hRKHPFont = 0; vgui::HFont m_hGhostFont = 0; + vgui::HFont m_hSmallWeaponsFont = 0; int m_iDeadTex = 0; SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; From c52f6418fd91e4975f2708970fad6336acf11977 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:56:53 +0100 Subject: [PATCH 03/17] Tweak ClientScheme.res spec + overlay + scoreboard fonts Mainly for higher res like 1440p and 2160p/4k --- game/neo/resource/ClientScheme.res | 154 +++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 19 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 74d17d2921..6655075b27 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -282,13 +282,31 @@ Scheme "5" { "name" "Verdana" - "tall" "16" [!$LINUX] //"24" - "tall" "20" [$LINUX] + "tall" "18" [!$LINUX] //"24" + "tall" "22" [$LINUX] "weight" "900" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" } "6" + { + "name" "Verdana" + "tall" "22" [!$LINUX] //"24" + "tall" "26" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1600 2099" + } + "7" + { + "name" "Verdana" + "tall" "26" [!$LINUX] //"24" + "tall" "32" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "2100 10000" + } + "8" { "name" "Verdana" "tall" "12" [!$LINUX] @@ -296,7 +314,7 @@ Scheme "range" "0x0000 0x00FF" "weight" "900" } - "7" + "9" { "name" "Arial" "tall" "12" @@ -589,7 +607,27 @@ Scheme "tall" "22" [$LINUX] "weight" "1000" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "6" + { + "name" "Verdana" + "tall" "20" [!$LINUX] + "tall" "24" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1600 2099" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "7" + { + "name" "Verdana" + "tall" "24" [!$LINUX] + "tall" "28" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "2100 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A } } @@ -645,7 +683,17 @@ Scheme "tall" "24" [$LINUX] "weight" "1000" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "6" + { + "name" "Verdana" + "tall" "26" [!$LINUX] + "tall" "32" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1600 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A } } @@ -701,7 +749,17 @@ Scheme "tall" "22" [$LINUX] "weight" "1000" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "6" + { + "name" "Verdana" + "tall" "24" [!$LINUX] + "tall" "28" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1600 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A } } @@ -757,7 +815,17 @@ Scheme "tall" "20" [$LINUX] "weight" "900" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "6" + { + "name" "Verdana" + "tall" "24" [!$LINUX] + "tall" "28" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1600 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A } } @@ -813,7 +881,27 @@ Scheme "tall" "26" [$LINUX] "weight" "1000" "antialias" "1" - "yres" "1200 10000" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "6" + { + "name" "Verdana" + "tall" "26" [!$LINUX] + "tall" "32" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1600 2099" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + } + "7" + { + "name" "Verdana" + "tall" "36" [!$LINUX] + "tall" "42" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "2100 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A } } @@ -884,7 +972,17 @@ Scheme "tall" "52" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "1600 6000" + "yres" "1600 2099" + "antialias" "1" + "additive" "0" + } + "8" + { + "name" "killfeedicons" + "tall" "62" + "weight" "600" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "2100 6000" "antialias" "1" "additive" "0" } @@ -895,7 +993,7 @@ Scheme "1" { "name" "killfeedicons" - "tall" "12" + "tall" "9" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "480 599" @@ -904,7 +1002,7 @@ Scheme "2" { "name" "killfeedicons" - "tall" "14" + "tall" "10" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "600 767" @@ -913,7 +1011,7 @@ Scheme "3" { "name" "killfeedicons" - "tall" "16" + "tall" "12" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "768 1023" @@ -923,7 +1021,7 @@ Scheme "4" { "name" "killfeedicons" - "tall" "14"// TESTING + "tall" "14" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "1024 1199" @@ -933,7 +1031,7 @@ Scheme "5" { "name" "killfeedicons" - "tall" "24" + "tall" "16" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "1200 1440" @@ -943,7 +1041,7 @@ Scheme "6" { "name" "killfeedicons" - "tall" "26" + "tall" "22" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "1441 1599" @@ -953,7 +1051,7 @@ Scheme "7" { "name" "killfeedicons" - "tall" "28" + "tall" "26" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "1600 6000" @@ -1753,7 +1851,7 @@ Scheme "7" { "name" "killfeedicons" - "tall" "28" + "tall" "32" "weight" "600" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "yres" "1600 6000" @@ -2875,7 +2973,25 @@ Scheme "tall" "15" "weight" "0" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "1200 6000" + "yres" "1200 1599" + "antialias" "1" + } + "4" + { + "name" "Verdana" + "tall" "18" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1600 2099" + "antialias" "1" + } + "5" + { + "name" "Verdana" + "tall" "24" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "2100 6000" "antialias" "1" } } From 24193635547df72f826805ea1d63205446799846 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:20:10 +0100 Subject: [PATCH 04/17] Give dead icon in overlay a red conversion CAvatarImage altered so it generated a dead avatar variant where the colors is brightened, saturated, and grayscaled to red. --- .../neo/ui/neo_hud_spectator_overlay.cpp | 28 ++++++------- .../client/neo/ui/neo_hud_spectator_overlay.h | 3 +- src/game/client/neo/ui/neo_scoreboard.cpp | 5 +++ src/game/client/neo/ui/neo_scoreboard.h | 1 + src/game/client/vgui_avatarimage.cpp | 42 +++++++++++++++++++ src/game/client/vgui_avatarimage.h | 14 +++++++ 6 files changed, 77 insertions(+), 16 deletions(-) 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 fa46ca5cd8..6fb827b7c5 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -191,7 +191,8 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() ++m_iTeamPlayersCount[iTeam]; // Reuse the avatar image already generated by the scoreboard - pCard->pAvatar = nullptr; + pCard->pAvatarAlive = nullptr; + pCard->pAvatarDead = nullptr; if (g_pNeoScoreBoard && g_pNeoScoreBoard->m_pImageList && CNEOScoreBoard::ShowAvatars()) { const CSteamID steamId = GetSteamIDForPlayerIndex(i); @@ -204,11 +205,15 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() const MapAvatarValue &avatar = mapAvatars[iMapIdx]; if (avatar.i184Idx >= 0) { - pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + } + if (avatar.i184DeadIdx >= 0) + { + pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184DeadIdx)); } } - bScoreboardNeedUpdate = !pCard->pAvatar; + bScoreboardNeedUpdate = bScoreboardNeedUpdate || !pCard->pAvatarAlive || !pCard->pAvatarDead; } } @@ -362,11 +367,12 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawFilledRect(iAvatarX + 1, iAvatarY + 1, iAvatarX + iAvatarSize - 1, iAvatarY + iAvatarSize - 1); // Draw the actual Steam avatar (borrowed from the scoreboard's avatar list) - if (card.pAvatar && card.pAvatar->IsValid()) + CAvatarImage *pAvatar = card.bAlive ? card.pAvatarAlive : card.pAvatarDead; + if (pAvatar && pAvatar->IsValid()) { - card.pAvatar->SetPos(iAvatarX, iAvatarY); - card.pAvatar->SetSize(iAvatarSize, iAvatarSize); - card.pAvatar->Paint(); + pAvatar->SetPos(iAvatarX, iAvatarY); + pAvatar->SetSize(iAvatarSize, iAvatarSize); + pAvatar->Paint(); } else if (card.wszPlayerName[0]) { @@ -411,14 +417,6 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, } else { - // Dead red tint - vgui::surface()->DrawSetColor(OVERLAY_THEME.deadAvatarTint); - vgui::surface()->DrawFilledRect( - iAvatarX, - iAvatarY, - iAvatarX + iAvatarSize, - iAvatarY + iAvatarSize); - // Dead icon vgui::surface()->DrawSetColor(COLOR_WHITE); vgui::surface()->DrawSetTexture(m_iDeadTex); 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 e65a308d5b..8da39a3a00 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -24,7 +24,8 @@ struct SpectatorPlayerCard int iHP = 0; int iRoundKills = 0; bool bAlive = false; - CAvatarImage *pAvatar = nullptr; + CAvatarImage *pAvatarAlive = nullptr; + CAvatarImage *pAvatarDead = nullptr; // Used for animations/fading float flLastAttackTime = 0.0f; float flLastAliveTime = 0.0f; diff --git a/src/game/client/neo/ui/neo_scoreboard.cpp b/src/game/client/neo/ui/neo_scoreboard.cpp index c4b3748875..d0081f2855 100644 --- a/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/src/game/client/neo/ui/neo_scoreboard.cpp @@ -502,14 +502,19 @@ void CNEOScoreBoard::Update() auto *pImage64 = new CAvatarImage; auto *pImage184 = new CAvatarImage; + auto *pImageDead184 = new CAvatarImage; + pImageDead184->m_bDeadAvatar = true; + pImage32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); pImage64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); pImage184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); + pImageDead184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); pPlayerInfo->avatar = { .i32Idx = m_pImageList->AddImage(pImage32), .i64Idx = m_pImageList->AddImage(pImage64), .i184Idx = m_pImageList->AddImage(pImage184), + .i184DeadIdx = m_pImageList->AddImage(pImageDead184), }; m_mapAvatarsToImageList.Insert(pPlayerInfo->steamID, pPlayerInfo->avatar); diff --git a/src/game/client/neo/ui/neo_scoreboard.h b/src/game/client/neo/ui/neo_scoreboard.h index 5d96ed3180..f2a613f84f 100644 --- a/src/game/client/neo/ui/neo_scoreboard.h +++ b/src/game/client/neo/ui/neo_scoreboard.h @@ -23,6 +23,7 @@ struct MapAvatarValue int i32Idx; int i64Idx; int i184Idx; + int i184DeadIdx; }; struct CNEOScoreBoardPlayer diff --git a/src/game/client/vgui_avatarimage.cpp b/src/game/client/vgui_avatarimage.cpp index 3756ba940d..5e61fae355 100644 --- a/src/game/client/vgui_avatarimage.cpp +++ b/src/game/client/vgui_avatarimage.cpp @@ -182,7 +182,41 @@ void CAvatarImage::LoadAvatarImage() int destBufferSize = wide * tall * 4; byte *rgbDest = (byte*)stackalloc( destBufferSize ); if ( steamapicontext->SteamUtils()->GetImageRGBA( iAvatar, rgbDest, destBufferSize ) ) +#ifdef NEO + { + if (m_bDeadAvatar) + { + const float contrast = -64.0f; + const float factor = (259.0f * (contrast + 255.0f)) / (255.0f * (259.0f - contrast)); + for (uint32 offset = 0; offset < (wide * tall * 4); offset += 4) + { + int r = (rgbDest + offset)[0]; + int g = (rgbDest + offset)[1]; + int b = (rgbDest + offset)[2]; + + // Bump brightness + r = Clamp((int)(((r / 255.0f) * 1.25f) * 255.0f), 0, 255); + g = Clamp((int)(((g / 255.0f) * 1.25f) * 255.0f), 0, 255); + b = Clamp((int)(((b / 255.0f) * 1.25f) * 255.0f), 0, 255); + + // Bump down contrast + r = Clamp((int)(factor * (r - 128) + 128), 0, 255); + g = Clamp((int)(factor * (g - 128) + 128), 0, 255); + b = Clamp((int)(factor * (b - 128) + 128), 0, 255); + + // Convert to grayscale - Luminosity, then only for red + const float flNGray = ((0.3f * (r / 255.0f)) + (0.59f * (g / 255.0f)) + (0.11f * (b / 255.0f))); + const int iGray = Clamp((int)(255.0f * flNGray), 0, 255); + (rgbDest + offset)[0] = iGray; + (rgbDest + offset)[1] = 0; + (rgbDest + offset)[2] = 0; + } + } + InitFromRGBA( iAvatar, rgbDest, wide, tall ); + } +#else InitFromRGBA( iAvatar, rgbDest, wide, tall ); +#endif stackfree( rgbDest ); } @@ -220,12 +254,20 @@ void CAvatarImage::UpdateFriendStatus( void ) //----------------------------------------------------------------------------- void CAvatarImage::InitFromRGBA( int iAvatar, const byte *rgba, int width, int height ) { +#ifdef NEO + int iTexIndex = s_AvatarImageCache.Find( AvatarImagePair_t( m_SteamID, iAvatar, m_bDeadAvatar ) ); +#else int iTexIndex = s_AvatarImageCache.Find( AvatarImagePair_t( m_SteamID, iAvatar ) ); +#endif if ( iTexIndex == s_AvatarImageCache.InvalidIndex() ) { m_iTextureID = vgui::surface()->CreateNewTextureID( true ); g_pMatSystemSurface->DrawSetTextureRGBAEx2( m_iTextureID, rgba, width, height, IMAGE_FORMAT_RGBA8888, true ); +#ifdef NEO + iTexIndex = s_AvatarImageCache.Insert( AvatarImagePair_t( m_SteamID, iAvatar, m_bDeadAvatar ) ); +#else iTexIndex = s_AvatarImageCache.Insert( AvatarImagePair_t( m_SteamID, iAvatar ) ); +#endif s_AvatarImageCache[ iTexIndex ] = m_iTextureID; } else diff --git a/src/game/client/vgui_avatarimage.h b/src/game/client/vgui_avatarimage.h index 2c264b9053..f811b5a398 100644 --- a/src/game/client/vgui_avatarimage.h +++ b/src/game/client/vgui_avatarimage.h @@ -37,15 +37,28 @@ struct AvatarImagePair_t { AvatarImagePair_t() { m_iAvatar = 0; } +#ifdef NEO + AvatarImagePair_t( CSteamID steamID, int av, bool dead ) { m_SteamID = steamID; m_iAvatar = av; m_bDeadAvatar = dead; } +#else AvatarImagePair_t( CSteamID steamID, int av ) { m_SteamID = steamID; m_iAvatar = av; } +#endif bool operator<( const AvatarImagePair_t &rhs ) const { +#ifdef NEO + return m_SteamID.ConvertToUint64() < rhs.m_SteamID.ConvertToUint64() || + ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar < rhs.m_iAvatar ) || + ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar == rhs.m_iAvatar && m_bDeadAvatar < rhs.m_bDeadAvatar ); +#else return m_SteamID.ConvertToUint64() < rhs.m_SteamID.ConvertToUint64() || ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar < rhs.m_iAvatar ); +#endif } CSteamID m_SteamID; int m_iAvatar; +#ifdef NEO + bool m_bDeadAvatar = false; +#endif }; //----------------------------------------------------------------------------- @@ -121,6 +134,7 @@ class CAvatarImage : public vgui::IImage int GetAvatarTall() { return m_avatarTall; } #ifdef NEO int getTextureID() { return m_iTextureID; } + bool m_bDeadAvatar = false; #endif //============================================================================= From 675bde417a1425e432edfe3314871e71723e2dd0 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:02:47 +0100 Subject: [PATCH 05/17] Move off class/loadout/team font use to its own The menu isn't scaled for higher res properly so just let it have its own variant of Default for now. --- game/neo/resource/ClientScheme.res | 127 ++++++++++++++++++ game/neo/resource/neo_ui/Neo_ClassMenu.res | 10 +- .../resource/neo_ui/Neo_LoadoutMenu_Other.res | 2 +- game/neo/resource/neo_ui/Neo_TeamMenu.res | 20 +-- 4 files changed, 143 insertions(+), 16 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 6655075b27..d1bcbbcb65 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -464,6 +464,133 @@ Scheme "weight" "0" } } + // For the class, loadout, and team menu font, not scaled for higher res + "CLTDefault" + { + "1" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "15" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "480 599" + } + "2" + { + "name" "Verdana" + "tall" "13" [!$LINUX] + "tall" "16" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "600 767" + } + "3" + { + "name" "Verdana" + "tall" "14" [!$LINUX] + "tall" "18" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + } + "4" + { + "name" "Verdana" + "tall" "16" [!$LINUX] //"20" + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + } + "5" + { + "name" "Verdana" + "tall" "18" [!$LINUX] //"24" + "tall" "22" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1200 10000" + } + "8" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "15" [$LINUX] + "range" "0x0000 0x00FF" + "weight" "900" + } + "9" + { + "name" "Arial" + "tall" "12" + "range" "0x0000 0x00FF" + "weight" "800" + } + } + "CLTDefaultBold" + { + "1" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "15" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "480 599" + } + "2" + { + "name" "Verdana" + "tall" "13" [!$LINUX] + "tall" "16" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "600 767" + } + "3" + { + "name" "Verdana" + "tall" "14" [!$LINUX] + "tall" "18" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + } + "4" + { + "name" "Verdana" + "tall" "16" [!$LINUX] //"20" + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + } + "5" + { + "name" "Verdana" + "tall" "18" [!$LINUX] //"24" + "tall" "22" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1200 10000" + } + "8" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "15" [$LINUX] + "range" "0x0000 0x00FF" + "weight" "900" + } + "9" + { + "name" "Arial" + "tall" "12" + "range" "0x0000 0x00FF" + "weight" "800" + } + } HudHintText { "1" diff --git a/game/neo/resource/neo_ui/Neo_ClassMenu.res b/game/neo/resource/neo_ui/Neo_ClassMenu.res index 5d7c9e3ac9..1c29c350c0 100644 --- a/game/neo/resource/neo_ui/Neo_ClassMenu.res +++ b/game/neo/resource/neo_ui/Neo_ClassMenu.res @@ -133,7 +133,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "1" - "font" "Default" + "font" "CLTDefault" "wrap" "0" } "Scout_Button" @@ -153,7 +153,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "Default" + "font" "CLTDefault" "wrap" "0" "Command" "setclass 1" "Default" "0" @@ -175,7 +175,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "1" - "font" "Default" + "font" "CLTDefault" "wrap" "0" } "Assault_Button" @@ -216,7 +216,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "1" - "font" "Default" + "font" "CLTDefault" "wrap" "0" } "Heavy_Button" @@ -308,7 +308,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "Default" + "font" "CLTDefault" "wrap" "0" "Command" "playerstate_reverse" "Default" "0" diff --git a/game/neo/resource/neo_ui/Neo_LoadoutMenu_Other.res b/game/neo/resource/neo_ui/Neo_LoadoutMenu_Other.res index 1763ff630c..e2e0220136 100644 --- a/game/neo/resource/neo_ui/Neo_LoadoutMenu_Other.res +++ b/game/neo/resource/neo_ui/Neo_LoadoutMenu_Other.res @@ -546,7 +546,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "Default" + "font" "CLTDefault" "wrap" "0" "Command" "playerstate_reverse" "Default" "0" diff --git a/game/neo/resource/neo_ui/Neo_TeamMenu.res b/game/neo/resource/neo_ui/Neo_TeamMenu.res index 3679f0a9c9..6ec0293a88 100644 --- a/game/neo/resource/neo_ui/Neo_TeamMenu.res +++ b/game/neo/resource/neo_ui/Neo_TeamMenu.res @@ -114,7 +114,7 @@ "textAlignment" "east" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" } "neo_IconPanel4" @@ -151,7 +151,7 @@ "textAlignment" "west" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" } "neo_jscorelabel" @@ -171,7 +171,7 @@ "textAlignment" "west" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" } "neo_Divider1" @@ -205,7 +205,7 @@ "textAlignment" "west" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" } "neo_nscorelabel" @@ -225,7 +225,7 @@ "textAlignment" "west" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" } "neo_jinraibutton" @@ -245,7 +245,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" "command" "jointeam 2" "Default" "0" @@ -267,7 +267,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" "command" "jointeam 3" "Default" "0" @@ -289,7 +289,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" "command" "jointeam a" "Default" "0" @@ -311,7 +311,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" "command" "jointeam 1" "Default" "0" @@ -333,7 +333,7 @@ "textAlignment" "center" "dulltext" "0" "brighttext" "0" - "font" "DefaultBold" + "font" "CLTDefaultBold" "wrap" "0" "command" "vguicancel" "Default" "1" From 4e4dd5adb9cf128954d92a404995a8bb16607fbe Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:05:25 +0100 Subject: [PATCH 06/17] Use resource outline/shadow not do it ourselves --- game/neo/resource/ClientScheme.res | 25 +++++++--- .../neo/ui/neo_hud_spectator_overlay.cpp | 50 ++++--------------- 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index d1bcbbcb65..8ac13bc953 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -770,7 +770,7 @@ Scheme "antialias" "1" "yres" "1 599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "dropshadow" "1" } "2" { @@ -781,7 +781,7 @@ Scheme "antialias" "1" "yres" "600 767" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "dropshadow" "1" } "3" { @@ -792,6 +792,7 @@ Scheme "antialias" "1" "yres" "768 1023" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "4" { @@ -802,6 +803,7 @@ Scheme "antialias" "1" "yres" "1024 1199" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "5" { @@ -812,6 +814,7 @@ Scheme "antialias" "1" "yres" "1200 1599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "6" { @@ -822,6 +825,7 @@ Scheme "antialias" "1" "yres" "1600 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } } @@ -902,7 +906,7 @@ Scheme "antialias" "1" "yres" "1 599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "dropshadow" "1" } "2" { @@ -913,7 +917,7 @@ Scheme "antialias" "1" "yres" "600 767" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "dropshadow" "1" } "3" { @@ -924,6 +928,7 @@ Scheme "antialias" "1" "yres" "768 1023" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "4" { @@ -934,6 +939,7 @@ Scheme "antialias" "1" "yres" "1024 1199" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "5" { @@ -944,6 +950,7 @@ Scheme "antialias" "1" "yres" "1200 1599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } "6" { @@ -954,6 +961,7 @@ Scheme "antialias" "1" "yres" "1600 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" } } @@ -968,7 +976,7 @@ Scheme "antialias" "1" "yres" "1 599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "outline" "1" } "2" { @@ -979,7 +987,7 @@ Scheme "antialias" "1" "yres" "600 767" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "additive" "1" + "outline" "1" } "3" { @@ -990,6 +998,7 @@ Scheme "antialias" "1" "yres" "768 1023" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "outline" "1" } "4" { @@ -1000,6 +1009,7 @@ Scheme "antialias" "1" "yres" "1024 1199" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "outline" "1" } "5" { @@ -1010,6 +1020,7 @@ Scheme "antialias" "1" "yres" "1200 1599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "outline" "1" } "6" { @@ -1020,6 +1031,7 @@ Scheme "antialias" "1" "yres" "1600 2099" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "outline" "1" } "7" { @@ -1030,6 +1042,7 @@ Scheme "antialias" "1" "yres" "2100 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "outline" "1" } } 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 6fb827b7c5..05f96b396e 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -405,12 +405,6 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, : iAvatarX + iAvatarSize - iWideHP - iTextPad; const int iHPTextY = iAvatarY + iAvatarSize - iTallHP; - // Shadow - vgui::surface()->DrawSetTextColor(COLOR_BLACK); - vgui::surface()->DrawSetTextPos(iHPTextX + 2, iHPTextY + 2); - vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); - - // Front vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); vgui::surface()->DrawSetTextPos(iHPTextX, iHPTextY); vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); @@ -443,12 +437,6 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, : iAvatarX + iAvatarSize - iWideRK - iTextPad; const int iRKTextY = iAvatarY; - // Shadow - vgui::surface()->DrawSetTextColor(COLOR_BLACK); - vgui::surface()->DrawSetTextPos(iRKTextX + 2, iRKTextY + 2); - vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); - - // Front vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); vgui::surface()->DrawSetTextPos(iRKTextX, iRKTextY); vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); @@ -537,20 +525,11 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, : x + (wide - iAvatarSize) - iTextPad - iPlayerNameWide; const int iNameY = y + (((iAvatarSize / 2.0f) - iPlayerNameTall) / 2.0f); - // Shadow - { - vgui::surface()->DrawSetTextColor(COLOR_BLACK); - vgui::surface()->DrawSetTextPos(iNameX + 2, iNameY + 2); - vgui::surface()->DrawPrintText(card.wszPlayerName, V_wcslen(card.wszPlayerName)); - } - // Front - { - vgui::surface()->DrawSetTextColor(card.bAlive - ? OVERLAY_THEME.textPrimary - : Color(140, 140, 150, 200)); - vgui::surface()->DrawSetTextPos(iNameX, iNameY); - vgui::surface()->DrawPrintText(card.wszPlayerName, V_wcslen(card.wszPlayerName)); - } + vgui::surface()->DrawSetTextColor(card.bAlive + ? OVERLAY_THEME.textPrimary + : Color(140, 140, 150, 200)); + vgui::surface()->DrawSetTextPos(iNameX, iNameY); + vgui::surface()->DrawPrintText(card.wszPlayerName, V_wcslen(card.wszPlayerName)); // Class name vgui::surface()->DrawSetTextFont(m_hClassFont); @@ -562,20 +541,11 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, : x + iTextPad; const int iClNamePosY = y + (((iAvatarSize / 2.0f) - iClassNameTall) / 2.0f); - // Shadow - { - vgui::surface()->DrawSetTextColor(COLOR_BLACK); - vgui::surface()->DrawSetTextPos(iClNamePosX + 1, iClNamePosY + 1); - vgui::surface()->DrawPrintText(pwszClassName, V_wcslen(pwszClassName)); - } - // Front - { - vgui::surface()->DrawSetTextColor(card.bAlive - ? OVERLAY_THEME.textPrimary - : Color(140, 140, 150, 200)); - vgui::surface()->DrawSetTextPos(iClNamePosX, iClNamePosY); - vgui::surface()->DrawPrintText(pwszClassName, V_wcslen(pwszClassName)); - } + vgui::surface()->DrawSetTextColor(card.bAlive + ? OVERLAY_THEME.textPrimary + : Color(140, 140, 150, 200)); + vgui::surface()->DrawSetTextPos(iClNamePosX, iClNamePosY); + vgui::surface()->DrawPrintText(pwszClassName, V_wcslen(pwszClassName)); } // Extra info row: XP | Death | Round kills | Weapon logo From 0a655c901717844a1c62c8a442809ad179f3ad62 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:36:39 +0100 Subject: [PATCH 07/17] fix non-unity build --- src/game/client/neo/ui/neo_hud_spectator_overlay.cpp | 1 + 1 file changed, 1 insertion(+) 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 05f96b396e..a6fa165118 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include From d8ea890e40ea45bb90981ffc8378dfc1a1b52f34 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:09:20 +0100 Subject: [PATCH 08/17] more aliased looking outlines --- game/neo/resource/ClientScheme.res | 91 ++++++++++++++++++- .../neo/ui/neo_hud_spectator_overlay.cpp | 22 ++++- .../client/neo/ui/neo_hud_spectator_overlay.h | 1 + 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 8ac13bc953..6a4422bb25 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -965,7 +965,88 @@ Scheme } } - NHudSpectatorOverlayRoundKillHP + NHudSpectatorOverlayRoundKillHPFront + { + "1" + { + "name" "Verdana" + "tall" "12" [!$LINUX] + "tall" "14" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "1 599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "2" + { + "name" "Verdana" + "tall" "14" [!$LINUX] + "tall" "16" [$LINUX] + "weight" "700" + "antialias" "1" + "yres" "600 767" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "3" + { + "name" "Verdana" + "tall" "16" [!$LINUX] + "tall" "20" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "768 1023" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "4" + { + "name" "Verdana" + "tall" "20" [!$LINUX] + "tall" "24" [$LINUX] + "weight" "900" + "antialias" "1" + "yres" "1024 1199" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "5" + { + "name" "Verdana" + "tall" "22" [!$LINUX] + "tall" "26" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1200 1599" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "6" + { + "name" "Verdana" + "tall" "26" [!$LINUX] + "tall" "32" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "1600 2099" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + "7" + { + "name" "Verdana" + "tall" "36" [!$LINUX] + "tall" "42" [$LINUX] + "weight" "1000" + "antialias" "1" + "yres" "2100 10000" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "dropshadow" "1" + } + } + + NHudSpectatorOverlayRoundKillHPBack { "1" { @@ -977,6 +1058,7 @@ Scheme "yres" "1 599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "2" { @@ -988,6 +1070,7 @@ Scheme "yres" "600 767" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "3" { @@ -999,6 +1082,7 @@ Scheme "yres" "768 1023" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "4" { @@ -1010,6 +1094,7 @@ Scheme "yres" "1024 1199" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "5" { @@ -1021,6 +1106,7 @@ Scheme "yres" "1200 1599" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "6" { @@ -1032,6 +1118,7 @@ Scheme "yres" "1600 2099" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } "7" { @@ -1043,9 +1130,11 @@ Scheme "yres" "2100 10000" "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A "outline" "1" + "blur" "1" } } + NHudSpectatorOverlayGhost { "1" 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 a6fa165118..0f8807799d 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -135,7 +135,8 @@ void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) m_hNameFont = pScheme->GetFont("NHudSpectatorOverlayName", true); m_hInfoFont = pScheme->GetFont("NHudSpectatorOverlayInfo", true); m_hClassFont = pScheme->GetFont("NHudSpectatorOverlayClass", true); - m_hRKHPFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHP", true); + m_hRKHPFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHPFront", true); + m_hRKHPBackFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHPBack", true); m_hGhostFont = pScheme->GetFont("NHudSpectatorOverlayGhost", true); m_hSmallWeaponsFont = pScheme->GetFont("NHudSpectatorOverlaySmallWeapons", true); @@ -144,6 +145,7 @@ void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) if (!m_hInfoFont) m_hInfoFont = pScheme->GetFont("DefaultSmall", true); if (!m_hClassFont) m_hClassFont = pScheme->GetFont("DefaultSmall", true); if (!m_hRKHPFont) m_hRKHPFont = pScheme->GetFont("Default", true); + if (!m_hRKHPBackFont) m_hRKHPBackFont = pScheme->GetFont("Default", true); if (!m_hGhostFont) m_hGhostFont = pScheme->GetFont("NHudKillfeedIcons", true); if (!m_hSmallWeaponsFont) m_hSmallWeaponsFont = pScheme->GetFont("NHudKillfeedIcons", true); @@ -399,13 +401,20 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, int iWideHP = 0, iTallHP = 0; vgui::surface()->GetTextSize(m_hRKHPFont, wszHP, iWideHP, iTallHP); - vgui::surface()->DrawSetTextFont(m_hRKHPFont); const int iHPTextX = bIsLeftSide ? iAvatarX + iTextPad : iAvatarX + iAvatarSize - iWideHP - iTextPad; const int iHPTextY = iAvatarY + iAvatarSize - iTallHP; + // Back + vgui::surface()->DrawSetTextFont(m_hRKHPBackFont); + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iHPTextX, iHPTextY); + vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); + + // Front + vgui::surface()->DrawSetTextFont(m_hRKHPFont); vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); vgui::surface()->DrawSetTextPos(iHPTextX, iHPTextY); vgui::surface()->DrawPrintText(wszHP, V_wcslen(wszHP)); @@ -431,13 +440,20 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, int iWideRK = 0, iTallRK = 0; vgui::surface()->GetTextSize(m_hRKHPFont, wszNum, iWideRK, iTallRK); - vgui::surface()->DrawSetTextFont(m_hRKHPFont); const int iRKTextX = bIsLeftSide ? iAvatarX + iTextPad : iAvatarX + iAvatarSize - iWideRK - iTextPad; const int iRKTextY = iAvatarY; + // Back + vgui::surface()->DrawSetTextFont(m_hRKHPBackFont); + vgui::surface()->DrawSetTextColor(COLOR_BLACK); + vgui::surface()->DrawSetTextPos(iRKTextX, iRKTextY); + vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); + + // Front + vgui::surface()->DrawSetTextFont(m_hRKHPFont); vgui::surface()->DrawSetTextColor(OVERLAY_THEME.textPrimary); vgui::surface()->DrawSetTextPos(iRKTextX, iRKTextY); vgui::surface()->DrawPrintText(wszNum, V_wcslen(wszNum)); 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 8da39a3a00..07be96bbd2 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -67,6 +67,7 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement vgui::HFont m_hInfoFont = 0; vgui::HFont m_hClassFont = 0; vgui::HFont m_hRKHPFont = 0; + vgui::HFont m_hRKHPBackFont = 0; vgui::HFont m_hGhostFont = 0; vgui::HFont m_hSmallWeaponsFont = 0; int m_iDeadTex = 0; From 9e5155f284db4476e4ad1451b2dbb82141a51a53 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:52:48 +0100 Subject: [PATCH 09/17] add fallback lower res avatars --- .../client/neo/ui/neo_hud_spectator_overlay.cpp | 17 +++++++++++++++++ src/game/client/neo/ui/neo_scoreboard.cpp | 13 +++++++++++++ src/game/client/neo/ui/neo_scoreboard.h | 2 ++ 3 files changed, 32 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 0f8807799d..ae49f7c7de 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -210,10 +210,27 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() { pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); } + else if (avatar.i64Idx >= 0) + { + pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); + } + else if (avatar.i32Idx >= 0) + { + pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); + } + if (avatar.i184DeadIdx >= 0) { pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184DeadIdx)); } + else if (avatar.i64DeadIdx >= 0) + { + pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64DeadIdx)); + } + else if (avatar.i32DeadIdx >= 0) + { + pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32DeadIdx)); + } } bScoreboardNeedUpdate = bScoreboardNeedUpdate || !pCard->pAvatarAlive || !pCard->pAvatarDead; diff --git a/src/game/client/neo/ui/neo_scoreboard.cpp b/src/game/client/neo/ui/neo_scoreboard.cpp index d0081f2855..fc0eef3669 100644 --- a/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/src/game/client/neo/ui/neo_scoreboard.cpp @@ -498,21 +498,31 @@ void CNEOScoreBoard::Update() int iMapIndex = m_mapAvatarsToImageList.Find(pPlayerInfo->steamID); if (iMapIndex == m_mapAvatarsToImageList.InvalidIndex()) { + // NEO TODO (nullsystem): Improve the way it fetches images, generate + // both non-altered and dead from one request auto *pImage32 = new CAvatarImage; auto *pImage64 = new CAvatarImage; auto *pImage184 = new CAvatarImage; + auto *pImageDead32 = new CAvatarImage; + auto *pImageDead64 = new CAvatarImage; auto *pImageDead184 = new CAvatarImage; + pImageDead32->m_bDeadAvatar = true; + pImageDead64->m_bDeadAvatar = true; pImageDead184->m_bDeadAvatar = true; pImage32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); pImage64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); pImage184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); + pImageDead32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); + pImageDead64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); pImageDead184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); pPlayerInfo->avatar = { .i32Idx = m_pImageList->AddImage(pImage32), + .i32DeadIdx = m_pImageList->AddImage(pImageDead32), .i64Idx = m_pImageList->AddImage(pImage64), + .i64DeadIdx = m_pImageList->AddImage(pImageDead64), .i184Idx = m_pImageList->AddImage(pImage184), .i184DeadIdx = m_pImageList->AddImage(pImageDead184), }; @@ -529,6 +539,9 @@ void CNEOScoreBoard::Update() pPlayerInfo->avatar.i32Idx = -1; pPlayerInfo->avatar.i64Idx = -1; pPlayerInfo->avatar.i184Idx = -1; + pPlayerInfo->avatar.i32DeadIdx = -1; + pPlayerInfo->avatar.i64DeadIdx = -1; + pPlayerInfo->avatar.i184DeadIdx = -1; } ++m_iTotalPlayers; diff --git a/src/game/client/neo/ui/neo_scoreboard.h b/src/game/client/neo/ui/neo_scoreboard.h index f2a613f84f..350fa2b1c8 100644 --- a/src/game/client/neo/ui/neo_scoreboard.h +++ b/src/game/client/neo/ui/neo_scoreboard.h @@ -21,7 +21,9 @@ enum ENeoScoreBoardPadding struct MapAvatarValue { int i32Idx; + int i32DeadIdx; int i64Idx; + int i64DeadIdx; int i184Idx; int i184DeadIdx; }; From c92eb326d9b92c560508581d06b92528c75e4870 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:19:49 +0100 Subject: [PATCH 10/17] use CPanelAnimationVar... reclarify CLTDefault --- game/neo/resource/ClientScheme.res | 11 +++++---- game/neo/scripts/HudLayout.res | 10 +++++++- .../neo/ui/neo_hud_spectator_overlay.cpp | 23 +------------------ .../client/neo/ui/neo_hud_spectator_overlay.h | 16 ++++++------- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 6a4422bb25..73f97f7de6 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -464,7 +464,8 @@ Scheme "weight" "0" } } - // For the class, loadout, and team menu font, not scaled for higher res + // For the class, loadout, and team menu to use how the Default font used to be + // because those menu are very small for HiDPI res like 1440/4k "CLTDefault" { "1" @@ -512,7 +513,7 @@ Scheme "antialias" "1" "yres" "1200 10000" } - "8" + "6" { "name" "Verdana" "tall" "12" [!$LINUX] @@ -520,7 +521,7 @@ Scheme "range" "0x0000 0x00FF" "weight" "900" } - "9" + "7" { "name" "Arial" "tall" "12" @@ -575,7 +576,7 @@ Scheme "antialias" "1" "yres" "1200 10000" } - "8" + "6" { "name" "Verdana" "tall" "12" [!$LINUX] @@ -583,7 +584,7 @@ Scheme "range" "0x0000 0x00FF" "weight" "900" } - "9" + "7" { "name" "Arial" "tall" "12" diff --git a/game/neo/scripts/HudLayout.res b/game/neo/scripts/HudLayout.res index 4f409b7356..363158d816 100644 --- a/game/neo/scripts/HudLayout.res +++ b/game/neo/scripts/HudLayout.res @@ -1016,6 +1016,14 @@ neo_spectator_overlay { - "fieldName" "neo_spectator_overlay" + "fieldName" "neo_spectator_overlay" + "NameFont" "NHudSpectatorOverlayName" + "InfoFont" "NHudSpectatorOverlayInfo" + "ClassFont" "NHudSpectatorOverlayClass" + "RKHPFont" "NHudSpectatorOverlayRoundKillHPFront" + "RKHPBackFont" "NHudSpectatorOverlayRoundKillHPBack" + "GhostFont" "NHudSpectatorOverlayGhost" + "SmallWeaponsFont" "NHudSpectatorOverlaySmallWeapons" + "DeadTexture" "vgui/hud/kill_kill" } } 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 ae49f7c7de..8b5a7d243d 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -58,9 +58,6 @@ CNEOHud_SpectatorOverlay::CNEOHud_SpectatorOverlay(const char* pElementName, vgu SetParent(g_pClientMode->GetViewport()); } - m_iDeadTex = vgui::surface()->CreateNewTextureID(true); - vgui::surface()->DrawSetTextureFile(m_iDeadTex, "vgui/hud/kill_kill", false, true); - int iScrWide, iScrTall; vgui::surface()->GetScreenSize(iScrWide, iScrTall); SetBounds(0, 0, iScrWide, iScrTall); @@ -131,24 +128,6 @@ void CNEOHud_SpectatorOverlay::ApplySchemeSettings(vgui::IScheme* pScheme) LoadControlSettings("scripts/HudLayout.res"); - // Try to load fonts specifically for this HUD - m_hNameFont = pScheme->GetFont("NHudSpectatorOverlayName", true); - m_hInfoFont = pScheme->GetFont("NHudSpectatorOverlayInfo", true); - m_hClassFont = pScheme->GetFont("NHudSpectatorOverlayClass", true); - m_hRKHPFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHPFront", true); - m_hRKHPBackFont = pScheme->GetFont("NHudSpectatorOverlayRoundKillHPBack", true); - m_hGhostFont = pScheme->GetFont("NHudSpectatorOverlayGhost", true); - m_hSmallWeaponsFont = pScheme->GetFont("NHudSpectatorOverlaySmallWeapons", true); - - // Fallbacks - if (!m_hNameFont) m_hNameFont = pScheme->GetFont("Default", true); - if (!m_hInfoFont) m_hInfoFont = pScheme->GetFont("DefaultSmall", true); - if (!m_hClassFont) m_hClassFont = pScheme->GetFont("DefaultSmall", true); - if (!m_hRKHPFont) m_hRKHPFont = pScheme->GetFont("Default", true); - if (!m_hRKHPBackFont) m_hRKHPBackFont = pScheme->GetFont("Default", true); - if (!m_hGhostFont) m_hGhostFont = pScheme->GetFont("NHudKillfeedIcons", true); - if (!m_hSmallWeaponsFont) m_hSmallWeaponsFont = pScheme->GetFont("NHudKillfeedIcons", true); - int iScrWide, iScrTall; vgui::surface()->GetScreenSize(iScrWide, iScrTall); SetBounds(0, 0, iScrWide, iScrTall); @@ -440,7 +419,7 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, { // Dead icon vgui::surface()->DrawSetColor(COLOR_WHITE); - vgui::surface()->DrawSetTexture(m_iDeadTex); + vgui::surface()->DrawSetTexture(m_iDeadTexture); vgui::surface()->DrawTexturedRect( iAvatarX, iAvatarY, 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 07be96bbd2..bdab029175 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -63,14 +63,14 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement const int wide, const int tall, const Color accentColor, const float flWideAs43) const; - vgui::HFont m_hNameFont = 0; - vgui::HFont m_hInfoFont = 0; - vgui::HFont m_hClassFont = 0; - vgui::HFont m_hRKHPFont = 0; - vgui::HFont m_hRKHPBackFont = 0; - vgui::HFont m_hGhostFont = 0; - vgui::HFont m_hSmallWeaponsFont = 0; - int m_iDeadTex = 0; + CPanelAnimationVar(vgui::HFont, m_hNameFont, "NameFont", "NHudSpectatorOverlayName"); + CPanelAnimationVar(vgui::HFont, m_hInfoFont, "InfoFont", "NHudSpectatorOverlayInfo"); + CPanelAnimationVar(vgui::HFont, m_hClassFont, "ClassFont", "NHudSpectatorOverlayClass"); + CPanelAnimationVar(vgui::HFont, m_hRKHPFont, "RKHPFont", "NHudSpectatorOverlayRoundKillHPFront"); + CPanelAnimationVar(vgui::HFont, m_hRKHPBackFont, "RKHPBackFont", "NHudSpectatorOverlayRoundKillHPBack"); + CPanelAnimationVar(vgui::HFont, m_hGhostFont, "GhostFont", "NHudSpectatorOverlayGhost"); + CPanelAnimationVar(vgui::HFont, m_hSmallWeaponsFont, "SmallWeaponsFont", "NHudSpectatorOverlaySmallWeapons"); + CPanelAnimationVarAliasType(int, m_iDeadTexture, "DeadTexture", "vgui/hud/kill_kill", "textureid"); SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; int m_iCardsSize = 0; From 70e32c1e54c9e2e2e379b8c9054fc935a8d9239c Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:54:09 +0100 Subject: [PATCH 11/17] fixup avatar scoreboard updating --- .../client/neo/ui/neo_hud_spectator_overlay.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 8b5a7d243d..79e9386f85 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -182,18 +182,20 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() { CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; const int iMapIdx = mapAvatars.Find(steamId); + bool bAvatarNeedUpdate = true; if (iMapIdx != mapAvatars.InvalidIndex()) { const MapAvatarValue &avatar = mapAvatars[iMapIdx]; if (avatar.i184Idx >= 0) { pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + bAvatarNeedUpdate = false; } - else if (avatar.i64Idx >= 0) + if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i64Idx >= 0) { pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); } - else if (avatar.i32Idx >= 0) + if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i32Idx >= 0) { pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); } @@ -201,18 +203,18 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() if (avatar.i184DeadIdx >= 0) { pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184DeadIdx)); + bAvatarNeedUpdate = false; } - else if (avatar.i64DeadIdx >= 0) + if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i64DeadIdx >= 0) { pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64DeadIdx)); } - else if (avatar.i32DeadIdx >= 0) + if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i32DeadIdx >= 0) { pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32DeadIdx)); } } - - bScoreboardNeedUpdate = bScoreboardNeedUpdate || !pCard->pAvatarAlive || !pCard->pAvatarDead; + bScoreboardNeedUpdate = bScoreboardNeedUpdate || bAvatarNeedUpdate; } } From 009eda682cec0143102e5b6fd7f9a264632e7a9c Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:20:07 +0100 Subject: [PATCH 12/17] Rework dead avatar Now all CAvatarImage will also generate a dead texture as well as normal one at the same time rather than requiring a separate var + fetch. So one fetch for two avatar textures now. Also apply dead texture in scoreboard. --- .../client/neo/ui/neo_hud_killer_info.cpp | 1 + .../neo/ui/neo_hud_spectator_overlay.cpp | 39 +++------ .../client/neo/ui/neo_hud_spectator_overlay.h | 3 +- src/game/client/neo/ui/neo_scoreboard.cpp | 30 +------ src/game/client/neo/ui/neo_scoreboard.h | 3 - src/game/client/vgui_avatarimage.cpp | 87 +++++++++++-------- src/game/client/vgui_avatarimage.h | 29 ++++--- 7 files changed, 86 insertions(+), 106 deletions(-) diff --git a/src/game/client/neo/ui/neo_hud_killer_info.cpp b/src/game/client/neo/ui/neo_hud_killer_info.cpp index cf4191eaa2..81d153385d 100644 --- a/src/game/client/neo/ui/neo_hud_killer_info.cpp +++ b/src/game/client/neo/ui/neo_hud_killer_info.cpp @@ -175,6 +175,7 @@ void CNEOHud_KillerInfo::DrawNeoHudElement() if (pAvatarImg) { + pAvatarImg->m_bDeadAvatar = false; pAvatarImg->SetPos(rect.x0 + iMargin, rect.y0 + iMargin); pAvatarImg->SetSize(iAvatarWT, iAvatarWT); pAvatarImg->Paint(); 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 79e9386f85..dd9e63fc53 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -173,14 +173,13 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() ++m_iTeamPlayersCount[iTeam]; // Reuse the avatar image already generated by the scoreboard - pCard->pAvatarAlive = nullptr; - pCard->pAvatarDead = nullptr; + pCard->pAvatar = nullptr; if (g_pNeoScoreBoard && g_pNeoScoreBoard->m_pImageList && CNEOScoreBoard::ShowAvatars()) { const CSteamID steamId = GetSteamIDForPlayerIndex(i); if (steamId.IsValid()) { - CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; + const CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; const int iMapIdx = mapAvatars.Find(steamId); bool bAvatarNeedUpdate = true; if (iMapIdx != mapAvatars.InvalidIndex()) @@ -188,30 +187,16 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() const MapAvatarValue &avatar = mapAvatars[iMapIdx]; if (avatar.i184Idx >= 0) { - pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); bAvatarNeedUpdate = false; } - if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i64Idx >= 0) + if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i64Idx >= 0) { - pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); } - if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i32Idx >= 0) + if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i32Idx >= 0) { - pCard->pAvatarAlive = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); - } - - if (avatar.i184DeadIdx >= 0) - { - pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184DeadIdx)); - bAvatarNeedUpdate = false; - } - if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i64DeadIdx >= 0) - { - pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64DeadIdx)); - } - if ((!pCard->pAvatarAlive || !pCard->pAvatarAlive->IsValid()) && avatar.i32DeadIdx >= 0) - { - pCard->pAvatarDead = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32DeadIdx)); + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); } } bScoreboardNeedUpdate = bScoreboardNeedUpdate || bAvatarNeedUpdate; @@ -368,12 +353,12 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawFilledRect(iAvatarX + 1, iAvatarY + 1, iAvatarX + iAvatarSize - 1, iAvatarY + iAvatarSize - 1); // Draw the actual Steam avatar (borrowed from the scoreboard's avatar list) - CAvatarImage *pAvatar = card.bAlive ? card.pAvatarAlive : card.pAvatarDead; - if (pAvatar && pAvatar->IsValid()) + if (card.pAvatar && card.pAvatar->IsValid()) { - pAvatar->SetPos(iAvatarX, iAvatarY); - pAvatar->SetSize(iAvatarSize, iAvatarSize); - pAvatar->Paint(); + card.pAvatar->m_bDeadAvatar = !card.bAlive; + card.pAvatar->SetPos(iAvatarX, iAvatarY); + card.pAvatar->SetSize(iAvatarSize, iAvatarSize); + card.pAvatar->Paint(); } else if (card.wszPlayerName[0]) { 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 bdab029175..4f6d5b86f9 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -24,8 +24,7 @@ struct SpectatorPlayerCard int iHP = 0; int iRoundKills = 0; bool bAlive = false; - CAvatarImage *pAvatarAlive = nullptr; - CAvatarImage *pAvatarDead = nullptr; + CAvatarImage *pAvatar = nullptr; // Used for animations/fading float flLastAttackTime = 0.0f; float flLastAliveTime = 0.0f; diff --git a/src/game/client/neo/ui/neo_scoreboard.cpp b/src/game/client/neo/ui/neo_scoreboard.cpp index 12334766be..dcf4899be7 100644 --- a/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/src/game/client/neo/ui/neo_scoreboard.cpp @@ -498,42 +498,22 @@ void CNEOScoreBoard::Update() int iMapIndex = m_mapAvatarsToImageList.Find(pPlayerInfo->steamID); if (iMapIndex == m_mapAvatarsToImageList.InvalidIndex()) { - // NEO TODO (nullsystem): Improve the way it fetches images, generate - // both non-altered and dead from one request auto *pImage32 = new CAvatarImage; auto *pImage64 = new CAvatarImage; auto *pImage184 = new CAvatarImage; - auto *pImageDead32 = new CAvatarImage; - auto *pImageDead64 = new CAvatarImage; - auto *pImageDead184 = new CAvatarImage; - - pImageDead32->m_bDeadAvatar = true; - pImageDead64->m_bDeadAvatar = true; - pImageDead184->m_bDeadAvatar = true; - pImage32->SetAvatarSize(32, 32); pImage64->SetAvatarSize(64, 64); pImage184->SetAvatarSize(184, 184); - pImageDead32->SetAvatarSize(32, 32); - pImageDead64->SetAvatarSize(64, 64); - pImageDead184->SetAvatarSize(184, 184); - pImage32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); pImage64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); pImage184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); - pImageDead32->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize32x32); - pImageDead64->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize64x64); - pImageDead184->SetAvatarSteamID(pPlayerInfo->steamID, k_EAvatarSize184x184); pPlayerInfo->avatar = { .i32Idx = m_pImageList->AddImage(pImage32), - .i32DeadIdx = m_pImageList->AddImage(pImageDead32), .i64Idx = m_pImageList->AddImage(pImage64), - .i64DeadIdx = m_pImageList->AddImage(pImageDead64), .i184Idx = m_pImageList->AddImage(pImage184), - .i184DeadIdx = m_pImageList->AddImage(pImageDead184), }; m_mapAvatarsToImageList.Insert(pPlayerInfo->steamID, pPlayerInfo->avatar); @@ -548,9 +528,6 @@ void CNEOScoreBoard::Update() pPlayerInfo->avatar.i32Idx = -1; pPlayerInfo->avatar.i64Idx = -1; pPlayerInfo->avatar.i184Idx = -1; - pPlayerInfo->avatar.i32DeadIdx = -1; - pPlayerInfo->avatar.i64DeadIdx = -1; - pPlayerInfo->avatar.i184DeadIdx = -1; } ++m_iTotalPlayers; @@ -1007,6 +984,7 @@ void CNEOScoreBoard::OnMainLoop(const NeoUI::Mode eMode) if (pAvatarImg) { + pAvatarImg->m_bDeadAvatar = pPlayerInfo->bDead; pAvatarImg->SetPos(m_uiCtx.rWidgetArea.x0, m_uiCtx.rWidgetArea.y0); pAvatarImg->SetSize(m_uiCtx.irWidgetTall, m_uiCtx.irWidgetTall); pAvatarImg->Paint(); @@ -1035,10 +1013,9 @@ void CNEOScoreBoard::OnMainLoop(const NeoUI::Mode eMode) } if (iCurTeam >= FIRST_GAME_TEAM) { - // Darken the avatar if dead or not ready + // Darken the avatar if not ready if (pAvatarImg - && (pPlayerInfo->bDead - || (bShowReadyUp && false == pPlayerInfo->bReady))) + && (bShowReadyUp && false == pPlayerInfo->bReady)) { vgui::surface()->DrawSetColor(0, 0, 0, 200); vgui::surface()->DrawFilledRect( @@ -1153,6 +1130,7 @@ void CNEOScoreBoard::OnMainLoop(const NeoUI::Mode eMode) } if (pAvatarImg) { + pAvatarImg->m_bDeadAvatar = false; pAvatarImg->SetPos(m_uiCtx.dPanel.x + iAvatarOffset, m_uiCtx.dPanel.y + iAvatarOffset); pAvatarImg->SetSize(iAvatarWT, iAvatarWT); diff --git a/src/game/client/neo/ui/neo_scoreboard.h b/src/game/client/neo/ui/neo_scoreboard.h index 350fa2b1c8..5d96ed3180 100644 --- a/src/game/client/neo/ui/neo_scoreboard.h +++ b/src/game/client/neo/ui/neo_scoreboard.h @@ -21,11 +21,8 @@ enum ENeoScoreBoardPadding struct MapAvatarValue { int i32Idx; - int i32DeadIdx; int i64Idx; - int i64DeadIdx; int i184Idx; - int i184DeadIdx; }; struct CNEOScoreBoardPlayer diff --git a/src/game/client/vgui_avatarimage.cpp b/src/game/client/vgui_avatarimage.cpp index 2061df81d1..edae32e1cc 100644 --- a/src/game/client/vgui_avatarimage.cpp +++ b/src/game/client/vgui_avatarimage.cpp @@ -21,7 +21,11 @@ DECLARE_BUILD_FACTORY( CAvatarImagePanel ); #pragma warning(disable : 4355) // warning C4355: 'this': used in base member initializer list #endif +#ifdef NEO +CUtlMap< AvatarImagePair_t, AvatarImagePairValue_t> CAvatarImage::s_AvatarImageCache; +#else CUtlMap< AvatarImagePair_t, int> CAvatarImage::s_AvatarImageCache; // cache of steam id's to textureids to use for images +#endif // NEO bool CAvatarImage::m_sbInitializedAvatarCache = false; //----------------------------------------------------------------------------- @@ -55,6 +59,9 @@ CAvatarImage::CAvatarImage( void ) // [menglish] Default icon for avatar icons if there is no avatar icon for the player m_iTextureID = -1; +#ifdef NEO + m_iTextureDeadID = -1; +#endif // NEO // set up friend icon m_pFriendIcon = gHUD.GetIcon( "ico_friend_indicator_avatar" ); @@ -231,11 +238,7 @@ void CAvatarImage::UpdateFriendStatus( void ) //----------------------------------------------------------------------------- void CAvatarImage::InitFromRGBA( int iAvatar, const byte *rgba, int width, int height ) { -#ifdef NEO - int iTexIndex = s_AvatarImageCache.Find( AvatarImagePair_t( m_SteamID, iAvatar, m_bDeadAvatar ) ); -#else int iTexIndex = s_AvatarImageCache.Find( AvatarImagePair_t( m_SteamID, iAvatar ) ); -#endif if ( iTexIndex == s_AvatarImageCache.InvalidIndex() ) { #ifdef NEO @@ -244,39 +247,43 @@ void CAvatarImage::InitFromRGBA( int iAvatar, const byte *rgba, int width, int h byte *rgbDest = (byte*)stackalloc( destBufferSize ); if ( steamapicontext->SteamUtils()->GetImageRGBA( iAvatar, rgbDest, destBufferSize ) ) { - if (m_bDeadAvatar) + // Create normal avatar from RGBA without edits + m_iTextureID = vgui::surface()->CreateNewTextureID( true ); + g_pMatSystemSurface->DrawSetTextureRGBAEx2( m_iTextureID, rgbDest, width, height, IMAGE_FORMAT_RGBA8888, true ); + + // Create dead avatar from RGBA with redness edits + const float contrast = -64.0f; + const float factor = (259.0f * (contrast + 255.0f)) / (255.0f * (259.0f - contrast)); + for (uint32 offset = 0; offset < (width * height * 4); offset += 4) { - const float contrast = -64.0f; - const float factor = (259.0f * (contrast + 255.0f)) / (255.0f * (259.0f - contrast)); - for (uint32 offset = 0; offset < (width * height * 4); offset += 4) - { - int r = (rgbDest + offset)[0]; - int g = (rgbDest + offset)[1]; - int b = (rgbDest + offset)[2]; - - // Bump brightness - r = Clamp((int)(((r / 255.0f) * 1.25f) * 255.0f), 0, 255); - g = Clamp((int)(((g / 255.0f) * 1.25f) * 255.0f), 0, 255); - b = Clamp((int)(((b / 255.0f) * 1.25f) * 255.0f), 0, 255); - - // Bump down contrast - r = Clamp((int)(factor * (r - 128) + 128), 0, 255); - g = Clamp((int)(factor * (g - 128) + 128), 0, 255); - b = Clamp((int)(factor * (b - 128) + 128), 0, 255); - - // Convert to grayscale - Luminosity, then only for red - const float flNGray = ((0.3f * (r / 255.0f)) + (0.59f * (g / 255.0f)) + (0.11f * (b / 255.0f))); - const int iGray = Clamp((int)(255.0f * flNGray), 0, 255); - (rgbDest + offset)[0] = iGray; - (rgbDest + offset)[1] = 0; - (rgbDest + offset)[2] = 0; - } + int r = (rgbDest + offset)[0]; + int g = (rgbDest + offset)[1]; + int b = (rgbDest + offset)[2]; + + // Bump brightness + r = Clamp((int)(((r / 255.0f) * 1.25f) * 255.0f), 0, 255); + g = Clamp((int)(((g / 255.0f) * 1.25f) * 255.0f), 0, 255); + b = Clamp((int)(((b / 255.0f) * 1.25f) * 255.0f), 0, 255); + + // Bump down contrast + r = Clamp((int)(factor * (r - 128) + 128), 0, 255); + g = Clamp((int)(factor * (g - 128) + 128), 0, 255); + b = Clamp((int)(factor * (b - 128) + 128), 0, 255); + + // Convert to grayscale - Luminosity, then only for red + const float flNGray = ((0.3f * (r / 255.0f)) + (0.59f * (g / 255.0f)) + (0.11f * (b / 255.0f))); + const int iGray = Clamp((int)(255.0f * flNGray), 0, 255); + (rgbDest + offset)[0] = iGray; + (rgbDest + offset)[1] = 0; + (rgbDest + offset)[2] = 0; } + m_iTextureDeadID = vgui::surface()->CreateNewTextureID( true ); + g_pMatSystemSurface->DrawSetTextureRGBAEx2( m_iTextureDeadID, rgbDest, width, height, IMAGE_FORMAT_RGBA8888, true ); - m_iTextureID = vgui::surface()->CreateNewTextureID( true ); - g_pMatSystemSurface->DrawSetTextureRGBAEx2( m_iTextureID, rgbDest, width, height, IMAGE_FORMAT_RGBA8888, true ); - iTexIndex = s_AvatarImageCache.Insert( AvatarImagePair_t( m_SteamID, iAvatar, m_bDeadAvatar ) ); - s_AvatarImageCache[ iTexIndex ] = m_iTextureID; + // Add textures to global cache + iTexIndex = s_AvatarImageCache.Insert( AvatarImagePair_t( m_SteamID, iAvatar ) ); + s_AvatarImageCache[ iTexIndex ].normal = m_iTextureID; + s_AvatarImageCache[ iTexIndex ].dead = m_iTextureDeadID; } stackfree( rgbDest ); #else @@ -286,8 +293,16 @@ void CAvatarImage::InitFromRGBA( int iAvatar, const byte *rgba, int width, int h s_AvatarImageCache[ iTexIndex ] = m_iTextureID; #endif // NEO } +#ifdef NEO + else + { + m_iTextureID = s_AvatarImageCache[ iTexIndex ].normal; + m_iTextureDeadID = s_AvatarImageCache[ iTexIndex ].dead; + } +#else else m_iTextureID = s_AvatarImageCache[ iTexIndex ]; +#endif // NEO m_bValid = true; } @@ -320,7 +335,11 @@ void CAvatarImage::Paint( void ) if ( m_bValid ) { +#ifdef NEO + vgui::surface()->DrawSetTexture( m_bDeadAvatar ? m_iTextureDeadID : m_iTextureID ); +#else vgui::surface()->DrawSetTexture( m_iTextureID ); +#endif // NEO vgui::surface()->DrawSetColor( m_Color ); vgui::surface()->DrawTexturedRect(posX, posY, posX + m_avatarWide, posY + m_avatarTall); } diff --git a/src/game/client/vgui_avatarimage.h b/src/game/client/vgui_avatarimage.h index f811b5a398..89f1bffbeb 100644 --- a/src/game/client/vgui_avatarimage.h +++ b/src/game/client/vgui_avatarimage.h @@ -34,31 +34,26 @@ // be retrieved. //============================================================================= +#ifdef NEO +struct AvatarImagePairValue_t +{ + int normal; + int dead; +}; +#endif // NEO + struct AvatarImagePair_t { AvatarImagePair_t() { m_iAvatar = 0; } -#ifdef NEO - AvatarImagePair_t( CSteamID steamID, int av, bool dead ) { m_SteamID = steamID; m_iAvatar = av; m_bDeadAvatar = dead; } -#else AvatarImagePair_t( CSteamID steamID, int av ) { m_SteamID = steamID; m_iAvatar = av; } -#endif bool operator<( const AvatarImagePair_t &rhs ) const { -#ifdef NEO - return m_SteamID.ConvertToUint64() < rhs.m_SteamID.ConvertToUint64() || - ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar < rhs.m_iAvatar ) || - ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar == rhs.m_iAvatar && m_bDeadAvatar < rhs.m_bDeadAvatar ); -#else return m_SteamID.ConvertToUint64() < rhs.m_SteamID.ConvertToUint64() || ( m_SteamID.ConvertToUint64() == rhs.m_SteamID.ConvertToUint64() && m_iAvatar < rhs.m_iAvatar ); -#endif } CSteamID m_SteamID; int m_iAvatar; -#ifdef NEO - bool m_bDeadAvatar = false; -#endif }; //----------------------------------------------------------------------------- @@ -133,7 +128,6 @@ class CAvatarImage : public vgui::IImage int GetAvatarWide() { return m_avatarWide; } int GetAvatarTall() { return m_avatarTall; } #ifdef NEO - int getTextureID() { return m_iTextureID; } bool m_bDeadAvatar = false; #endif @@ -167,6 +161,9 @@ class CAvatarImage : public vgui::IImage Color m_Color; int m_iTextureID; +#ifdef NEO + int m_iTextureDeadID; +#endif // NEO int m_nX, m_nY; int m_wide, m_tall; int m_avatarWide, m_avatarTall; @@ -194,7 +191,11 @@ class CAvatarImage : public vgui::IImage // HPE_END //============================================================================= +#ifdef NEO + static CUtlMap< AvatarImagePair_t, AvatarImagePairValue_t > s_AvatarImageCache; +#else static CUtlMap< AvatarImagePair_t, int > s_AvatarImageCache; +#endif static bool m_sbInitializedAvatarCache; CCallback m_sPersonaStateChangedCallback; From 8c4561e8848de30d5ee22f5c3b3b1e994c560124 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:47:44 +0100 Subject: [PATCH 13/17] fix showing dead avatar for spec --- src/game/client/neo/ui/neo_scoreboard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/neo/ui/neo_scoreboard.cpp b/src/game/client/neo/ui/neo_scoreboard.cpp index dcf4899be7..ce1baf2d59 100644 --- a/src/game/client/neo/ui/neo_scoreboard.cpp +++ b/src/game/client/neo/ui/neo_scoreboard.cpp @@ -984,7 +984,7 @@ void CNEOScoreBoard::OnMainLoop(const NeoUI::Mode eMode) if (pAvatarImg) { - pAvatarImg->m_bDeadAvatar = pPlayerInfo->bDead; + pAvatarImg->m_bDeadAvatar = pPlayerInfo->bDead && bIsPlaying; pAvatarImg->SetPos(m_uiCtx.rWidgetArea.x0, m_uiCtx.rWidgetArea.y0); pAvatarImg->SetSize(m_uiCtx.irWidgetTall, m_uiCtx.irWidgetTall); pAvatarImg->Paint(); From d4fd2e87f80329807be8d9454376a3bf203369d5 Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:26:52 +0100 Subject: [PATCH 14/17] change how avatar bg draws --- src/game/client/neo/ui/neo_hud_spectator_overlay.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 dd9e63fc53..2e72d724ad 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -347,11 +347,6 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, const int iAvatarX = bIsLeftSide ? x : x + wide - iAvatarSize; const int iAvatarY = y; - // Draw avatar background - vgui::surface()->DrawSetColor(OVERLAY_THEME.cardBg); - vgui::surface()->DrawOutlinedRect(iAvatarX, iAvatarY, iAvatarX + iAvatarSize, iAvatarY + iAvatarSize); - vgui::surface()->DrawFilledRect(iAvatarX + 1, iAvatarY + 1, iAvatarX + iAvatarSize - 1, iAvatarY + iAvatarSize - 1); - // Draw the actual Steam avatar (borrowed from the scoreboard's avatar list) if (card.pAvatar && card.pAvatar->IsValid()) { @@ -362,6 +357,9 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, } else if (card.wszPlayerName[0]) { + vgui::surface()->DrawSetColor(OVERLAY_THEME.cardBg); + vgui::surface()->DrawFilledRect(iAvatarX, iAvatarY, iAvatarX + iAvatarSize, iAvatarY + iAvatarSize); + // Fallback when no avatar: first character of the player name // Usually just bots though wchar_t wszInitial[2] = { card.wszPlayerName[0], L'\0' }; From be0a98fc8b8e3474e16bb38e100ebc6c98dc34dd Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:32:28 +0100 Subject: [PATCH 15/17] Added in overlay bindings for spec binds Similar to top hud spec binds but for overlay. Also the players order in card array now changed to make sure it's Jinrai team first always. --- game/neo/cfg/exampleOverlayConfig.cfg | 29 ++ .../neo/ui/neo_hud_spectator_overlay.cpp | 378 ++++++++++++------ .../client/neo/ui/neo_hud_spectator_overlay.h | 7 +- 3 files changed, 297 insertions(+), 117 deletions(-) create mode 100644 game/neo/cfg/exampleOverlayConfig.cfg diff --git a/game/neo/cfg/exampleOverlayConfig.cfg b/game/neo/cfg/exampleOverlayConfig.cfg new file mode 100644 index 0000000000..8e79aec63b --- /dev/null +++ b/game/neo/cfg/exampleOverlayConfig.cfg @@ -0,0 +1,29 @@ +// Enables overlay and bind keys relative to overlay's HUD +cl_neo_hud_spectator_overlay_enabled 1 + +bind 1 "spec_player_by_overlay_position 0" +bind 2 "spec_player_by_overlay_position 1" +bind 3 "spec_player_by_overlay_position 2" +bind 4 "spec_player_by_overlay_position 3" +bind 5 "spec_player_by_overlay_position 4" +bind 6 "spec_player_by_overlay_position 5" +bind 7 "spec_player_by_overlay_position 6" +bind 8 "spec_player_by_overlay_position 7" +bind 9 "spec_player_by_overlay_position 8" +bind 0 "spec_player_by_overlay_position 9" + +bind mwheelup select_next_alive_player_in_overlay +bind mwheeldown select_previous_alive_player_in_overlay +// Ideally these replace spec_next and spec_previous +bind mouse2 spec_next_entity_in_overlay +bind mouse1 spec_previous_entity_in_overlay + +bind mouse5 spec_player_under_mouse +bind mouse4 spec_fastest_player + +bind i spec_last_event +bind o spec_last_hurt +bind p spec_last_shooter +bind j spec_last_attacker +bind k spec_last_killer +bind l spec_last_ghoster 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 2e72d724ad..ab50e42d46 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -9,6 +9,8 @@ #include "neo_scoreboard.h" #include "neo_weapon_loadout.h" #include "neo_hud_deathnotice.h" +#include "neo_misc.h" +#include "hltvcamera.h" #include #include "iclientmode.h" @@ -155,155 +157,156 @@ void CNEOHud_SpectatorOverlay::UpdateStateForNeoHudElementDraw() return; } - for (int i = 1; i <= gpGlobals->maxClients; ++i) + for (const auto iPutTeam : {TEAM_JINRAI, TEAM_NSF}) { - if (i == pLocalPlayer->entindex() || !g_PR->IsValid(i) || g_PR->IsHLTV(i)) + for (int i = 1; i <= gpGlobals->maxClients; ++i) { - continue; - } + if (i == pLocalPlayer->entindex() || !g_PR->IsValid(i) || g_PR->IsHLTV(i)) + { + continue; + } - const int iTeam = g_PR->GetTeam(i); - if (false == IN_BETWEEN_EQ(FIRST_GAME_TEAM, iTeam, TEAM__TOTAL - 1) - || (m_iCardsSize >= MAX_PLAYERS_ARRAY_SAFE)) - { - continue; - } + const int iTeam = g_PR->GetTeam(i); + if (iTeam != iPutTeam || (m_iCardsSize >= MAX_PLAYERS_ARRAY_SAFE)) + { + continue; + } - SpectatorPlayerCard *pCard = &m_cards[m_iCardsSize++]; - ++m_iTeamPlayersCount[iTeam]; + SpectatorPlayerCard *pCard = &m_cards[m_iCardsSize++]; + ++m_iTeamPlayersCount[iTeam]; - // Reuse the avatar image already generated by the scoreboard - pCard->pAvatar = nullptr; - if (g_pNeoScoreBoard && g_pNeoScoreBoard->m_pImageList && CNEOScoreBoard::ShowAvatars()) - { - const CSteamID steamId = GetSteamIDForPlayerIndex(i); - if (steamId.IsValid()) + // Reuse the avatar image already generated by the scoreboard + pCard->pAvatar = nullptr; + if (g_pNeoScoreBoard && g_pNeoScoreBoard->m_pImageList && CNEOScoreBoard::ShowAvatars()) { - const CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; - const int iMapIdx = mapAvatars.Find(steamId); - bool bAvatarNeedUpdate = true; - if (iMapIdx != mapAvatars.InvalidIndex()) + const CSteamID steamId = GetSteamIDForPlayerIndex(i); + if (steamId.IsValid()) { - const MapAvatarValue &avatar = mapAvatars[iMapIdx]; - if (avatar.i184Idx >= 0) + const CUtlMap &mapAvatars = g_pNeoScoreBoard->m_mapAvatarsToImageList; + const int iMapIdx = mapAvatars.Find(steamId); + bool bAvatarNeedUpdate = true; + if (iMapIdx != mapAvatars.InvalidIndex()) { - pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); - bAvatarNeedUpdate = false; - } - if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i64Idx >= 0) - { - pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); - } - if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i32Idx >= 0) - { - pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); + const MapAvatarValue &avatar = mapAvatars[iMapIdx]; + if (avatar.i184Idx >= 0) + { + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i184Idx)); + bAvatarNeedUpdate = false; + } if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i64Idx >= 0) + { + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i64Idx)); + } + if ((!pCard->pAvatar || !pCard->pAvatar->IsValid()) && avatar.i32Idx >= 0) + { + pCard->pAvatar = static_cast(g_pNeoScoreBoard->m_pImageList->GetImage(avatar.i32Idx)); + } } + bScoreboardNeedUpdate = bScoreboardNeedUpdate || bAvatarNeedUpdate; } - bScoreboardNeedUpdate = bScoreboardNeedUpdate || bAvatarNeedUpdate; } - } - - Q_UTF8ToUnicode(g_PR->GetPlayerName(i), pCard->wszPlayerName, sizeof(pCard->wszPlayerName)); - C_NEO_Player *pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + Q_UTF8ToUnicode(g_PR->GetPlayerName(i), pCard->wszPlayerName, sizeof(pCard->wszPlayerName)); - // XP and class from player resource - pCard->iTeam = iTeam; - pCard->iXP = g_PR->GetXP(i); - pCard->iDeath = g_PR->GetDeaths(i); - pCard->iClass = g_PR->GetClass(i); + C_NEO_Player *pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); - pCard->iRoundKills = 0; - if (pNeoPlayer) - { - pCard->iUserID = pNeoPlayer->GetUserID(); - pCard->iEntIndex = pNeoPlayer->entindex(); + // XP and class from player resource + pCard->iTeam = iTeam; + pCard->iXP = g_PR->GetXP(i); + pCard->iDeath = g_PR->GetDeaths(i); + pCard->iClass = g_PR->GetClass(i); - for (int j = 0; j < gRoundKillerUserIDsSize; ++j) + pCard->iRoundKills = 0; + if (pNeoPlayer) { - pCard->iRoundKills += (gRoundKillerUserIDs[j] == pCard->iUserID); + pCard->iUserID = pNeoPlayer->GetUserID(); + pCard->iEntIndex = pNeoPlayer->entindex(); + + for (int j = 0; j < gRoundKillerUserIDsSize; ++j) + { + pCard->iRoundKills += (gRoundKillerUserIDs[j] == pCard->iUserID); + } } - } - // Health - NEO NOTE (nullsystem): GetHealth can be non-0 even when dead - pCard->bAlive = g_PR->IsAlive(i); - pCard->iHP = pCard->bAlive ? g_PR->GetHealth(i) : 0; + // Health - NEO NOTE (nullsystem): GetHealth can be non-0 even when dead + pCard->bAlive = g_PR->IsAlive(i); + pCard->iHP = pCard->bAlive ? g_PR->GetHealth(i) : 0; - // Deal with timings to can do animations/fading - SpectatorPlayerCard *pOldCard = nullptr; - if (pCard->iUserID > 0) - { - if (oldCards[m_iCardsSize - 1].iUserID == pCard->iUserID) - { - pOldCard = &oldCards[m_iCardsSize - 1]; - } - else + // Deal with timings to can do animations/fading + SpectatorPlayerCard *pOldCard = nullptr; + if (pCard->iUserID > 0) { - for (int j = 0; j < iOldCardsSize; ++j) + if (oldCards[m_iCardsSize - 1].iUserID == pCard->iUserID) { - if (oldCards[j].iUserID == pCard->iUserID) + pOldCard = &oldCards[m_iCardsSize - 1]; + } + else + { + for (int j = 0; j < iOldCardsSize; ++j) { - pOldCard = &oldCards[j]; - break; + if (oldCards[j].iUserID == pCard->iUserID) + { + pOldCard = &oldCards[j]; + break; + } } } } - } - if (pOldCard) - { - // Alive -> dead - pCard->flLastAliveTime = pOldCard->flLastAliveTime; - if (pOldCard->bAlive && !pCard->bAlive) + if (pOldCard) { - pCard->flLastAliveTime = gpGlobals->curtime; - } + // Alive -> dead + pCard->flLastAliveTime = pOldCard->flLastAliveTime; + if (pOldCard->bAlive && !pCard->bAlive) + { + pCard->flLastAliveTime = gpGlobals->curtime; + } - // HP reduction - pCard->flLastHPChangeTime = pOldCard->flLastHPChangeTime; - pCard->iLastHP = pOldCard->iLastHP; - if ((pOldCard->iHP > pCard->iHP) - && ((gpGlobals->curtime - pCard->flLastHPChangeTime) > FL_CHECK_HP_REDUCTION_DELTA_MAX)) - { - pCard->flLastHPChangeTime = gpGlobals->curtime; - pCard->iLastHP = pOldCard->iHP; + // HP reduction + pCard->flLastHPChangeTime = pOldCard->flLastHPChangeTime; + pCard->iLastHP = pOldCard->iLastHP; + if ((pOldCard->iHP > pCard->iHP) + && ((gpGlobals->curtime - pCard->flLastHPChangeTime) > FL_CHECK_HP_REDUCTION_DELTA_MAX)) + { + pCard->flLastHPChangeTime = gpGlobals->curtime; + pCard->iLastHP = pOldCard->iHP; + } } - } - // Weapon - pCard->flLastAttackTime = 0.0f; - pCard->iTotalWeaponIcons = 0; - pCard->iWeaponIdxPrimary = -1; - pCard->iWeaponIdxActive = -1; - if (pNeoPlayer) - { - const auto *pWepActive = static_cast(pNeoPlayer->GetActiveWeapon()); - if (pWepActive) + // Weapon + pCard->flLastAttackTime = 0.0f; + pCard->iTotalWeaponIcons = 0; + pCard->iWeaponIdxPrimary = -1; + pCard->iWeaponIdxActive = -1; + if (pNeoPlayer) { - pCard->flLastAttackTime = pWepActive->GetLastAttackTime(); - } + const auto *pWepActive = static_cast(pNeoPlayer->GetActiveWeapon()); + if (pWepActive) + { + pCard->flLastAttackTime = pWepActive->GetLastAttackTime(); + } - wchar_t wszWeaponIcon[2] = {}; - for (int j = 0; j < MAX_WEAPONS; ++j) - { - // NEO NOTE (nullsystem): Disallow knife, but keep ghost in - // array but ghost only for the outside indicator will be - // skipped over in the weapon icons list - if (const auto *pNeoWep = static_cast(pNeoPlayer->GetWeapon(j)); - pNeoWep && pNeoWep->WeaponIndex() != NEO_WIDX_KNIFE) + wchar_t wszWeaponIcon[2] = {}; + for (int j = 0; j < MAX_WEAPONS; ++j) { - if (pNeoWep->GetSlot() == 0) - { - pCard->iWeaponIdxPrimary = pCard->iTotalWeaponIcons; - } - if (pWepActive == pNeoWep) + // NEO NOTE (nullsystem): Disallow knife, but keep ghost in + // array but ghost only for the outside indicator will be + // skipped over in the weapon icons list + if (const auto *pNeoWep = static_cast(pNeoPlayer->GetWeapon(j)); + pNeoWep && pNeoWep->WeaponIndex() != NEO_WIDX_KNIFE) { - pCard->iWeaponIdxActive = pCard->iTotalWeaponIcons; + if (pNeoWep->GetSlot() == 0) + { + pCard->iWeaponIdxPrimary = pCard->iTotalWeaponIcons; + } + if (pWepActive == pNeoWep) + { + pCard->iWeaponIdxActive = pCard->iTotalWeaponIcons; + } + + // Icons are really only 1 character long + Q_UTF8ToUnicode(pNeoWep->GetDeathIcon(), + wszWeaponIcon, sizeof(wszWeaponIcon)); + pCard->wcaWeaponIcons[pCard->iTotalWeaponIcons++] = wszWeaponIcon[0]; } - - // Icons are really only 1 character long - Q_UTF8ToUnicode(pNeoWep->GetDeathIcon(), - wszWeaponIcon, sizeof(wszWeaponIcon)); - pCard->wcaWeaponIcons[pCard->iTotalWeaponIcons++] = wszWeaponIcon[0]; } } } @@ -772,3 +775,148 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() } } +int CNEOHud_SpectatorOverlay::CardIdxFromLocalObserverTarget() const +{ + const C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); + if (pNeoPlayer && pNeoPlayer->IsObserver()) + { + if (const C_BaseEntity *pTarget = pNeoPlayer->GetObserverTarget()) + { + const int iCmpEntIndex = pTarget->entindex(); + for (int i = 0; i < m_iCardsSize; ++i) + { + if (iCmpEntIndex == m_cards[i].iEntIndex) + { + return i; + } + } + } + } + return -1; +} + +static void SetPlayerTarget(const int iPlayerIndex) +{ + if (iPlayerIndex) + { + if (engine->IsHLTV()) + { + HLTVCamera()->SetPrimaryTarget(iPlayerIndex); + } + else + { + engine->ClientCmd(VarArgs("spec_player_entity_number %d", iPlayerIndex)); + } + } +} + +static const CNEOHud_SpectatorOverlay *CheckCommonSpecOverlayErrors(const char *pszFuncName) +{ + const auto *pOverlay = GET_NAMED_HUDELEMENT(CNEOHud_SpectatorOverlay, neo_spectator_overlay); + if (!pOverlay) + { + return nullptr; + } + + if (false == cl_neo_hud_spectator_overlay_enabled.GetBool()) + { + ConMsg("%s: Requires \"cl_neo_hud_spectator_overlay_enabled 1\"!\n", pszFuncName); + return nullptr; + } + + if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) + { + ConMsg("%s: HLTV Camera is PVS locked\n", pszFuncName); + return nullptr; + } + + return pOverlay; +} + +CON_COMMAND_F(spec_player_by_overlay_position, "Spectate player by position in the overlay hud, from first of Jinrai to last of NSF", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + static constexpr const char HELP_MSG[] = + "Usage: spec_player_by_overlay_position { player position in overlay hud, from first of Jinrai to last of NSF, 0 indexed }\n"; + + const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); + if (!pOverlay) return; + + if (args.ArgC() != 2) + { + ConMsg(HELP_MSG); + return; + } + + const int iSpecTargetIdx = atoi(args[1]); + if (iSpecTargetIdx < 0 || iSpecTargetIdx >= pOverlay->m_iCardsSize) + { + ConMsg(HELP_MSG); + return; + } + SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); +} + +CON_COMMAND_F(spec_next_entity_in_overlay, "Spectate the next valid player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); + if (!pOverlay) return; + + const int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); + if (iSpecTargetIdx >= 0) + { + SetPlayerTarget(pOverlay->m_cards[LoopAroundInArray(iSpecTargetIdx + 1, pOverlay->m_iCardsSize)].iEntIndex); + } +} + +CON_COMMAND_F(spec_previous_entity_in_overlay, "Spectate the previous valid player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); + if (!pOverlay) return; + + const int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); + if (iSpecTargetIdx >= 0) + { + SetPlayerTarget(pOverlay->m_cards[LoopAroundInArray(iSpecTargetIdx - 1, pOverlay->m_iCardsSize)].iEntIndex); + } +} + +CON_COMMAND_F(select_next_alive_player_in_overlay, "Select the next alive player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); + if (!pOverlay) return; + + int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); + if (iSpecTargetIdx >= 0) + { + for (int i = 0; i < pOverlay->m_iCardsSize; ++i) + { + iSpecTargetIdx = LoopAroundInArray(iSpecTargetIdx + 1, pOverlay->m_iCardsSize); + if (pOverlay->m_cards[iSpecTargetIdx].bAlive) + { + break; + } + } + SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); + } +} + +CON_COMMAND_F(select_previous_alive_player_in_overlay, "Select the previous alive player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); + if (!pOverlay) return; + + int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); + if (iSpecTargetIdx >= 0) + { + for (int i = 0; i < pOverlay->m_iCardsSize; ++i) + { + iSpecTargetIdx = LoopAroundInArray(iSpecTargetIdx - 1, pOverlay->m_iCardsSize); + if (pOverlay->m_cards[iSpecTargetIdx].bAlive) + { + break; + } + } + SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); + } +} + 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 4f6d5b86f9..483ecd6b68 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -50,6 +50,11 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement void ApplySchemeSettings(vgui::IScheme *pScheme) final; void Paint() final; + SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; + int m_iCardsSize = 0; + + int CardIdxFromLocalObserverTarget() const; + protected: void UpdateStateForNeoHudElementDraw() final; void DrawNeoHudElement() final; @@ -71,8 +76,6 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement CPanelAnimationVar(vgui::HFont, m_hSmallWeaponsFont, "SmallWeaponsFont", "NHudSpectatorOverlaySmallWeapons"); CPanelAnimationVarAliasType(int, m_iDeadTexture, "DeadTexture", "vgui/hud/kill_kill", "textureid"); - SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; - int m_iCardsSize = 0; int m_iTeamPlayersCount[TEAM__TOTAL] = {}; }; From f3aa76aecb9a925079e4e59a0dbdf0929f7f87cb Mon Sep 17 00:00:00 2001 From: nullsystem <15316579+nullsystem@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:13:04 +0100 Subject: [PATCH 16/17] Merged functionalities of both top and overlay hud binds together Now both share the same binds but when overlay enabled will use overlay to deal with binds and disable top hud, if overlay disabled the top hud deals with it. --- game/neo/cfg/exampleOverlayConfig.cfg | 29 --- src/game/client/CMakeLists.txt | 1 + .../client/neo/ui/neo_hud_round_state.cpp | 156 +------------- .../neo/ui/neo_hud_spectator_overlay.cpp | 181 +++++----------- .../client/neo/ui/neo_hud_spectator_overlay.h | 18 +- .../client/neo/ui/neo_spec_player_by_hud.cpp | 201 ++++++++++++++++++ 6 files changed, 282 insertions(+), 304 deletions(-) delete mode 100644 game/neo/cfg/exampleOverlayConfig.cfg create mode 100644 src/game/client/neo/ui/neo_spec_player_by_hud.cpp diff --git a/game/neo/cfg/exampleOverlayConfig.cfg b/game/neo/cfg/exampleOverlayConfig.cfg deleted file mode 100644 index 8e79aec63b..0000000000 --- a/game/neo/cfg/exampleOverlayConfig.cfg +++ /dev/null @@ -1,29 +0,0 @@ -// Enables overlay and bind keys relative to overlay's HUD -cl_neo_hud_spectator_overlay_enabled 1 - -bind 1 "spec_player_by_overlay_position 0" -bind 2 "spec_player_by_overlay_position 1" -bind 3 "spec_player_by_overlay_position 2" -bind 4 "spec_player_by_overlay_position 3" -bind 5 "spec_player_by_overlay_position 4" -bind 6 "spec_player_by_overlay_position 5" -bind 7 "spec_player_by_overlay_position 6" -bind 8 "spec_player_by_overlay_position 7" -bind 9 "spec_player_by_overlay_position 8" -bind 0 "spec_player_by_overlay_position 9" - -bind mwheelup select_next_alive_player_in_overlay -bind mwheeldown select_previous_alive_player_in_overlay -// Ideally these replace spec_next and spec_previous -bind mouse2 spec_next_entity_in_overlay -bind mouse1 spec_previous_entity_in_overlay - -bind mouse5 spec_player_under_mouse -bind mouse4 spec_fastest_player - -bind i spec_last_event -bind o spec_last_hurt -bind p spec_last_shooter -bind j spec_last_attacker -bind k spec_last_killer -bind l spec_last_ghoster diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt index 947d08280f..317cbb87a5 100644 --- a/src/game/client/CMakeLists.txt +++ b/src/game/client/CMakeLists.txt @@ -1673,6 +1673,7 @@ set(UNITY_SOURCE_NEO_UI neo/ui/neo_hud_worldpos_marker_generic.cpp neo/ui/neo_scoreboard.cpp neo/ui/neo_hud_context_hint.cpp + neo/ui/neo_spec_player_by_hud.cpp ) set_source_files_properties( 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 4aae1bed44..9cea014437 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -20,6 +20,7 @@ #include "c_playerresource.h" #include "vgui_avatarimage.h" #include "neo_scoreboard.h" +#include "neo_hud_spectator_overlay.h" #include "hltvcamera.h" @@ -558,6 +559,11 @@ void CNEOHud_RoundState::DrawNeoHudElement() if (!g_PR) return; + if (localPlayerSpecOrNoTeam && cl_neo_hud_spectator_overlay_enabled.GetBool()) + { + return; + } + if (NEORules()->IsTeamplay() && (!cl_neo_squad_hud_original.GetBool() || localPlayerSpecOrNoTeam)) { // Sort player list even if not drawing new hud so spectators can use commands for (int i = 0; i < MAX_PLAYERS; i++) @@ -1361,153 +1367,3 @@ int CNEOHud_RoundState::GetSelectedPlayerInHud() return -1; } -CON_COMMAND_F( spec_player_by_hud_position, "Spectate player by position in the top hud", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if ( args.ArgC() != 2 ) - { - ConMsg( "Usage: spec_player_by_hud_position { player position in top hud, 0 indexed }\n" ); - return; - } - - int positionInHud = atoi( args[1] ); - if (positionInHud < 0 || positionInHud > MAX_PLAYERS - 1) - { - ConMsg( "Usage: spec_player_by_hud_position { player position in top hud, 0 indexed }\n" ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - const int entityIndex = g_pNeoHudRoundState->GetEntityIndexAtPositionInHud(positionInHud, true); - if (entityIndex) - { - engine->IsHLTV() ? HLTVCamera()->SetPrimaryTarget(entityIndex) : engine->ClientCmd(VarArgs("spec_player_entity_number %d", entityIndex)); - } -} - -CON_COMMAND_F( spec_next_entity_in_hud, "Spectate next valid player to the right of the current spectate target", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - int spectateTargetMinusIndexedPositionInHud = 0; - C_BaseEntity *pSpectateTarget = pNeoPlayer->GetObserverTarget(); - if (pSpectateTarget) - { - spectateTargetMinusIndexedPositionInHud = g_pNeoHudRoundState->GetMinusIndexedPositionOfPlayerInHud(pSpectateTarget->entindex()); - } - - const int playerIndex = g_pNeoHudRoundState->GetEntityIndexAtPositionInHud(g_pNeoHudRoundState->GetNextAlivePlayerInHud(spectateTargetMinusIndexedPositionInHud, false)); - if (playerIndex) - { - engine->IsHLTV() ? HLTVCamera()->SetPrimaryTarget(playerIndex) : engine->ClientCmd(VarArgs("spec_player_entity_number %d", playerIndex)); - } -} - -CON_COMMAND_F( spec_previous_entity_in_hud, "Spectate next valid player to the left of the current spectate target", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - int spectateTargetMinusIndexedPositionInHud = 0; - C_BaseEntity *pSpectateTarget = pNeoPlayer->GetObserverTarget(); - if (pSpectateTarget) - { - spectateTargetMinusIndexedPositionInHud = g_pNeoHudRoundState->GetMinusIndexedPositionOfPlayerInHud(pSpectateTarget->entindex()); - } - - const int playerIndex = g_pNeoHudRoundState->GetEntityIndexAtPositionInHud(g_pNeoHudRoundState->GetNextAlivePlayerInHud(spectateTargetMinusIndexedPositionInHud, true)); - if (playerIndex) - { - engine->IsHLTV() ? HLTVCamera()->SetPrimaryTarget(playerIndex) : engine->ClientCmd(VarArgs("spec_player_entity_number %d", playerIndex)); - } -} - -CON_COMMAND_F( select_next_alive_player_in_hud, "Select the next alive player in the top hud", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: Selection is used to switch observer target in spectate_player_selected_in_hud, but HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - g_pNeoHudRoundState->SelectNextAlivePlayerInHud(); -} - -CON_COMMAND_F( select_previous_alive_player_in_hud, "Select the previous alive player in the top hud", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: Selection is used to switch observer target in spectate_player_selected_in_hud, but HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - g_pNeoHudRoundState->SelectPreviousAlivePlayerInHud(); -} - -CON_COMMAND_F( spectate_player_selected_in_hud, "Spectate entity selected in the top hud", FCVAR_CLIENTCMD_CAN_EXECUTE ) -{ - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg( "%s: HLTV Camera is PVS locked\n", __FUNCTION__ ); - return; - } - - if (!g_pNeoHudRoundState) - return; - - C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); - if ( !pNeoPlayer || !pNeoPlayer->IsObserver() ) - return; - - const int entityIndex = g_pNeoHudRoundState->GetSelectedPlayerInHud(); - if (entityIndex) - { - engine->IsHLTV() ? HLTVCamera()->SetPrimaryTarget(entityIndex) : engine->ClientCmd(VarArgs("spec_player_entity_number %d", entityIndex)); - } -} 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 ab50e42d46..0aabbe72ce 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.cpp @@ -10,7 +10,6 @@ #include "neo_weapon_loadout.h" #include "neo_hud_deathnotice.h" #include "neo_misc.h" -#include "hltvcamera.h" #include #include "iclientmode.h" @@ -45,9 +44,12 @@ static const struct Theme static constexpr const float FL_CHECK_HP_REDUCTION_DELTA_MAX = 1.5f; +CNEOHud_SpectatorOverlay *g_pNeoHudSpecOverlay; + CNEOHud_SpectatorOverlay::CNEOHud_SpectatorOverlay(const char* pElementName, vgui::Panel* parent) : CHudElement(pElementName), EditablePanel(parent, pElementName) { + g_pNeoHudSpecOverlay = this; SetAutoDelete(true); m_iHideHudElementNumber = NEO_HUD_ELEMENT_SPECTATOR_OVERLAY; @@ -69,6 +71,7 @@ CNEOHud_SpectatorOverlay::CNEOHud_SpectatorOverlay(const char* pElementName, vgu CNEOHud_SpectatorOverlay::~CNEOHud_SpectatorOverlay() { + g_pNeoHudSpecOverlay = nullptr; } static bool ShouldDrawHUD() @@ -94,6 +97,7 @@ void CNEOHud_SpectatorOverlay::ClearAll() m_iCardsSize = 0; V_memset(m_cards, 0, sizeof(m_cards)); V_memset(m_iTeamPlayersCount, 0, sizeof(m_iTeamPlayersCount)); + m_iEntIndexSelect = 0; } void CNEOHud_SpectatorOverlay::Init() @@ -683,6 +687,8 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawPrintText(WSZ_DNI_GHOST, 1); } + const int iMargin = flWideAs43 / 480.0f; + // Spectator current observing border const auto *pLocalPlayer = C_NEO_Player::GetLocalNEOPlayer(); if (card.iEntIndex == GetSpectatorTarget() @@ -691,8 +697,6 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, && (pLocalPlayer->GetObserverMode() == OBS_MODE_IN_EYE || pLocalPlayer->GetObserverMode() == OBS_MODE_CHASE)) { - const int iMargin = flWideAs43 / 480.0f; - vgui::surface()->DrawSetColor(COLOR_WHITE); vgui::surface()->DrawFilledRect(x, y - iMargin, x + wide, y); // top vgui::surface()->DrawFilledRect(x, y + tall, x + wide, y + tall + iMargin); // bottom @@ -700,6 +704,22 @@ void CNEOHud_SpectatorOverlay::DrawPlayerCard(const SpectatorPlayerCard &card, vgui::surface()->DrawFilledRect(x + wide, y - iMargin, x + wide + iMargin, y + tall + iMargin); // right } + // m_iEntIndexSelect indicator + if (card.iEntIndex == m_iEntIndexSelect) + { + vgui::surface()->DrawSetColor(COLOR_NEO_ORANGE); + if (bIsLeftSide) + { + // Left border, Jinrai + vgui::surface()->DrawFilledRect(x - iMargin, y - iMargin, x, y + tall + iMargin); + } + else + { + // Right border, NSF + vgui::surface()->DrawFilledRect(x + wide, y - iMargin, x + wide + iMargin, y + tall + iMargin); + } + } + // Weapon firing indicator bar - Jinrai right side, NSF left side static constexpr const float FL_LAST_ATTACK_DELTA_MAX = 0.5f; const float flLastAttackDelta = gpGlobals->curtime - card.flLastAttackTime; @@ -775,148 +795,61 @@ void CNEOHud_SpectatorOverlay::DrawNeoHudElement() } } -int CNEOHud_SpectatorOverlay::CardIdxFromLocalObserverTarget() const +int CNEOHud_SpectatorOverlay::SpecTargetNextEntIdx(const ESpecType eSpecType, EDirection eDirection) const { + int iSpecTargetIdx = -1; const C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); if (pNeoPlayer && pNeoPlayer->IsObserver()) { - if (const C_BaseEntity *pTarget = pNeoPlayer->GetObserverTarget()) + const C_BaseEntity *pTarget = pNeoPlayer->GetObserverTarget(); + const bool bHasSelEntIndex = (eSpecType == SPECTYPE_SELECT && m_iEntIndexSelect > 0); + if (pTarget || bHasSelEntIndex) { - const int iCmpEntIndex = pTarget->entindex(); + const int iCmpEntIndex = bHasSelEntIndex ? m_iEntIndexSelect : pTarget->entindex(); for (int i = 0; i < m_iCardsSize; ++i) { if (iCmpEntIndex == m_cards[i].iEntIndex) { - return i; + iSpecTargetIdx = i; + break; } } - } - } - return -1; -} - -static void SetPlayerTarget(const int iPlayerIndex) -{ - if (iPlayerIndex) - { - if (engine->IsHLTV()) - { - HLTVCamera()->SetPrimaryTarget(iPlayerIndex); - } - else - { - engine->ClientCmd(VarArgs("spec_player_entity_number %d", iPlayerIndex)); - } - } -} - -static const CNEOHud_SpectatorOverlay *CheckCommonSpecOverlayErrors(const char *pszFuncName) -{ - const auto *pOverlay = GET_NAMED_HUDELEMENT(CNEOHud_SpectatorOverlay, neo_spectator_overlay); - if (!pOverlay) - { - return nullptr; - } - - if (false == cl_neo_hud_spectator_overlay_enabled.GetBool()) - { - ConMsg("%s: Requires \"cl_neo_hud_spectator_overlay_enabled 1\"!\n", pszFuncName); - return nullptr; - } - - if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) - { - ConMsg("%s: HLTV Camera is PVS locked\n", pszFuncName); - return nullptr; - } - - return pOverlay; -} - -CON_COMMAND_F(spec_player_by_overlay_position, "Spectate player by position in the overlay hud, from first of Jinrai to last of NSF", FCVAR_CLIENTCMD_CAN_EXECUTE) -{ - static constexpr const char HELP_MSG[] = - "Usage: spec_player_by_overlay_position { player position in overlay hud, from first of Jinrai to last of NSF, 0 indexed }\n"; - - const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); - if (!pOverlay) return; - - if (args.ArgC() != 2) - { - ConMsg(HELP_MSG); - return; - } - - const int iSpecTargetIdx = atoi(args[1]); - if (iSpecTargetIdx < 0 || iSpecTargetIdx >= pOverlay->m_iCardsSize) - { - ConMsg(HELP_MSG); - return; - } - SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); -} - -CON_COMMAND_F(spec_next_entity_in_overlay, "Spectate the next valid player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) -{ - const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); - if (!pOverlay) return; - - const int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); - if (iSpecTargetIdx >= 0) - { - SetPlayerTarget(pOverlay->m_cards[LoopAroundInArray(iSpecTargetIdx + 1, pOverlay->m_iCardsSize)].iEntIndex); - } -} - -CON_COMMAND_F(spec_previous_entity_in_overlay, "Spectate the previous valid player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) -{ - const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); - if (!pOverlay) return; - - const int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); - if (iSpecTargetIdx >= 0) - { - SetPlayerTarget(pOverlay->m_cards[LoopAroundInArray(iSpecTargetIdx - 1, pOverlay->m_iCardsSize)].iEntIndex); - } -} - -CON_COMMAND_F(select_next_alive_player_in_overlay, "Select the next alive player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) -{ - const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); - if (!pOverlay) return; - - int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); - if (iSpecTargetIdx >= 0) - { - for (int i = 0; i < pOverlay->m_iCardsSize; ++i) - { - iSpecTargetIdx = LoopAroundInArray(iSpecTargetIdx + 1, pOverlay->m_iCardsSize); - if (pOverlay->m_cards[iSpecTargetIdx].bAlive) + // If m_iEntIndexSelect selected is now invalid + if (pTarget && bHasSelEntIndex && iSpecTargetIdx == -1) { - break; + const int iCmpEntIndex2 = pTarget->entindex(); + for (int i = 0; i < m_iCardsSize; ++i) + { + if (iCmpEntIndex2 == m_cards[i].iEntIndex) + { + iSpecTargetIdx = i; + break; + } + } } } - SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); + else if ((eSpecType == SPECTYPE_SELECT && m_iEntIndexSelect == 0) || (eSpecType == SPECTYPE_DIRECT)) + { + iSpecTargetIdx = 0; + eDirection = DIRECTION_NIL; + } } -} - -CON_COMMAND_F(select_previous_alive_player_in_overlay, "Select the previous alive player in the overlay hud", FCVAR_CLIENTCMD_CAN_EXECUTE) -{ - const CNEOHud_SpectatorOverlay *pOverlay = CheckCommonSpecOverlayErrors(__FUNCTION__); - if (!pOverlay) return; - - int iSpecTargetIdx = pOverlay->CardIdxFromLocalObserverTarget(); if (iSpecTargetIdx >= 0) { - for (int i = 0; i < pOverlay->m_iCardsSize; ++i) + if (eDirection != DIRECTION_NIL) { - iSpecTargetIdx = LoopAroundInArray(iSpecTargetIdx - 1, pOverlay->m_iCardsSize); - if (pOverlay->m_cards[iSpecTargetIdx].bAlive) + for (int i = 0; i < m_iCardsSize; ++i) { - break; + iSpecTargetIdx = LoopAroundInArray(iSpecTargetIdx + + ((eDirection == DIRECTION_PREV) ? -1 : +1), m_iCardsSize); + if (m_cards[iSpecTargetIdx].bAlive) + { + break; + } } } - SetPlayerTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); + return m_cards[iSpecTargetIdx].iEntIndex; } + return 0; } 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 483ecd6b68..1d60cfcff0 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -6,6 +6,8 @@ class CAvatarImage; +extern ConVar cl_neo_hud_spectator_overlay_enabled; + struct SpectatorPlayerCard { int iUserID = 0; @@ -53,7 +55,19 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement SpectatorPlayerCard m_cards[MAX_PLAYERS_ARRAY_SAFE] = {}; int m_iCardsSize = 0; - int CardIdxFromLocalObserverTarget() const; + enum ESpecType + { + SPECTYPE_DIRECT = 0, + SPECTYPE_SELECT, + }; + enum EDirection + { + DIRECTION_NIL = 0, + DIRECTION_NEXT, + DIRECTION_PREV, + }; + int SpecTargetNextEntIdx(const ESpecType eSpecType, EDirection eDirection) const; + int m_iEntIndexSelect = 0; protected: void UpdateStateForNeoHudElementDraw() final; @@ -79,3 +93,5 @@ class CNEOHud_SpectatorOverlay : public CNEOHud_ChildElement, public CHudElement int m_iTeamPlayersCount[TEAM__TOTAL] = {}; }; +extern CNEOHud_SpectatorOverlay *g_pNeoHudSpecOverlay; + diff --git a/src/game/client/neo/ui/neo_spec_player_by_hud.cpp b/src/game/client/neo/ui/neo_spec_player_by_hud.cpp new file mode 100644 index 0000000000..5983ab79a1 --- /dev/null +++ b/src/game/client/neo/ui/neo_spec_player_by_hud.cpp @@ -0,0 +1,201 @@ +#include "cbase.h" + +#include "neo_hud_spectator_overlay.h" +#include "neo_hud_round_state.h" +#include "hud_element_helper.h" +#include "hltvcamera.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static void SetPlayerEntityTarget(const int iEntIndex) +{ + if (iEntIndex > 0) + { + if (engine->IsHLTV()) + { + HLTVCamera()->SetPrimaryTarget(iEntIndex); + } + else + { + engine->ClientCmd(VarArgs("spec_player_entity_number %d", iEntIndex)); + } + } +} + +struct HUDCommonRet +{ + CNEOHud_SpectatorOverlay *pOverlay = nullptr; + CNEOHud_RoundState *pRoundState = nullptr; + C_NEO_Player *pNeoPlayer = nullptr; +}; + +static HUDCommonRet CheckCommonSpecOverlayErrors(const char *pszFuncName) +{ + HUDCommonRet ret = {}; + + const bool bUseOverlay = cl_neo_hud_spectator_overlay_enabled.GetBool(); + + auto *pOverlay = g_pNeoHudSpecOverlay; + auto *pRoundState = g_pNeoHudRoundState; + if ((bUseOverlay && !pOverlay) || (!bUseOverlay && !pRoundState)) + { + return ret; + } + + if (engine->IsHLTV() && HLTVCamera()->IsPVSLocked()) + { + ConMsg("%s: HLTV Camera is PVS locked\n", pszFuncName); + return ret; + } + + C_NEO_Player *pNeoPlayer = C_NEO_Player::GetLocalNEOPlayer(); + if (!pNeoPlayer || !pNeoPlayer->IsObserver()) + { + return ret; + } + + if (bUseOverlay) + { + ret.pOverlay = pOverlay; + } + else + { + ret.pRoundState = pRoundState; + } + ret.pNeoPlayer = pNeoPlayer; + return ret; +} + +CON_COMMAND_F(spec_player_by_hud_position, "Spectate player by position in the hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + static constexpr const char HELP_MSG_OL[] = + "Usage: spec_player_by_hud_position { player position in overlay hud, from first of Jinrai to last of NSF, 0 indexed }\n"; + static constexpr const char HELP_MSG_RS[] = + "Usage: spec_player_by_hud_position { player position in top hud, 0 indexed }\n"; + + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (args.ArgC() != 2) + { + ConMsg(pOverlay ? HELP_MSG_OL : HELP_MSG_RS); + return; + } + + if (pOverlay) + { + const int iSpecTargetIdx = atoi(args[1]); + if (iSpecTargetIdx < 0 || iSpecTargetIdx >= pOverlay->m_iCardsSize) + { + ConMsg(HELP_MSG_OL); + return; + } + SetPlayerEntityTarget(pOverlay->m_cards[iSpecTargetIdx].iEntIndex); + } + else if (pRoundState) + { + const int positionInHud = atoi(args[1]); + if (positionInHud < 0 || positionInHud > MAX_PLAYERS - 1) + { + ConMsg(HELP_MSG_RS); + return; + } + SetPlayerEntityTarget(g_pNeoHudRoundState->GetEntityIndexAtPositionInHud(positionInHud, true)); + } +} + +CON_COMMAND_F(spec_next_entity_in_hud, "Spectate the next valid player in the hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (pOverlay) + { + pOverlay->m_iEntIndexSelect = 0; + SetPlayerEntityTarget(pOverlay->SpecTargetNextEntIdx( + CNEOHud_SpectatorOverlay::SPECTYPE_DIRECT, + CNEOHud_SpectatorOverlay::DIRECTION_NEXT)); + } + else if (pRoundState) + { + C_BaseEntity *pSpectateTarget = pNeoPlayer->GetObserverTarget(); + const int iSpecTargetMinusIdxPos = (pSpectateTarget) + ? g_pNeoHudRoundState->GetMinusIndexedPositionOfPlayerInHud(pSpectateTarget->entindex()) + : 0; + SetPlayerEntityTarget( + g_pNeoHudRoundState->GetEntityIndexAtPositionInHud( + g_pNeoHudRoundState->GetNextAlivePlayerInHud( + iSpecTargetMinusIdxPos, false))); + } +} + +CON_COMMAND_F(spec_previous_entity_in_hud, "Spectate the previous valid player in the hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (pOverlay) + { + pOverlay->m_iEntIndexSelect = 0; + SetPlayerEntityTarget(pOverlay->SpecTargetNextEntIdx( + CNEOHud_SpectatorOverlay::SPECTYPE_DIRECT, + CNEOHud_SpectatorOverlay::DIRECTION_PREV)); + } + else if (pRoundState) + { + C_BaseEntity *pSpectateTarget = pNeoPlayer->GetObserverTarget(); + const int iSpecTargetMinusIdxPos = (pSpectateTarget) + ? pRoundState->GetMinusIndexedPositionOfPlayerInHud(pSpectateTarget->entindex()) + : 0; + SetPlayerEntityTarget( + pRoundState->GetEntityIndexAtPositionInHud( + pRoundState->GetNextAlivePlayerInHud( + iSpecTargetMinusIdxPos, true))); + } +} + +CON_COMMAND_F(select_next_alive_player_in_hud, "Select the next alive player in the hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (pOverlay) + { + pOverlay->m_iEntIndexSelect = pOverlay->SpecTargetNextEntIdx( + CNEOHud_SpectatorOverlay::SPECTYPE_SELECT, + CNEOHud_SpectatorOverlay::DIRECTION_NEXT); + } + else if (pRoundState) + { + pRoundState->SelectNextAlivePlayerInHud(); + } +} + +CON_COMMAND_F(select_previous_alive_player_in_hud, "Select the previous alive player in the hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (pOverlay) + { + pOverlay->m_iEntIndexSelect = pOverlay->SpecTargetNextEntIdx( + CNEOHud_SpectatorOverlay::SPECTYPE_SELECT, + CNEOHud_SpectatorOverlay::DIRECTION_PREV); + } + else if (pRoundState) + { + pRoundState->SelectPreviousAlivePlayerInHud(); + } +} + +CON_COMMAND_F(spectate_player_selected_in_hud, "Spectate entity selected in the top hud", FCVAR_CLIENTCMD_CAN_EXECUTE) +{ + auto [pOverlay, pRoundState, pNeoPlayer] = CheckCommonSpecOverlayErrors(__FUNCTION__); + + if (pOverlay) + { + SetPlayerEntityTarget(pOverlay->m_iEntIndexSelect); + pOverlay->m_iEntIndexSelect = 0; + } + else if (pRoundState) + { + SetPlayerEntityTarget(g_pNeoHudRoundState->GetSelectedPlayerInHud()); + } +} + From 43fc310951f6f99163e5ac488c044ca04d41118e 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 17/17] 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. --- src/game/client/neo/ui/neo_hud_round_state.h | 4 + .../neo/ui/neo_hud_spectator_overlay.cpp | 192 +++++++++++++++++- .../client/neo/ui/neo_hud_spectator_overlay.h | 1 + src/game/client/neo/ui/neo_ui.cpp | 14 +- src/game/client/neo/ui/neo_ui.h | 7 +- 5 files changed, 214 insertions(+), 4 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 adcd056014..e8a2ad75fb 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 0aabbe72ce..cb61cbb460 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" @@ -26,6 +30,24 @@ 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"); +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"); +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, 2.0f); +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, 2.0f); DECLARE_NAMED_HUDELEMENT(CNEOHud_SpectatorOverlay, neo_spectator_overlay); @@ -103,6 +125,7 @@ void CNEOHud_SpectatorOverlay::ClearAll() void CNEOHud_SpectatorOverlay::Init() { ListenForGameEvent("round_start"); + ListenForGameEvent("game_end"); ClearAll(); } @@ -126,6 +149,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) @@ -760,12 +807,155 @@ 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 < 2; ++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]) + { + NeoUI::Texture(pszJinLogo, x, y, iLogoWH, iLogoWH, + "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL + | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID + | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + } + 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 < 2; ++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]) + { + NeoUI::Texture(pszNSFLogo, x, y, iLogoWH, iLogoWH, + "", + NeoUI::TEXTUREOPTFLAGS_DONOTCROPTOPANEL + | NeoUI::TEXTUREOPTFLAGS_DONOTCACHEIFINVALID + | NeoUI::TEXTUREOPTFLAGS_RESIZETO256); + } + 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 1d60cfcff0..623d31d490 100644 --- a/src/game/client/neo/ui/neo_hud_spectator_overlay.h +++ b/src/game/client/neo/ui/neo_hud_spectator_overlay.h @@ -88,6 +88,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_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index a190ae0ea3..669bacb4f3 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" @@ -1742,6 +1743,14 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt { Assert(false); } + if (texFlags & NeoUI::TEXTUREOPTFLAGS_RESIZETO256) + { + 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); @@ -1777,7 +1786,10 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt vgui::surface()->DrawSetTextureFile(iTex, szTexturePath, false, true); bApplied = true; } - hdl = c->htTexMap.Insert(szTexturePath, (bApplied) ? iTex : -1); + if (bApplied || !(texFlags & TEXTUREOPTFLAGS_DONOTCACHEIFINVALID)) + { + hdl = c->htTexMap.Insert(szTexturePath, (bApplied) ? iTex : -1); + } } if (hdl != c->htTexMap.InvalidHandle()) diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index f7dd16769f..5dcd87c995 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -650,11 +650,14 @@ void BeginIgnoreXOffset(); void EndIgnoreXOffset(); // NeoUI::Texture is non-widget, but utilizes NeoUI's image/texture handling -enum TextureOptFlags +enum TextureOptFlag_ { TEXTUREOPTFLAGS_NONE = 0, - TEXTUREOPTFLAGS_DONOTCROPTOPANEL = 1, // Disable cropping based on panel's dimension + 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; 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();