A Blazor-like Sandbox.UI Panel that implements a config browser UI for saving, loading and deleting map configurations. It displays save modal with name validation and overwrite warning, or a load list with selection and delete mode, and calls LobbyCommands to save/load/delete configs.
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@using NZombies;
@inherits Panel
@*
CONFIG BROWSER — save with a name, load or delete any saved config.
Ported in BEHAVIOUR from the original's sh_int_configsaver.lua and
sh_int_configloader.lua:
SAVE a modal with a name field, live validation, and a save button that
is dead until the name is usable.
LOAD a list of every config for this map, one selected at a time, with a
separate armed DELETE MODE rather than a delete button per row.
⚠️ A child Panel of the lobby, not its own PanelComponent — no GameObject,
no scene edit. The last panel built the other way was added at runtime, never
saved, and silently disappeared on restart.
*@
@if ( Showing != ConfigBrowserState.Mode.None )
{
<div class="configbrowser">
<div class="sheet">
<div class="head">
<div class="title">@(Showing == ConfigBrowserState.Mode.Save ? "Save config" : "Load config")</div>
<div class="map">@ConfigBrowserState.CurrentMap</div>
<div class="x" [email protected]>✕</div>
</div>
@if ( Showing == ConfigBrowserState.Mode.Save )
{
<div class="body">
<div class="fieldlabel">Name</div>
<TextEntry class="nameentry" [email protected]
OnTextEdited=@( ( string v ) => { ConfigBrowserState.TypedName = v; } ) />
@if ( !string.IsNullOrEmpty( Warning ) )
{
<div class="warn">@Warning</div>
}
else if ( Exists )
{
<div class="warn overwrite">A config called "@Trimmed" already exists — saving overwrites it</div>
}
<div class="summary">
@Counts
</div>
</div>
<div class="foot">
<div class="btn @(CanSave ? "" : "disabled")" onclick=@DoSave>Save</div>
</div>
}
else
{
<div class="list">
@if ( !Entries.Any() )
{
<div class="empty">No configs saved for this map yet.</div>
}
@foreach ( var entry in Entries )
{
var e = entry;
<div class="row @(Selected == e.Name ? "sel" : "") @(e.IsActive ? "active" : "")"
onclick=@( () => { ConfigBrowserState.Selected = e.Name; } )>
<div class="name">@e.Label</div>
@if ( e.IsActive )
{
<div class="badge">loaded</div>
}
</div>
}
</div>
<div class="foot">
<div class="btn ghost @(DeleteMode ? "armed" : "")"
onclick=@( () => { ConfigBrowserState.DeleteMode = !DeleteMode; } )>
@(DeleteMode ? "Cancel delete" : "Delete")
</div>
<div class="btn @(HasSelection ? "" : "disabled") @(DeleteMode ? "danger" : "")"
onclick=@DoConfirm>
@(DeleteMode ? "Delete selected" : "Load")
</div>
</div>
}
</div>
</div>
}
@code
{
ConfigBrowserState.Mode Showing => ConfigBrowserState.Showing;
string Selected => ConfigBrowserState.Selected;
bool DeleteMode => ConfigBrowserState.DeleteMode;
List<ConfigBrowserState.Entry> Entries => ConfigBrowserState.Entries;
bool HasSelection => !string.IsNullOrEmpty( Selected );
string Trimmed => (ConfigBrowserState.TypedName ?? "").Trim();
string Warning => ConfigBrowserState.NameWarning( ConfigBrowserState.TypedName );
bool CanSave => string.IsNullOrEmpty( Warning );
/// <summary>Does saving overwrite something? Worth saying BEFORE the click,
/// since there is no undo and no confirm step after it.</summary>
bool Exists => CanSave && Entries.Any( e =>
string.Equals( e.Name, Trimmed, System.StringComparison.OrdinalIgnoreCase ) );
/// <summary>What is actually in the config being saved — the only way to
/// notice you are about to write an empty one over a good one.</summary>
string Counts
{
get
{
var c = ActiveConfig.Current;
return $"{c.ZombieSpawns.Count} zombie spawns · {c.PlayerSpawns.Count} player spawns · "
+ $"{c.Debris.Count} barriers · {c.PowerSwitches.Count} switches";
}
}
void DoSave()
{
if ( !CanSave ) return;
LobbyCommands.SaveConfig( Trimmed );
ConfigBrowserState.Close();
}
void DoConfirm()
{
if ( !HasSelection ) return;
if ( DeleteMode )
{
LobbyCommands.DeleteConfig( Selected );
ConfigBrowserState.Selected = "";
ConfigBrowserState.DeleteMode = false;
return;
}
LobbyCommands.LoadConfig( Selected );
ConfigBrowserState.Close();
}
// ⚠️ The typed name and the entry list both have to be in here, or the
// warning and the overwrite notice never appear — they are computed from
// state this panel does not own.
protected override int BuildHash()
=> System.HashCode.Combine( Showing, Selected, DeleteMode,
ConfigBrowserState.TypedName, Entries.Count );
}