EasterEgg/HexPlatforms.Shields.cs

Manager code for two cyan shield columns in a map. Finds the shield faces by material and distance, splits them out of the shared world mesh into two cloned GameObjects (shield-only and rest-only), toggles their Enabled state to hide/show the shield locally, and requests navmesh tile regeneration on the host when a column changes.

File AccessNative Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace NZombies;

/// <summary>
/// BASALT'S TWO CYAN SHIELD COLUMNS — how either is taken down and put back. The map has two: one beside teleporter #0's
/// near pad, which the shield lock opens, with the altar in its middle (`HexPlatforms.Lock.cs`, `.Altar.cs`); and its twin
/// at the teleporter's far pad on the west upper floor, which the light blue flame brings down (`HexPlatforms.Twin.cs`).
/// Each is a hexagonal column of `EFFECTS/COM_SHIELD002A`, 120u across, floor to ceiling.
///
/// ⛔ A SHIELD IS PART OF A BIGGER MESH, SO IT IS SPLIT OUT OF IT. The importer cuts the world into chunks of faces, and each
/// column shares its world mesh with the room round it — the lock's with the north-east walls and a hexagonal frame
/// ("Mesh 3"), the twin's with the west walls ("Mesh 49"): switching that object off would take the walls with it. So the
/// first time a shield goes, its mesh is cloned twice — one copy without the column's six faces, which takes the
/// original's place, and one with only them — and the original is switched off (<see cref="SplitColumn"/>). From then the
/// shield goes and comes back by switching its own object off and on. Its faces are found by what they are — the
/// material, within <see cref="ShieldReach"/> of the column's middle — not by the mesh's name.
///
/// ⛔ AND THE NAVMESH ROUND IT IS BUILT AGAIN, ON THE HOST, each time (<see cref="RebuildColumnNav"/>): it was generated with
/// the columns standing, so without it no zombie could walk in where a shield stood.
///
/// ⚠️ LOCAL: every machine splits and hides its own copy of the faces, from the state it was told.
/// </summary>
public sealed partial class HexPlatforms
{
	/// <summary>One cyan shield column: where it stands, and what this manager knows of its faces and its split. LOCAL.</summary>
	sealed class ShieldColumn
	{
		/// <summary>The shield's own object, once split out — by name, so a manager after a hotload takes it up again.</summary>
		public string Name;

		/// <summary>What both of its split's copies carry.</summary>
		public string Tag;

		/// <summary>For the log: "cyan shield", "twin shield".</summary>
		public string Word;

		/// <summary>The column's middle: its faces lie 60u out of it.</summary>
		public Vector3 Centre;

		/// <summary>The world mesh holding its faces as the map built it, and the faces; found once, on first need.</summary>
		public MeshComponent Mesh;
		public List<HalfEdgeMesh.FaceHandle> Faces;

		/// <summary>The split's two copies: the shield alone, and the rest of its mesh.</summary>
		public GameObject Go, Rest;

		/// <summary>Said once that it could not be found or split, not at every rebuild.</summary>
		public bool Missing;
	}

	/// <summary>
	/// What the lock's split copies carry, and the twin's. ⚠️ NOT `PanelTag`, WHICH `Ensure` SWEEPS AWAY — sweeping a
	/// rest-of-the-mesh copy would take the room's walls with it, the original being off. ⚠️ THE LOCK'S IS WHAT IT WAS
	/// BEFORE THERE WERE TWO, so a split made then is still found.
	/// </summary>
	const string ShieldTag = "nz_shieldsplit", TwinTag = "nz_shieldsplit_twin";

	/// <summary>How far from a column's middle a face of the shield's material may be and still be the column's own.</summary>
	const float ShieldReach = 100f;

	/// <summary>The shields' material, as the importer named it after the BSP's `EFFECTS/COM_SHIELD002A`.</summary>
	const string ShieldMaterial = "com_shield002a";

	ShieldColumn _lockColumn, _twinColumn;

	/// <summary>The lock's column, beside teleporter #0's near pad.</summary>
	ShieldColumn LockColumn => _lockColumn ??= new() { Name = "Basalt cyan shield", Tag = ShieldTag, Word = "cyan shield", Centre = ShieldCentre };

	/// <summary>The twin, at teleporter #0's far pad.</summary>
	ShieldColumn TwinColumn => _twinColumn ??= new() { Name = "Basalt twin shield", Tag = TwinTag, Word = "twin shield", Centre = TwinCentre };

