UI/ConfigBrowser.razor

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.

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