UI/ConfigBrowserState.cs

Static UI state for a config browser. Tracks whether the browser is open (Load/Save/None), selected row, typed save name, delete-mode toggle, current map key, and provides a listing of saved configs plus helper validation and a console command to open the menu.

File AccessNetworking
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// What the config browser is doing — a seam, like LobbyState, because razor
/// types are generated and cannot be referenced from a plain .cs file.
/// </summary>
public static class ConfigBrowserState
{
	public enum Mode
	{
		/// <summary>Closed.</summary>
		None,

		/// <summary>Pick a config to load, or delete one.</summary>
		Load,

		/// <summary>Type a name and write the current config to it.</summary>
		Save,
	}

	public static Mode Showing { get; set; } = Mode.None;

	/// <summary>Row highlighted in the load list, or "".</summary>
	public static string Selected { get; set; } = "";

	/// <summary>Name being typed in the save box.</summary>
	public static string TypedName { get; set; } = "default";

	/// <summary>
	/// Delete-mode toggle, ported from the original's loader.
	///
	/// ⚠️ A MODE, NOT A PER-ROW BUTTON. The original does this deliberately:
	/// a delete icon on every row is one mis-click from destroying a config,
	/// and there is no undo. Arming a mode first means the destructive click is
	/// never the one you make by accident.
	/// </summary>
	public static bool DeleteMode { get; set; }

	public static void Open( Mode mode )
	{
		Showing = mode;
		Selected = "";
		DeleteMode = false;

		if ( mode == Mode.Save ) TypedName = ActiveConfig.Current.Name;
	}

	public static void Close()
	{
		Showing = Mode.None;
		DeleteMode = false;
	}

	// ── data ─────────────────────────────────────────────────────────────────

	/// <summary>
	/// Which map's configs to list.
	///
	/// ✅ THE SHARED ACCESSOR NOW EXISTS. This used to be hardcoded alongside LobbyCommands.MapName
	/// with a note that the two MUST agree; both now resolve to <see cref="NZMap.Current"/>,
	/// which reads the scene's MapInstance. So the moment the map changes, this browser lists that
	/// map's configs with no further wiring.
	/// </summary>
	public static string CurrentMap => LobbyCommands.MapName;

	/// <summary>A saved config: its file's name, whether it is the one loaded, and its <see cref="MapConfig.Title"/> if it has one.</summary>
	public record Entry( string Name, bool IsActive, string Title = "" )
	{
		/// <summary>What the list shows: the title and the file's name, or the name alone (2026-09-28).</summary>
		public string Label => string.IsNullOrWhiteSpace( Title ) ? Name : Title + " (" + Name + ")";
	}

	/// <summary>Every saved config for this map, newest names last.</summary>
	public static List<Entry> Entries => MapConfig.ListFor( CurrentMap )
		.OrderBy( n => n )
		// ⚠️ THE LOADED ONE'S TITLE FROM MEMORY, so an unsaved edit shows; every other from its file (`MapConfig.TitleOf`)
		.Select( n => n == ActiveConfig.Current.Name
			? new Entry( n, true, ActiveConfig.Current.Title ?? "" )
			: new Entry( n, false, MapConfig.TitleOf( CurrentMap, n ) ) )
		.ToList();

	/// <summary>
	/// Is this a usable config name?
	///
	/// ⚠️ The original rejects ';' because its save format is
	/// "map;name.txt" — a semicolon in the name would produce a file it could
	/// never find again. Ours is a folder per map, so the real hazard is path
	/// separators and traversal; MapConfig.Sanitise already strips them, but
	/// rejecting up front means the name you typed is the name you get rather
	/// than a silently mangled one.
	/// </summary>
	public static bool IsValidName( string name )
		=> !string.IsNullOrWhiteSpace( name )
			&& name.Trim().All( c => char.IsLetterOrDigit( c ) || c == '_' || c == '-' || c == ' ' );

	public static string NameWarning( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) ) return "Name cannot be empty";
		if ( !IsValidName( name ) ) return "Letters, numbers, spaces, _ and - only";
		return "";
	}

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

	[ConCmd( "nz_config_menu" )]
	public static void OpenMenu( string which = "load" )
	{
		// ⛔ NOT IN A PUBLISHED COPY, whose lobby has a Gamemode row in place of this browser (`Edition`, 2026-10-05)
		if ( Edition.IsPublished )
		{
			Showing = Mode.None;
			Log.Info( "[nz] the published game has no config browser — nz_gamemode lists its gamemodes" );
			return;
		}

		Showing = which.ToLowerInvariant() switch
		{
			"save" => Mode.Save,
			"close" or "" => Mode.None,
			_ => Mode.Load,
		};

		Log.Info( $"[nz] config browser: {Showing}" );

		if ( Showing != Mode.Load ) return;

		foreach ( var e in Entries )
			Log.Info( $"[nz]   {e.Name}{(e.IsActive ? "   (active)" : "")}" );
	}
}