Config/ActiveConfig.cs

Static helper that holds the currently active MapConfig and exposes its grouped subsettings (Zombies, Player, Gameplay, etc.), plus helpers to swap, reset, notify changes, load from file, and report whether the config is playable.

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

namespace NZombies;

/// <summary>
/// The config the game is currently running on.
///
/// One place for "what are the rules right now", so systems read settings
/// instead of hardcoded constants. Starts as a fresh MapConfig, which means the
/// ported defaults from ZombieStats — so the game is correctly balanced before
/// anyone saves anything.
///
/// ⚠️ ALWAYS go through Set(). Assigning Current directly would leave
/// ZombieStats' memoized round curves holding values computed from the OLD
/// settings, and the symptom is subtle: health that ignores the config until
/// something else happens to clear the cache.
/// </summary>
public static class ActiveConfig
{
	public static MapConfig Current { get; private set; } = new();

	public static ZombieSettings Zombies => Current.Zombies;
	public static PlayerSettings Player => Current.Player;
	public static GameplaySettings Gameplay => Current.Gameplay;
	public static ArmorSettings Armor => Current.Armor;

	/// <summary>Boss round schedule. See BossSettings.</summary>
	public static BossSettings Bosses => Current.Bosses;
	public static SalvageSettings Salvage => Current.Salvage;
	public static PapSettings Pap => Current.Pap;
	public static AmmoBoxSettings AmmoBox => Current.AmmoBox;

	/// <summary>
	/// A setting was edited IN PLACE — push it to whatever has already read it.
	///
	/// ⚠️ Set() only runs on a whole-config SWAP, so editing a field on the live
	/// config notified nothing. Two things then quietly ignore you:
	///
	///   • ZombieStats caches health PER ROUND, so a round already computed
	///     keeps its old number — the curve appears not to respond at all.
	///   • PlayerController holds its own copies of walk/sprint/jump, taken once
	///     at spawn, so speed changes do nothing until you respawn.
	///
	/// Anything that edits a setting outside of Set() must call this, or the
	/// value changes and the game does not.
	/// </summary>
	public static void NotifyChanged()
	{
		ZombieStats.InvalidateCurves();

		foreach ( var p in Game.ActiveScene?.GetAllComponents<NZPlayer>() ?? Enumerable.Empty<NZPlayer>() )
			p.ApplyConfig();
	}

	/// <summary>Swap the active config and drop anything derived from the old one.</summary>
	public static void Set( MapConfig config )
	{
		Current = config ?? new MapConfig();
		ZombieStats.InvalidateCurves();

		// ⚠️ An empty config reports as NONE, not as 'default'. MapConfig.Name
		// defaults to "default" so that `nz_save` has something to write under —
		// which means a config nobody loaded still logs a plausible name, and the
		// one question this line exists to answer ("is anything loaded?") gets a
		// misleading yes.
		Log.Info( IsPlayable
			? $"[nz] active config: '{Current.Name}' for {Current.Map} "
				+ $"({Current.PlayerSpawns.Count} player / "
				+ $"{Current.ZombieSpawns.Count} zombie spawns)"
			: $"[nz] active config: NONE — {PlayableProblem}. "
				+ "Place them in Creative, or nz_load a saved one." );

		// ⛔ SWAPPING THE DATA IS NOT PUTTING IT IN THE WORLD, AND THIS IS WHERE
		// THAT WAS MISSED. `nz_load` reached exactly this far: the config was
		// live, `nz_debris_list` reported the barriers, and the map had none of
		// them in it — the managers were still holding the PREVIOUS config's
		// world. It looked like "debris only shows in Survival" because entering
		// Survival is one of the few paths that happens to rebuild.
		//
		// ⚠️ HERE rather than in LoadAndApply, because Set is the choke point.
		// Reset() goes through it too, and a reset that leaves the old map's
		// walls standing is the same bug wearing the opposite face.
		NZGame.ShowConfig();
	}

	/// <summary>Load a saved config and make it active. False if it isn't there.</summary>
	public static bool LoadAndApply( string map, string name )
	{
		var cfg = MapConfig.Load( map, name );
		if ( cfg is null ) return false;

		Set( cfg );
		return true;
	}

	/// <summary>Back to the ported defaults, discarding any loaded config.</summary>
	public static void Reset() => Set( new MapConfig() );

	/// <summary>
	/// Could a game actually be started on what is loaded?
	///
	/// ⚠️ ASKS WHAT THE GAME NEEDS, not whether a file was opened. `Current` is
	/// never null — it defaults to `new MapConfig()` — so "is a config loaded"
	/// has no honest bool behind it. The two things a round cannot run without
	/// are somewhere to put the player and somewhere to put the zombies.
	/// </summary>
	public static bool IsPlayable => Current is not null
		&& Current.PlayerSpawns.Count > 0
		&& Current.ZombieSpawns.Count > 0;

	/// <summary>
	/// Why it cannot be played, in a few words. Empty when it can.
	///
	/// ⚠️ Names the MISSING piece rather than saying "no config". "No zombie
	/// spawns" tells you which tool to go and use; "no config loaded" sends you
	/// looking for a file that may well already be open.
	/// </summary>
	public static string PlayableProblem
	{
		get
		{
			if ( Current is null ) return "No config";

			var missing = new List<string>();
			if ( Current.PlayerSpawns.Count == 0 ) missing.Add( "player spawns" );
			if ( Current.ZombieSpawns.Count == 0 ) missing.Add( "zombie spawns" );

			return missing.Count == 0 ? "" : $"No {string.Join( " or ", missing )}";
		}
	}
}