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.
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)" : "")}" );
}
}