	/// <summary>Is this one of the split's copies, of either column?</summary>
	static bool IsSplitCopy( GameObject go ) => go.Tags.Has( ShieldTag ) || go.Tags.Has( TwinTag );

	/// <summary>
	/// Is this a copy of one of the MAP'S OWN meshes — the arena's floor and the rest round it, a shield and the rest of its
	/// column? Those travel in a joiner's snapshot on purpose, and the joiner takes them up (`ArenaCopy`, `SplitCopy`); for
	/// `nz_net_snapshot`, which lists what travels.
	/// </summary>
	public static bool IsMapMeshCopy( GameObject go ) => go.IsValid() && (go.Tags.Has( ArenaTag ) || IsSplitCopy( go ));

	/// <summary>This mesh's faces that are a column's: of <see cref="ShieldMaterial"/>, their middles within <see cref="ShieldReach"/> of its middle.</summary>
	static List<HalfEdgeMesh.FaceHandle> ColumnFacesIn( MeshComponent mc, Vector3 centre )
	{
		var faces = new List<HalfEdgeMesh.FaceHandle>();
		var mesh = mc?.Mesh;
		if ( mesh is null ) return faces;

		foreach ( var f in mesh.FaceHandles )
		{
			var mat = mesh.GetFaceMaterial( f );
			var name = mat?.ResourcePath ?? mat?.Name ?? "";
			if ( name.Contains( ShieldMaterial, StringComparison.OrdinalIgnoreCase )
				&& mc.WorldTransform.PointToWorld( mesh.GetFaceCenter( f ) ).Distance( centre ) <= ShieldReach )
				faces.Add( f );
		}

		return faces;
	}

	/// <summary>
	/// A column's faces, and the world mesh holding them as the map built it — its six sides, and nothing of the other
	/// column. LOCAL, looked for once; none off basalt. Never a split's own copies.
	/// </summary>
	(MeshComponent Mesh, List<HalfEdgeMesh.FaceHandle> Faces) ColumnFaces( ShieldColumn c )
	{
		if ( c.Mesh.IsValid() && c.Faces is { Count: > 0 } ) return (c.Mesh, c.Faces);
		if ( !Scene.IsValid() ) return (null, null);

		foreach ( var mc in Scene.GetAllComponents<MeshComponent>() )
		{
			if ( IsSplitCopy( mc.GameObject ) ) continue;

			var faces = ColumnFacesIn( mc, c.Centre );
			if ( faces.Count == 0 ) continue;

			c.Mesh = mc;
			c.Faces = faces;
			Log.Info( $"[nz-hex] the {c.Word}: {faces.Count} faces of {mc.GameObject.Name}, within {ShieldReach:0}u of its middle" );
			break;
		}

		return (c.Mesh, c.Faces);
	}

	/// <summary>
	/// A column's shield as an object of its own, split once out of the world mesh that holds it: the mesh cloned twice —
	/// one copy without the shield's faces, in the original's place, and one with only them — and the original switched off.
	/// LOCAL. Null if the faces are not found, or the copies cannot be made.
	///
	/// ⚠️ CLONED, NOT EDITED IN PLACE. Hiding the faces (`PolygonMesh.SetFaceHidden`) and rebuilding the original did nothing
	/// in play — tried 2026-09-26: the shield still drew, and a trace still hit it. The hidden flag is the editor's. A
	/// MeshComponent switched on builds from the mesh it holds, so each copy is trimmed while it is still off.
	///
	/// ⚠️ A SPLIT A MANAGER BEFORE THIS ONE MADE — a hotload can bring a new manager — IS TAKEN UP AGAIN, never made twice.
	/// </summary>
	GameObject SplitColumn( ShieldColumn c )
	{
		if ( c.Go.IsValid() ) return c.Go;
		if ( !Scene.IsValid() ) return null;

		foreach ( var x in Scene.GetAllObjects( false ).Where( x => x.Tags.Has( c.Tag ) ) )
			if ( x.Name == c.Name ) c.Go = x;
			else c.Rest = x;
		if ( c.Go.IsValid() ) return c.Go;

		var (mc, faces) = ColumnFaces( c );
		if ( !mc.IsValid() || faces is not { Count: > 0 } ) return null;

		var src = mc.GameObject;
		var rest = SplitCopy( src, c, keepShield: false, $"{src.Name} — the rest, without the {c.Word}" );
		var shield = SplitCopy( src, c, keepShield: true, c.Name );
		if ( !rest.IsValid() || !shield.IsValid() )
		{
			if ( rest.IsValid() ) rest.Destroy();
			if ( shield.IsValid() ) shield.Destroy();
			return null;
		}

		rest.Enabled = true;
		shield.Enabled = true;
		src.Enabled = false;
		c.Rest = rest;
		c.Go = shield;
		Log.Info( $"[nz-hex] the {c.Word} split out of {src.Name}: its {faces.Count} faces an object of their own, the rest of"
			+ " the mesh a copy in its place" );
		return c.Go;
	}

