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.
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;
}
}