Static HUD seam for the survival HUD. Exposes delegates and a console command to inspect and refresh the player portrait, chooses a SteamId fallback between Connection.Local and Game.SteamId, and logs diagnostic information about avatar loading and element presence.
using Sandbox;
namespace NZombies;
/// <summary>
/// Seam for the survival HUD, like LobbyState — razor types are generated, so a
/// plain .cs file cannot reference SurvivalHud directly. The panel assigns these
/// on build; the console commands below drive them.
/// </summary>
public static class HudState
{
/// <summary>Drop the cached avatar texture so it is fetched again.</summary>
public static System.Action RefreshPortrait;
/// <summary>Whether the portrait actually has a texture right now.</summary>
public static System.Func<bool> PortraitLoaded;
/// <summary>Whether the @ref element exists at all right now.</summary>
public static System.Func<bool> PortraitElement;
/// <summary>
/// The local player's Steam ID, preferring the API the rest of this project
/// already uses.
///
/// ⚠️ `Game.SteamId` is the older accessor and LobbyMenu had already moved to
/// `Connection.Local` for names and player lists — so the HUD was the only
/// place still asking the old way. If it returns 0, `Texture.LoadAvatar( 0 )`
/// has nothing to fetch and the portrait is blank no matter how many times it
/// is re-applied, which is exactly the symptom that survived two fixes.
///
/// Tries both rather than picking, because being wrong here is silent.
/// </summary>
/// ⚠️ `long`, NOT `ulong`, and every conversion is explicit. `Texture.LoadAvatar`
/// takes a long, and comparing a SteamId against a bare `0` picks up
/// `implicit operator SteamId(int)`, which the engine marks obsolete —
/// "SteamIds should never be referenced as an int".
public static long LocalSteamId
{
get
{
var c = Connection.Local;
long fromConnection = c is null ? 0L : (long)c.SteamId;
return fromConnection != 0L ? fromConnection : (long)Game.SteamId;
}
}
/// <summary>
/// Why the points portrait is or isn't showing: nz_hud_portrait.
///
/// ⚠️ THE FAILURE THIS EXISTS FOR IS A TIMING ONE, so a still screenshot
/// cannot diagnose it, and it already survived one wrong fix. The portrait
/// appeared only after the player's first reload — not because reloading
/// matters, but because `@ref` points at an element the tree rebuild
/// DESTROYS AND RECREATES, and a reload is simply the first BuildHash change
/// most players trigger. Any "we already applied it" flag is describing an
/// element that no longer exists.
///
/// If it is ever blank again, this says whether Steam has an ID yet and
/// whether a texture came back, which separates "never asked" from "asked
/// and got nothing" from "got one but it is not on the element".
/// </summary>
[ConCmd( "nz_hud_portrait" )]
public static void Portrait( string action = "" )
{
var conn = Connection.Local;
long connId = conn is null ? 0L : (long)conn.SteamId;
long id = LocalSteamId;
Log.Info( $"[nz-hud] Connection.Local: {(conn is null ? "NULL" : conn.DisplayName)}"
+ $" SteamId {connId}" );
Log.Info( $"[nz-hud] Game.SteamId: {(long)Game.SteamId}" );
Log.Info( $"[nz-hud] using: {id}"
+ (id == 0L ? " ⛔ ZERO — nothing to fetch, portrait cannot load" : "") );
// Ask twice: a texture that is null now but valid a moment later means
// it loads lazily, which is a different fix from "no id".
var a1 = id == 0L ? null : Texture.LoadAvatar( id, 128 );
var a2 = id == 0L ? null : Texture.LoadAvatar( id, 64 );
Log.Info( $"[nz-hud] LoadAvatar(128) -> {(a1 is null ? "NULL" : $"{a1.Width}x{a1.Height}")}"
+ $" LoadAvatar(64) -> {(a2 is null ? "NULL" : $"{a2.Width}x{a2.Height}")}" );
Log.Info( "[nz-hud] the portrait is drawn from the MARKUP now "
+ $"(src=\"avatar:{id}\"), so these are informational — a blank "
+ "portrait with a valid id means the avatar: scheme is not resolving." );
Log.Info( $"[nz-hud] HUD panel present: {PortraitElement is not null}" );
Log.Info( $"[nz-hud] <img> element exists: {PortraitElement?.Invoke()}" );
Log.Info( $"[nz-hud] element has a texture: {PortraitLoaded?.Invoke()}" );
if ( PortraitElement?.Invoke() == false )
Log.Warning( "[nz-hud] ⛔ the @ref is NULL — OnUpdate bails every frame, "
+ "so nothing above matters. That is the bug, not the avatar." );
if ( action.ToLowerInvariant() != "refresh" ) return;
if ( RefreshPortrait is null ) { Log.Warning( "[nz-hud] no HUD panel" ); return; }
RefreshPortrait.Invoke();
Log.Info( "[nz-hud] latch cleared — it will re-fetch next frame" );
}
}