UI/ToolPanelState.cs

Static state for the tool settings panel. Stores the current context string, exposes methods to show a tool or group, follows the actively armed tool each frame by reading MapEditor.ActiveTool, and provides console commands to open the settings panel and nudge individual settings.

NetworkingFile Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// What the tool settings panel is showing.
///
/// A static rather than a property on the panel, for the same reason LobbyState
/// exists: razor components are generated types and cannot be referenced from a
/// plain .cs file, so the dev menu and the console both need a seam to poke.
/// </summary>
public static class ToolPanelState
{
	/// <summary>A tool id (NZTools.*), a ToolSettings group, or "" for hidden.</summary>
	public static string Context { get; set; } = "";

	/// <summary>Show the settings for a tool, and arm that tool.</summary>
	public static void ShowTool( string tool )
	{
		ToolCommands.Tool( tool );
		Context = tool;
	}

	/// <summary>Show a settings group without touching the armed tool.</summary>
	public static void ShowGroup( string group ) => Context = group;

	/// <summary>
	/// Is this a settings group rather than a tool id?
	///
	/// ⚠️ The naming convention IS the contract — every group constant in
	/// <see cref="ToolSettings"/> is `settings_*` and no `NZTools` id is. Anything
	/// added outside that convention will be treated as a tool and closed on the
	/// next frame, so keep the prefix.
	/// </summary>
	public static bool IsGroup( string context )
		=> !string.IsNullOrEmpty( context ) && context.StartsWith( "settings_" );

	/// <summary>
	/// Follow the armed tool.
	///
	/// ⚠️ Called every frame by the dev menu so holstering a tool (RMB, or
	/// arming another from the console) closes or retargets the panel. Without
	/// it the panel would keep showing settings for a tool you are no longer
	/// holding, and edits would silently apply to the next thing you placed.
	/// </summary>
	public static void FollowActiveTool()
	{
		// Settings groups are not tools — they stay open until closed.
		//
		// ⛔ MATCHED BY PREFIX, NOT BY AN EXPLICIT LIST. This was three hardcoded
		// constants, and adding a fourth group (`WeaponStats`) without knowing about
		// them produced a perfect silent failure: the click set `Context`, this ran
		// on the very next frame, saw a value that was not one of the three, decided
		// it must be a stale TOOL id and reset it to the armed tool — "". The button
		// worked, the panel was built, and nothing appeared. Reported as *"clicking
		// does nothing, do i need to restart the editor?"*
		//
		// ⚠️ Every group constant is `settings_*` and every tool id is not, so the
		// prefix IS the distinction the list was approximating. A new group now works
		// without editing this method, which is the only way this does not recur.
		if ( IsGroup( Context ) ) return;

		var editor = Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
		var active = editor?.ActiveTool ?? "";

		if ( Context != active ) Context = active;
	}

	// ── console ──────────────────────────────────────────────────────────────

	/// <summary>Open the settings panel: nz_settings [players|zombies|gameplay|tool].</summary>
	[ConCmd( "nz_settings" )]
	public static void Open( string which = "" )
	{
		Context = which.ToLowerInvariant() switch
		{
			"players" or "player" => ToolSettings.Players,
			"zombies" or "zombie" => ToolSettings.Zombies,
			"gameplay" or "points" => ToolSettings.Gameplay,
			"pap" or "packapunch" => ToolSettings.Pap,
			"ammo" or "ammobox" => ToolSettings.Ammo,
			"rooms" or "room" or "roomnames" => ToolSettings.Rooms,
			"" or "close" => "",
			_ => which,
		};

		if ( string.IsNullOrEmpty( Context ) ) { Log.Info( "[nz] settings panel closed" ); return; }

		var rows = ToolSettings.For( Context );
		Log.Info( $"[nz] {ToolSettings.TitleFor( Context )} — {rows.Count} setting(s)" );

		foreach ( var r in rows )
			Log.Info( $"[nz]   {r.Label,-22} {r.Value()}"
				+ (string.IsNullOrEmpty( r.Hint ) ? "" : $"   ({r.Hint})") );
	}

	/// <summary>Nudge one setting from the console: nz_setting &lt;label&gt; [dir].
	/// Matches on a label prefix, so `nz_setting price 1` works.</summary>
	[ConCmd( "nz_setting" )]
	public static void Nudge( string label = "", int dir = 1 )
	{
		if ( string.IsNullOrEmpty( Context ) ) { Log.Warning( "[nz] no settings open — nz_settings first" ); return; }

		var rows = ToolSettings.For( Context );
		var row = rows.FirstOrDefault( r =>
			r.Label.StartsWith( label, System.StringComparison.OrdinalIgnoreCase ) );

		if ( row is null )
		{
			Log.Warning( $"[nz] no setting matching '{label}' — have: "
				+ string.Join( ", ", rows.Select( r => r.Label ) ) );
			return;
		}

		row.Nudge( dir );
		Log.Info( $"[nz] {row.Label} -> {row.Value()}" );
	}
}