Doors/RoomZones.cs

Static manager for room zone data. It stores a list of RoomZone objects from the active config, sends the serialized list from the host to clients, applies a received list, and exposes console commands to list, rename, clear, and show zones.

NetworkingFile Access
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace NZombies;

/// <summary>
/// ROOM ZONES — drawn volumes that name the part of the map a player walks into (`RoomZone`, the Room zone tool, `RoomNames`).
/// Asked for as *"let's make a new tool, the zone tool — where i draw a zone like we do with the invisible wall tool, and when
/// a player goes trough it, the zone changes to the one the zone is named after — this needs networking too"* (2026-09-27):
/// a door names a room only as you come through it, so walking back the way you came never changed the name.
///
/// ⚠️ THE NETWORKING IS THE LIST, AND ONLY THE LIST. Every machine tests its own player against the zones (`RoomNames.Tick`),
/// so all that has to cross the wire is the zones themselves: with the whole config as a game loads (`NZNet.ConfigLoaded`),
/// and on their own from the host the moment one is drawn, renamed or removed (`NZNet.RoomZonesAre`). Sending the whole config
/// for that would rebuild every manager on every client for a renamed room.
/// </summary>
public static class RoomZones
{
	static List<RoomZone> Zones => ActiveConfig.Current?.RoomZones;

	/// <summary>The host's zones, to every other machine. HOST — after every change, here and in the tool.</summary>
	public static void Sync()
	{
		if ( !Networking.IsActive || NZGame.IsClient || Zones is null ) return;
		NZNet.RoomZonesAre( JsonSerializer.Serialize( Zones ) );
	}

	/// <summary>The host's zones, taken whole. EVERY OTHER MACHINE — `NZNet.RoomZonesAre`.</summary>
	public static void Apply( string json )
	{
		List<RoomZone> list;

		try { list = JsonSerializer.Deserialize<List<RoomZone>>( json ); }
		catch ( System.Exception e )
		{
			Log.Warning( $"[nz-rooms] the host's room zones did not read: {e.Message}" );
			return;
		}

		if ( list is null || ActiveConfig.Current is null ) return;

		ActiveConfig.Current.RoomZones = list;
		Log.Info( $"[nz-rooms] {list.Count} room zone(s) from the host" );
	}

	/// <summary>Rename a zone — blank, and it names nothing. The config in memory, `nz_save` keeps it; every machine told.</summary>
	public static bool Rename( int index, string name )
	{
		var zones = Zones;
		if ( zones is null || index < 0 || index >= zones.Count || zones[index] is null ) return false;

		zones[index].Name = name?.Trim() ?? "";
		Sync();
		return true;
	}

	/// <summary>A zone for the console: its name, where it is, how big, and whether this machine's player is in it.</summary>
	static string Text( int i, RoomZone z )
		=> $"zone {i}: {RoomNames.Quoted( z.Name?.Trim() )} · at {z.Position.x:0}, {z.Position.y:0}, {z.Position.z:0}"
			+ $" · {z.Size.x:0} x {z.Size.y:0} x {z.Size.z:0}{(z.HasFootprint ? $", {z.Footprint.Count} corners" : "")}"
			+ (RoomNames.ZonesAround.Contains( z ) ? " · YOU ARE IN IT" : "");

	// ══ console ══════════════════════════════════════════════════════════════════════════════

	/// <summary>`nz_room_zones` — every room zone, its name, where and how big, and which ones this machine's player stands in.</summary>
	[ConCmd( "nz_room_zones" )]
	public static void ListZones()
	{
		var zones = Zones;
		if ( zones is null || zones.Count == 0 )
		{
			Log.Info( "[nz-rooms] no room zones — Q > Walls > Room zone draws one" );
			return;
		}

		for ( var i = 0; i < zones.Count; i++ )
			if ( zones[i] is not null ) Log.Info( $"[nz-rooms] {Text( i, zones[i] )}" );

		Log.Info( $"[nz-rooms] now in {RoomNames.Describe}" );
	}

	/// <summary>
	/// `nz_room_zone_name &lt;index&gt; [name...]` — rename a zone; no name, and it names nothing. `params`, so a name can have
	/// spaces. The config in memory — `nz_save` keeps it.
	/// </summary>
	[ConCmd( "nz_room_zone_name" )]
	public static void NameCmd( params string[] args )
	{
		if ( args is not { Length: > 0 } || !int.TryParse( args[0], out var i ) )
		{
			Log.Info( "[nz-rooms] nz_room_zone_name <index> [name...] — nz_room_zones lists them" );
			return;
		}

		var name = string.Join( " ", args.Skip( 1 ) ).Trim();
		if ( !Rename( i, name ) )
		{
			Log.Warning( $"[nz-rooms] no room zone {i} — there are {Zones?.Count ?? 0}" );
			return;
		}

		Log.Info( $"[nz-rooms] zone {i} is {RoomNames.Quoted( name )} — nz_save keeps it" );
	}

	/// <summary>`nz_room_zone_clear &lt;index|all&gt;` — remove one zone, or every one. The config in memory — `nz_save` keeps it.</summary>
	[ConCmd( "nz_room_zone_clear" )]
	public static void Clear( string which = "" )
	{
		var zones = Zones;
		if ( zones is null || zones.Count == 0 ) { Log.Info( "[nz-rooms] no room zones to remove" ); return; }

		if ( which.Trim().ToLowerInvariant() == "all" )
		{
			var n = zones.Count;
			zones.Clear();
			Sync();
			Log.Info( $"[nz-rooms] removed all {n} room zone(s)" );
			return;
		}

		if ( !int.TryParse( which, out var i ) || i < 0 || i >= zones.Count )
		{
			Log.Info( $"[nz-rooms] nz_room_zone_clear <index> — or all · there are {zones.Count} (0 to {zones.Count - 1})" );
			return;
		}

		var gone = zones[i];
		zones.RemoveAt( i );
		Sync();
		Log.Info( $"[nz-rooms] removed zone {i}, {RoomNames.Quoted( gone?.Name?.Trim() )} ({zones.Count} left)" );
	}

	/// <summary>`nz_room_zone_where` — the zones this machine's player stands in, and the name the top left shows.</summary>
	[ConCmd( "nz_room_zone_where" )]
	public static void Where()
	{
		var zones = Zones ?? new List<RoomZone>();
		var around = RoomNames.ZonesAround;

		Log.Info( around.Count == 0
			? "[nz-rooms] in no room zone — the doors and the pads name the room here"
			: $"[nz-rooms] in {around.Count} room zone(s): "
				+ string.Join( ", ", around.Select( z => $"{zones.IndexOf( z )} {RoomNames.Quoted( z.Name?.Trim() )}" ) ) );
		Log.Info( $"[nz-rooms] the top left: {RoomNames.Describe}" );
	}
}