Console command bindings for debugging and managing barricade entities. Implements placement, listing, tearing, repairing, opening, resetting, clearing, navmesh crossing checks/rebuilds, and a diagnostic for drop-side correction; mostly calls into Barricade and scene APIs.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// BARRICADE/COMMANDS — place, inspect and drive barricades from the console.
///
/// ⚠️ STANDING RULE: every button gets a command. There is no placement TOOL yet,
/// so right now these are the only way to make one at all — which is deliberate
/// ordering: the entity can be proven correct before a tool is built on top of it.
/// </summary>
public static class BarricadeCommands
{
/// <summary>
/// Place one where you are looking: `nz_barricade [distance]`.
///
/// ⚠️ Faces the PLAYER, like the original's tool does
/// (`Angle(0, (tr.HitPos - ply:GetPos()):Angle()[2], 0)` in
/// sh_tools_barricade.lua) — a window you approach head-on.
/// </summary>
[ConCmd( "nz_barricade" )]
public static void Place( float distance = 100f )
{
var scene = Game.ActiveScene;
var player = NZPlayer.Local;
if ( !player.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var controller = player.Components.Get<PlayerController>();
var eye = controller?.EyePosition ?? player.WorldPosition + Vector3.Up * 64f;
var rot = controller?.EyeAngles.ToRotation() ?? player.WorldRotation;
var tr = scene.Trace.Ray( eye, eye + rot.Forward * (distance * 4f ) )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
var at = tr.Hit ? tr.HitPosition : eye + rot.Forward * distance;
// Drop to the floor so the frame sits in the opening rather than hanging.
var down = scene.Trace.Ray( at + Vector3.Up * 96f, at - Vector3.Up * 4096f )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
if ( down.Hit ) at = down.HitPosition;
var go = scene.CreateObject();
go.Name = "Barricade";
go.WorldPosition = at;
// ⚠️ Yaw only — a barricade tilted to match a sloped floor would have its
// boards running downhill.
var yaw = (player.WorldPosition - at).WithZ( 0 ).EulerAngles.yaw;
go.WorldRotation = Rotation.From( 0f, yaw, 0f );
go.Components.Create<Barricade>();
Log.Info( $"[nz] barricade placed at {at} facing {yaw:0}° "
+ $"({Barricade.All.Count} total)" );
}
/// <summary>What is placed and what state it is in: `nz_barricades`.</summary>
[ConCmd( "nz_barricades" )]
public static void List()
{
if ( Barricade.All.Count == 0 ) { Log.Info( "[nz] no barricades placed" ); return; }
var player = NZPlayer.Local;
foreach ( var b in Barricade.All.Where( x => x.IsValid() ) )
{
var dist = player.IsValid()
? $"{b.WorldPosition.Distance( player.WorldPosition ):0}u away"
: "";
Log.Info( $"[nz] {b.Planks}/{Barricade.MaxPlanks} boards "
+ $"{(b.BlocksPlayer ? "blocks player" : "PASSABLE")} {dist}" );
}
}
/// <summary>Tear one board off the nearest: `nz_barricade_tear`.</summary>
[ConCmd( "nz_barricade_tear" )]
public static void Tear()
{
var b = Nearest();
if ( b is null ) return;
Log.Info( b.TearPlank()
? $"[nz] tore a board — {b.Planks}/{Barricade.MaxPlanks} left"
: "[nz] already open" );
}
/// <summary>Repair the nearest, paying the player: `nz_barricade_repair`.</summary>
[ConCmd( "nz_barricade_repair" )]
public static void RepairNearest()
{
var b = Nearest();
if ( b is null ) return;
var player = NZPlayer.Local;
var msg = b.Repair( player );
// ⚠️ An empty message means the per-board cooldown swallowed it, which is
// correct behaviour and not worth a line — say so rather than printing
// nothing, or the command looks broken.
Log.Info( string.IsNullOrEmpty( msg )
? $"[nz] on cooldown — one board per {b.RepairInterval:0.##}s"
: $"[nz] {msg}" );
}
/// <summary>Open the nearest completely: `nz_barricade_open`.</summary>
[ConCmd( "nz_barricade_open" )]
public static void Open()
{
var b = Nearest();
if ( b is null ) return;
b.Clear();
Log.Info( "[nz] barricade opened (0 boards)" );
}
/// <summary>Re-board every barricade: `nz_barricade_reset`.</summary>
[ConCmd( "nz_barricade_reset" )]
public static void ResetAll()
{
foreach ( var b in Barricade.All.Where( x => x.IsValid() ) )
b.Reboard();
Log.Info( $"[nz] reset {Barricade.All.Count} barricade(s) to full" );
}
/// <summary>Delete them all: `nz_barricade_clear`.</summary>
[ConCmd( "nz_barricade_clear" )]
public static void ClearAll()
{
int n = Barricade.All.Count;
foreach ( var b in Barricade.All.ToList() )
b?.GameObject?.Destroy();
Log.Info( $"[nz] removed {n} barricade(s)" );
}
static Barricade Nearest()
{
var player = NZPlayer.Local;
if ( !player.IsValid() ) { Log.Warning( "[nz] no player" ); return null; }
var b = Barricade.All
.Where( x => x.IsValid() )
.OrderBy( x => x.WorldPosition.DistanceSquared( player.WorldPosition ) )
.FirstOrDefault();
if ( b is null ) Log.Warning( "[nz] no barricades placed — nz_barricade to make one" );
return b;
}
/// <summary>
/// `nz_barr_cross` — did every barricade publish its crossing to the navmesh?
///
/// ⛔ THE CHECK THAT MATTERS AFTER A CONFIG LOAD. A crossing that failed to build leaves that
/// window carved out of the navmesh with nothing bridging it, which is invisible until a zombie
/// refuses to come through and looks exactly like it is "being stupid". The retry in
/// Barricade.OnUpdate should make failures temporary, so a persistent `no` here is a real
/// problem — most likely no walkable floor within CrossOffset of one side.
/// </summary>
[ConCmd( "nz_barr_cross" )]
public static void CrossReport()
{
var all = Barricade.All.Where( b => b.IsValid() ).ToList();
if ( all.Count == 0 ) { Log.Warning( "[nz-barr] no barricades standing" ); return; }
var nav = Game.ActiveScene?.NavMesh;
if ( nav is null || !nav.IsEnabled )
Log.Warning( "[nz-barr] scene navmesh is not enabled — no crossing can build" );
else if ( nav.IsGenerating )
Log.Info( "[nz-barr] navmesh is still generating — retries are in progress" );
Log.Info( $"[nz-barr] {"n",3} {"linked",7} {"valid",6} {"planks",7} {"offset",7} crossing" );
int linked = 0;
for ( int i = 0; i < all.Count; i++ )
{
var b = all[i];
if ( b.NavLinked ) linked++;
var where = b.CrossValid
? $"{b.CrossA} <-> {b.CrossB}"
: "(not built — no navmesh under one side?)";
Log.Info( $"[nz-barr] {i,3} {(b.NavLinked ? "yes" : "NO"),7} {(b.CrossValid ? "yes" : "NO"),6}"
+ $" {b.Planks,3}/{Barricade.MaxPlanks,-3} {b.CrossOffset,7:0} {where}" );
}
Log.Info( $"[nz-barr] {linked}/{all.Count} crossings in the navmesh" );
if ( linked < all.Count )
Log.Warning( "[nz-barr] the missing ones are carved-out windows with no route through."
+ " nz_barr_recross to force a retry now." );
}
/// <summary>`nz_barr_recross` — rebuild every crossing and its link, now.</summary>
[ConCmd( "nz_barr_recross" )]
public static void Recross()
{
int ok = 0, n = 0;
foreach ( var b in Barricade.All.Where( x => x.IsValid() ) )
{
n++;
if ( b.BuildNavLink() ) ok++;
}
Log.Info( $"[nz-barr] {ok}/{n} crossings rebuilt" );
if ( ok < n ) Log.Warning( "[nz-barr] nz_barr_cross to see which failed and where" );
}
/// <summary>
/// `nz_drop_side [distance] [spawn]` — prove the through-the-window drop correction
/// without needing a zombie to die at a window.
///
/// ⛔ EXISTS BECAUSE THE BUG IS ONLY VISIBLE FROM THE WRONG SIDE OF A WALL.
/// Reproducing it for real means holding a window, letting a zombie climb and killing
/// it on the right frame — and the failure then looks identical to "the drop roll did
/// not fire", because in both cases you see nothing.
/// </summary>
[ConCmd( "nz_drop_side" )]
public static void DropSide( float distance = 80f, int spawn = 1 )
{
var player = NZPlayer.Local;
if ( !player.IsValid() ) { Log.Warning( "[nz-drop] no player" ); return; }
var b = Game.ActiveScene.GetAllComponents<Barricade>()
.Where( x => x.IsValid() )
.OrderBy( x => x.DistanceToRun( player.WorldPosition ) )
.FirstOrDefault();
if ( !b.IsValid() ) { Log.Warning( "[nz-drop] no barricades placed" ); return; }
var side = b.DropSideOf( player.WorldPosition );
if ( side == 0 )
{
Log.Warning( "[nz-drop] you are standing IN the window plane — step back, the "
+ "correction has no side to aim at from here" );
return;
}
// ⚠️ THE TEST POINT MUST BE INSIDE THE REACH THE REAL PATH USES, or this reports a
// failure that is really "no window near there" — the correct answer, and the most
// misleading possible output from a command whose job is to say whether this works.
if ( distance > Barricade.DropReach )
{
Log.Warning( $"[nz-drop] distance {distance:0} is beyond the {Barricade.DropReach:0}u "
+ "reach ResolveDrop uses — that would report a false failure. Try 80." );
return;
}
var n = b.RunNormal.WithZ( 0f ).Normal;
var centre = (b.RunA + b.RunB) * 0.5f;
var outside = centre + n * (-side * distance);
var fixedAt = Barricade.ResolveDrop( outside );
Log.Info( $"[nz-drop] '{b.GameObject.Name}' · {b.Planks}/{Barricade.MaxPlanks} planks"
+ $" · inset {Barricade.DropInset:0}u · reach {Barricade.DropReach:0}u" );
Log.Info( $"[nz-drop] player side {side} · death {outside} side {b.DropSideOf( outside )}" );
Log.Info( $"[nz-drop] drop {fixedAt} side {b.DropSideOf( fixedAt )}"
+ $" moved {fixedAt.Distance( outside ):0.#}u" );
// ⛔ THE VERDICT IS THE SIDE, NOT THE DISTANCE. A correction that moved the point a
// long way but left it outside is a failure that "moved 140u" would read as success.
if ( b.DropSideOf( fixedAt ) == side )
Log.Info( "[nz-drop] OK — landed on the player's side" );
else
Log.Warning( "[nz-drop] FAIL — still on the wrong side" );
if ( spawn > 0 )
{
Powerup.Spawn( fixedAt, PowerupKind.MaxAmmo );
Log.Info( "[nz-drop] spawned a Max Ammo there · nz_drop_side 80 0 to skip" );
}
}
}