	/// <summary>A copy of this world mesh, still off, keeping only a column's faces — or only the others.</summary>
	static GameObject SplitCopy( GameObject src, ShieldColumn c, bool keepShield, string name )
	{
		GameObject go;
		try { go = src.Clone( new CloneConfig { Transform = src.WorldTransform, StartEnabled = false, Name = name } ); }
		catch ( Exception e ) { Log.Warning( $"[nz-hex] {src.Name} would not clone ({e.Message})" ); return null; }
		if ( !go.IsValid() ) return null;

		// ⛔ SetParent KEEPS THE WORLD TRANSFORM — and the place is set again after it all the same
		go.SetParent( src.Parent );
		go.WorldTransform = src.WorldTransform;
		go.Flags |= GameObjectFlags.NotSaved;

		// ⛔ NOT `NetworkMode.Never` (2026-09-29): a COPY OF A MAP MESH travels in a joiner's snapshot, and the joiner takes it up
		// by its tag (`SplitColumn`), as a new manager after a hotload does. The map's own mesh goes there too, switched off, and
		// `ColumnFaces` finds only what is on — so with the copies kept home, a player who joined after a shield went had a hole
		// where its column stood. The arena's floor copies are the same (`ArenaCopy`); see NZNetListener.
		go.Tags.Add( c.Tag );

		var mc = go.Components.Get<MeshComponent>( FindMode.EverythingInSelf );
		if ( mc?.Mesh is null ) { go.Destroy(); return null; }

		var shieldFaces = ColumnFacesIn( mc, c.Centre );
		mc.Mesh.RemoveFaces( mc.Mesh.FaceHandles.Where( f => shieldFaces.Contains( f ) != keepShield ).ToList() );
		return go;
	}

	/// <summary>
	/// A column's shield gone, or back. LOCAL. Gone splits it out first (<see cref="SplitColumn"/>), once; back, before any
	/// split, is already so — the map's own column stands.
	/// </summary>
	void SetColumnHidden( ShieldColumn c, bool hidden )
	{
		if ( !hidden && !c.Go.IsValid() && !Scene.GetAllObjects( false ).Any( x => x.Tags.Has( c.Tag ) ) ) return;

		var shield = SplitColumn( c );
		if ( !shield.IsValid() )
		{
			if ( hidden && !c.Missing ) Log.Warning( $"[nz-hex] the {c.Word} was not found, or would not split — it stays where it is" );
			c.Missing |= hidden;
			return;
		}

		if ( shield.Enabled == !hidden ) return;

		shield.Enabled = !hidden;
		Log.Info( $"[nz-hex] the {c.Word} {( hidden ? "is gone" : "is back" )}" );
		RebuildColumnNav( c );
	}

	/// <summary>
	/// The navmesh round a column built again from what stands there now — its shield gone, or back. HOST: the zombies walk
	/// the host's mesh.
	///
	/// ⛔ IT WAS GENERATED WITH THE COLUMNS STANDING, so without this no zombie could walk in where a shield stood — the altar
	/// defense's wave would crowd the edge of the lock's column, out of reach of the altar (`HexPlatforms.Defense.cs`).
	/// `RequestTilesGeneration` is how the debris open their doorways (`DebrisManager.RebuildNavAround`); tiles regenerate
	/// whole, so the box is padded well past a column's 60u.
	/// </summary>
	void RebuildColumnNav( ShieldColumn c )
	{
		if ( NZGame.IsClient || !Scene.IsValid() ) return;

		var nav = Scene.NavMesh;
		if ( nav is null ) return;

		var extent = new Vector3( 256f, 256f, 200f );
		nav.RequestTilesGeneration( new BBox( c.Centre - extent, c.Centre + extent ) );
		Log.Info( $"[nz-hex] the navmesh round the {c.Word}'s column is being built again" );
	}

	/// <summary>Is a column's shield there on this machine? For the selftest and the commands.</summary>
	bool ColumnStands( ShieldColumn c )
	{
		if ( c.Go.IsValid() ) return c.Go.Enabled;

		var (mc, faces) = ColumnFaces( c );
		return mc.IsValid() && faces is { Count: > 0 } && mc.GameObject.Enabled;
	}
}