Console command helpers for the invisible-wall editor tool. Exposes convars to set the next-placed wall properties, list/remove/clear/rebuild walls, toggle visibility and whether a wall blocks zombies, tilt walls, and inspect navmesh exclusion status.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// WALLS/COMMANDS — the invisible wall tool, without a mouse.
///
/// ⚠️ This tool needs console access more than any other, for a reason specific
/// to it: its output is INVISIBLE by default. "Did that work?" cannot be
/// answered by looking, so `nz_wall_list` is not a convenience here, it is the
/// only way to check a wall exists at all from outside creative.
/// </summary>
public static class WallCommands
{
static MapEditor Editor
{
get
{
var p = NZPlayer.Local;
return p?.Components.GetOrCreate<MapEditor>();
}
}
static InvisibleWallManager Manager => InvisibleWallManager.Ensure( Game.ActiveScene );
/// <summary>What the tool places next: nz_wall_set [visible] [material].</summary>
[ConCmd( "nz_wall_set" )]
public static void Set( bool visible = false, string material = "" )
{
var ed = Editor;
if ( !ed.IsValid() ) { Log.Warning( "[nz] no MapEditor" ); return; }
ed.WallVisible = visible;
if ( !string.IsNullOrWhiteSpace( material ) ) ed.WallMaterial = material;
Log.Info( $"[nz] next invisible wall: {(visible ? "VISIBLE" : "invisible")}"
+ $", {ed.DebrisCorners} corners then a height click"
+ (visible ? $", {ed.WallMaterial}" : "") );
}
/// <summary>
/// Show or hide one that already exists: nz_wall_vis <index> [visible].
///
/// ⚠️ Edits the CONFIG, so it is a real change that needs saving — not a
/// view setting. Toggling every wall on to see where they are and then
/// saving would ship a map full of grey slabs.
/// </summary>
[ConCmd( "nz_wall_vis" )]
public static void SetVisible( int index = 0, bool visible = true )
{
var m = Manager;
if ( m is null ) { Log.Warning( "[nz] no scene" ); return; }
if ( !m.SetVisible( index, visible ) )
{
Log.Warning( $"[nz] no invisible wall #{index}" );
return;
}
Log.Info( $"[nz] wall #{index} -> {(visible ? "visible" : "invisible")}"
+ " (unsaved — nz_save to keep it)" );
}
/// <summary>
/// Does the horde stop at this wall too? nz_wall_zombies <index> [on].
///
/// ⚠️ OFF IS THE DEFAULT AND USUALLY RIGHT. An invisible wall is normally a map
/// boundary keeping the PLAYER in; zombies spawn inside it anyway. Turning this on
/// bakes the wall into the navmesh so the horde routes around it — useful for fencing
/// off a route, and a real risk if that route was one they depended on.
///
/// ⚠️ Edits the CONFIG, like nz_wall_vis. nz_save to keep it.
/// </summary>
[ConCmd( "nz_wall_zombies" )]
public static void SetBlocksZombies( int index = 0, bool on = true )
{
var m = Manager;
if ( m is null ) { Log.Warning( "[nz] no scene" ); return; }
if ( !m.SetBlocksZombies( index, on ) )
{
Log.Warning( $"[nz] no invisible wall #{index}" );
return;
}
Log.Info( $"[nz] wall #{index} -> {(on ? "BLOCKS zombies (baked into the navmesh)" : "player-only (zombies walk through)")}"
+ " (unsaved — nz_save to keep it)" );
}
/// <summary>Every wall in the config, with what it is and whether it stands.</summary>
[ConCmd( "nz_wall_list" )]
public static void List()
{
var list = ActiveConfig.Current.InvisibleWalls;
if ( list.Count == 0 )
{
Log.Info( "[nz] no invisible walls placed" );
return;
}
var m = InvisibleWallManager.Instance;
for ( int i = 0; i < list.Count; i++ )
{
var w = list[i];
Log.Info( $"[nz] wall #{i} {(w.Visible ? "VISIBLE " : "invisible")}"
+ $" {Shape( w )}"
+ (MathF.Abs( w.Pitch ) > 0.5f || MathF.Abs( w.Roll ) > 0.5f
? $" tilt {w.Pitch:0.#}°/{w.Roll:0.#}°"
: "")
+ $" at {w.Position}"
+ (m is null ? " (no manager)" : m.IsStanding( i ) ? " standing" : " NOT STANDING") );
}
Log.Info( $"[nz] {list.Count} invisible wall(s) — solid, none block the navmesh" );
}
/// <summary>
/// What a wall is, in the words nz_wall_list uses. Shared with nz_wall_remove and RMB so
/// all three describe one wall the same way.
///
/// ⚠️ THE WIDTH IS THE POINT. A drawn wall used to list as "4-sided, 283 tall" and
/// nothing else, so a 550x558 block drawn by mistake read exactly like a 68x44 post.
/// </summary>
public static string Shape( InvisibleWall w )
=> w.HasFootprint
? $"{w.Footprint.Count}-sided, {w.Size.x:0}x{w.Size.y:0} across, {w.Size.z:0} tall"
: $"{w.Size.x:0}x{w.Size.y:0}x{w.Size.z:0}";
/// <summary>
/// Delete one wall by the number nz_wall_list gives it: nz_wall_remove <index>.
///
/// ⛔ THE WAY OUT FOR A WALL RMB CANNOT REACH. RMB takes the wall the crosshair is on, or
/// the one whose centre marker is near it; a wall you are standing inside, or one buried in
/// the map, gives it neither. Before this the only command that removed a wall was
/// nz_wall_clear, which removes every one of them.
///
/// ⚠️ THE WALLS AFTER IT MOVE DOWN ONE NUMBER. Remove #3 and the old #4 is #3 now, so run
/// nz_wall_list again before removing a second — the next number off the old list is the
/// wrong wall.
///
/// ⚠️ IN MEMORY ONLY UNTIL `nz_save`, like every other wall command here.
/// </summary>
[ConCmd( "nz_wall_remove" )]
public static void Remove( int index = -1 )
{
var list = ActiveConfig.Current.InvisibleWalls;
if ( index < 0 || index >= list.Count )
{
Log.Warning( $"[nz] no wall #{index} — nz_wall_list shows the numbers"
+ (list.Count > 0 ? $" (0 to {list.Count - 1})" : " (there are none)") );
return;
}
var w = list[index];
list.RemoveAt( index );
// Rebuild, not one Destroy — the manager keys its objects by number, and every wall
// after this one has just changed number. Same reason as nz_wall_clear.
Manager?.Rebuild();
Log.Info( $"[nz] wall #{index} removed: {Shape( w )} at {w.Position}"
+ $" ({list.Count} left, the ones after it moved down a number)"
+ " (unsaved — nz_save to keep it)" );
}
/// <summary>
/// Tilt a wall so it can be a ramp: `nz_wall_tilt <index> <pitch> [roll]`.
/// </summary>
///
/// ⚠️ A FLAT SLAB IS A STEP AND A TILTED ONE IS A ROUTE, which is the whole reason the field
/// exists — but only up to the map's `Nav.MaxSlope`. Past that the navmesh puts no walkable
/// polygon on it however solid it is, and the horde treats a perfectly good ramp as a wall.
/// The command says so rather than letting it be discovered in a play test.
///
/// ⚠️ IN MEMORY ONLY UNTIL `nz_save`, like every other wall command here.
[ConCmd( "nz_wall_tilt" )]
public static void Tilt( int index, float pitch, float roll = 0f )
{
var list = ActiveConfig.Current.InvisibleWalls;
if ( index < 0 || index >= list.Count )
{
Log.Warning( $"[nz] no wall #{index} — nz_wall_list" );
return;
}
var w = list[index];
w.Pitch = pitch;
w.Roll = roll;
InvisibleWallManager.Ensure( Game.ActiveScene )?.Rebuild();
var max = ActiveConfig.Current?.Nav?.MaxSlope ?? 45f;
if ( max > 0f && MathF.Abs( pitch ) > max )
Log.Warning( $"[nz] wall #{index} tilted to {pitch:0.#}° — STEEPER THAN MaxSlope"
+ $" ({max:0}°), so the navmesh will not make it walkable" );
else
Log.Info( $"[nz] wall #{index} tilted to {pitch:0.#}°/{roll:0.#}°"
+ " (unsaved — nz_save to keep it)" );
}
/// <summary>Delete them all. ⚠️ In-memory only until nz_save.</summary>
[ConCmd( "nz_wall_clear" )]
public static void ClearAll()
{
var n = ActiveConfig.Current.InvisibleWalls.Count;
ActiveConfig.Current.InvisibleWalls.Clear();
// Rebuild, not Clear — the manager owns GameObjects that would otherwise
// stand in the world with nothing in the config pointing at them, which
// is a wall nobody can select or remove.
Manager?.Rebuild();
Log.Info( $"[nz] {n} invisible wall(s) removed (unsaved — nz_save to keep it)" );
}
/// <summary>
/// Rebuild them from the config: nz_wall_rebuild.
///
/// Also re-applies the navmesh exclusion tag, which is the thing to reach for
/// if zombies ever start refusing a route near a wall.
/// </summary>
[ConCmd( "nz_wall_rebuild" )]
public static void RebuildAll()
{
var m = Manager;
if ( m is null ) { Log.Warning( "[nz] no scene" ); return; }
m.Rebuild();
}
/// <summary>
/// Is the navmesh actually ignoring them? nz_wall_nav.
///
/// ⛔ WORTH A COMMAND OF ITS OWN because the failure is silent and delayed:
/// a wall that HAS been baked into the navmesh looks exactly like one that
/// has not until a zombie declines a route, possibly rounds later.
/// </summary>
[ConCmd( "nz_wall_nav" )]
public static void NavStatus()
{
var nav = Game.ActiveScene?.NavMesh;
if ( nav is null ) { Log.Warning( "[nz] no navmesh on this scene" ); return; }
bool excluded = nav.ExcludedBodies.Has( InvisibleWallManager.NavIgnoreTag );
var walls = ActiveConfig.Current.InvisibleWalls;
var blocking = walls.Count( w => w.BlocksZombies );
Log.Info( $"[nz] navmesh excludes '{InvisibleWallManager.NavIgnoreTag}': {excluded}"
+ $" — {walls.Count} wall(s) placed" );
// ⚠️ THE PER-WALL SPLIT, because the global tag says nothing about a wall that
// deliberately opted out of it. "Excluded: true" with a blocking wall present is
// correct and would otherwise read as the wall being broken.
Log.Info( $"[nz] {walls.Count - blocking} player-only (horde walks through)"
+ $" · {blocking} blocking (baked in, horde routes around)" );
for ( int i = 0; i < walls.Count; i++ )
if ( walls[i].BlocksZombies )
Log.Info( $"[nz] #{i} blocks zombies at {walls[i].Position}" );
if ( !excluded )
Log.Warning( "[nz] ⚠ walls are NOT excluded — they will be baked into the "
+ "navmesh the next time tiles regenerate near one. nz_wall_rebuild "
+ "re-applies the tag." );
}
}