UI/HudState.cs

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.

NetworkingFile Access
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" );
	}
}