EasterEgg/EggFlags.cs

Global static manager for Easter-egg flags. It stores flag names with stable numeric indices and boolean values, provides registration, query helpers (IsSet, AllSet, AnySet), value setting, reset of values, and a snapshot for diagnostics.

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

namespace NZombies;

/// <summary>
/// The Easter-egg flag pool — the single global <c>Dictionary&lt;string,bool&gt;</c> the spec is
/// built on. Every EE step reads its <c>Required</c>/<c>Excluded</c> and writes its <c>Reward</c>
/// against this.
///
/// ⛔ SEPARATE NAMESPACE FROM GENERAL FLAGS. General flags (doors, power, perk machines, spawns)
/// live in <see cref="DoorLinks"/>. An EE flag "2" and a door link named "2" are unrelated values
/// in unrelated pools — nothing crosses over implicitly. The ONE bridge is one-way: an EE step's
/// reward may ALSO open a General flag (see <see cref="EggStep"/>), never the reverse. General
/// flags are not a valid Required/Excluded input here.
///
/// ⛔ SERVER-AUTHORITATIVE, exactly like DoorLinks — a plain static the host owns. Networking the
/// live values to clients is a later concern; for now nothing outside the host reads them.
///
/// ⚠️ FLAG INDEX is the tie-break key (spec §Flags): when two steps would set mutually-excluded
/// flags in the same batch, the lower index wins. Assigned in first-seen order and STABLE for the
/// life of the EE instance — a flag keeps its number across Set/Reset so the tie-break can't shift
/// under a completing step. Reset clears VALUES, not the index map.
/// </summary>
public static class EggFlags
{
	static readonly Dictionary<string, bool> _value = new( StringComparer.OrdinalIgnoreCase );
	static readonly Dictionary<string, int> _index = new( StringComparer.OrdinalIgnoreCase );
	static int _next;

	static string Clean( string flag ) => flag?.Trim() ?? "";

	/// <summary>Register a flag so it has a stable index, without changing its value. The editor
	/// calls this as flags are created; runtime Set does it lazily too.</summary>
	public static int Register( string flag )
	{
		flag = Clean( flag );
		if ( flag == "" ) return int.MaxValue;
		if ( !_index.TryGetValue( flag, out var i ) ) { i = _next++; _index[flag] = i; }
		return i;
	}

	/// <summary>The tie-break key. Unregistered/blank flags sort last.</summary>
	public static int IndexOf( string flag )
		=> _index.TryGetValue( Clean( flag ), out var i ) ? i : int.MaxValue;

	/// <summary>Is this flag active? Blank = false. Unknown = false.</summary>
	public static bool IsSet( string flag )
		=> flag is not null && _value.TryGetValue( Clean( flag ), out var v ) && v;

	/// <summary>Set a flag's value, registering it (and its index) on first sight.</summary>
	public static void Set( string flag, bool value = true )
	{
		flag = Clean( flag );
		if ( flag == "" ) return;
		Register( flag );
		_value[flag] = value;
	}

	/// <summary>True only if EVERY listed flag is active. Empty list = true (no requirement).</summary>
	public static bool AllSet( IEnumerable<string> flags )
		=> flags is null || flags.All( IsSet );

	/// <summary>True if ANY listed flag is active. Empty list = false.</summary>
	public static bool AnySet( IEnumerable<string> flags )
		=> flags is not null && flags.Any( IsSet );

	/// <summary>Wipe all VALUES back to inactive. Indices are kept — see the class note.</summary>
	public static void Reset() => _value.Clear();

	/// <summary>Every flag ever seen, with its current value and index — for diagnostics.</summary>
	public static IEnumerable<(string Flag, bool Value, int Index)> Snapshot()
		=> _index.OrderBy( kv => kv.Value )
			.Select( kv => (kv.Key, IsSet( kv.Key ), kv.Value) );
}