Diagnostics/UnderFoot.cs

Debugging utility for the game that answers what the player is standing on and nearby invisible colliders. Provides two console commands nz_under and nz_unclip. nz_under traces below the player, inspects controller ground, body sweep, renderer state, same-model 'twins', and finds nearby non-drawing colliders; nz_unclip removes named invisible (non-drawing) objects from the running scene.

File Access
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// `nz_under [radius]` — WHAT YOU ARE STANDING ON, AND WHAT ELSE LIKE IT IS AROUND YOU (2026-10-05). The user, on Live Wire:
/// *"what surface am i currently on? its invisible for me, and theres one just like it a bit to the side"*.
///
/// ⚠️ INVISIBLE AND SOLID IS USUALLY A CLIP BRUSH. The BSP importer keeps a Source map's clip brushes (player clips) as hidden
/// meshes with mesh collision, under "Clip Brushes" — 419 of them on Live Wire. Straight down from your feet this names the
/// object, its path and components, whether anything draws it, and the surface; then a grid of downward traces around you
/// lists every other surface that collides but draws nothing, nearest first, with its size and where it is from you.
/// </summary>
public static class UnderFoot
{
	[ConCmd( "nz_under" )]
	public static void Under( float radius = 400f )
	{
		var scene = Game.ActiveScene;
		var bodies = PlayerSpawner.AllBodies();
		var me = bodies.FirstOrDefault( PlayerPresence.Mine ) ?? bodies.FirstOrDefault();
		if ( !scene.IsValid() || !me.IsValid() )
		{
			Log.Warning( "[nz-under] no player — in play?" );
			return;
		}

		var feet = me.WorldPosition;
		var ctrl = me.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
		var facing = Rotation.FromYaw( ctrl.IsValid() ? ctrl.EyeAngles.yaw : me.WorldRotation.Yaw() );

		// ⛔ THE CONTROLLER'S OWN ANSWER FIRST, THEN YOUR BODY'S BOX. A ray from your centre misses what you rest on whenever
		// it is under the edge of your box, or only collides with players: on Live Wire it hit a floor 32 u BELOW the feet
		// while the user stood on something invisible (2026-10-05). `TraceBody` sweeps the body's own box with the same
		// collision rules the movement uses.
		if ( ctrl.IsValid() )
		{
			var g = ctrl.GroundObject;
			Log.Info( $"[nz-under] the controller says you stand on: {(g.IsValid() ? $"'{PathOf( g )}'" : "NOTHING (in the air)")}"
				+ (g.IsValid() ? $" · {(Draws( g ) ? "DRAWN" : "draws NOTHING — invisible")} · collider {ctrl.GroundComponent?.GetType().Name ?? "-"}"
					+ $" · surface '{ctrl.GroundSurface?.ResourceName ?? "-"}' · tags [{string.Join( ",", g.Tags.TryGetAll() )}]" : "") );
			if ( g.IsValid() ) Log.Info( $"[nz-under]   components: {Describe( g )}" );
			if ( ctrl.GroundComponent is Collider gc && gc.IsValid() )
			{
				var bb = gc.GetWorldBounds();
				Log.Info( $"[nz-under]   its bounds {Fmt( bb.Mins )}..{Fmt( bb.Maxs )} · {bb.Size.x:0} × {bb.Size.y:0} × {bb.Size.z:0} u"
					+ $" · its top is {bb.Maxs.z - feet.z:+0;-0} u from your feet" );
			}

			var body = ctrl.TraceBody( feet + Vector3.Up * 4f, feet + Vector3.Down * 64f, 1f, 1f );
			Log.Info( body.Hit && body.GameObject.IsValid()
				? $"[nz-under] your body's box comes to rest on '{PathOf( body.GameObject )}' at {Fmt( body.HitPosition )}"
					+ $" ({MathF.Max( 0f, body.Distance - 4f ):0} u down) · {(Draws( body.GameObject ) ? "DRAWN" : "draws NOTHING — invisible")}"
					+ $" · surface '{body.Surface?.ResourceName ?? "-"}'"
				: "[nz-under] your body's box finds nothing within 64 u below" );
		}

		var tr = scene.Trace.Ray( feet + Vector3.Up * 24f, feet + Vector3.Down * 256f ).IgnoreGameObjectHierarchy( me ).Run();
		if ( !tr.Hit || !tr.GameObject.IsValid() )
		{
			Log.Info( $"[nz-under] nothing within 256 u straight below your centre at {Fmt( feet )}" );
			return;
		}

		Log.Info( $"[nz-under] straight below your centre: '{PathOf( tr.GameObject )}' at {Fmt( tr.HitPosition )}"
			+ $" ({tr.Distance - 24f:0} u below your feet) · {(Draws( tr.GameObject ) ? "DRAWN" : "draws NOTHING — invisible")}" );
		Log.Info( $"[nz-under]   components: {Describe( tr.GameObject )} · surface '{tr.Surface?.ResourceName ?? "-"}'"
			+ $" · tags [{string.Join( ",", tr.GameObject.Tags.TryGetAll() )}]" );
		Renderer( tr.GameObject );

		// ⚠️ ITS TWINS TOO: "one just like it a bit to the side" is the same model on another object, which the hidden-only
		// sweep below cannot see when its renderer claims to draw (the Live Wire turbines, 2026-10-05).
		if ( tr.GameObject.Components.Get<ModelRenderer>( FindMode.EverythingInSelf )?.Model is Model model )
		{
			var twins = scene.GetAllComponents<ModelRenderer>()
				.Where( r => r.IsValid() && r.Model == model && r.GameObject != tr.GameObject
					&& r.WorldPosition.Distance( feet ) < 2000f )
				.OrderBy( r => r.WorldPosition.Distance( feet ) ).Take( 4 ).ToList();
			Log.Info( $"[nz-under]   the same model elsewhere within 2000 u: {twins.Count}" );
			foreach ( var t in twins )
			{
				Log.Info( $"[nz-under]   twin '{PathOf( t.GameObject )}' at {Fmt( t.WorldPosition )}" );
				Renderer( t.GameObject );
			}
		}

		// Others like it: every collider around you that draws nothing, from a PHYSICS OVERLAP, with its exact bounds.
		// ⛔ NOT RAYS ON A GRID, the first cut: 24 u apart, they stepped right over Clip Brush 83, an 8 u-thick wall (2026-10-05).
		bool Mine( GameObject o )
		{
			for ( var g = o; g.IsValid(); g = g.Parent ) if ( g == me ) return true;
			return false;
		}

		var area = BBox.FromPositionAndSize( feet + Vector3.Up * 32f, new Vector3( radius * 2f, radius * 2f, 320f ) );
		var near = new List<(GameObject Go, BBox B, float D)>();
		foreach ( var o in scene.FindInPhysics( area ).Distinct() )
		{
			if ( !o.IsValid() || Mine( o ) || Draws( o ) ) continue;
			var col = o.Components.Get<Collider>( FindMode.EverythingInSelf );
			if ( !col.IsValid() ) continue;
			var b = col.GetWorldBounds();
			near.Add( (o, b, DistanceTo( b, feet )) );
		}

		Log.Info( $"[nz-under] colliders within {radius:0} u that draw nothing: {near.Count}" + (near.Count > 12 ? " (nearest 12)" : "") );
		foreach ( var (go, b, d) in near.OrderBy( x => x.D ).Take( 12 ) )
		{
			var off = b.Center - feet;
			var ahead = Vector3.Dot( off, facing.Forward );
			var right = Vector3.Dot( off, facing.Right );
			Log.Info( $"[nz-under]   '{go.Name}'{(ctrl.IsValid() && go == ctrl.GroundObject ? "  ← you are on this" : "")}"
				+ $" · {b.Size.x:0} × {b.Size.y:0} × {b.Size.z:0} u, top at z {b.Maxs.z:0} ({MathF.Round( b.Maxs.z - feet.z ):+0;-0;0} u from your feet)"
				+ $" · nearest point {d:0} u away · centre {MathF.Abs( ahead ):0} u {(ahead >= 0 ? "ahead" : "behind")},"
				+ $" {MathF.Abs( right ):0} u to your {(right >= 0 ? "right" : "left")} · under '{go.Parent?.Name ?? "-"}'" );
		}
	}

	/// <summary>How far a point is from the nearest point of a box (0 inside it).</summary>
	static float DistanceTo( BBox b, Vector3 p )
		=> new Vector3( Math.Clamp( p.x, b.Mins.x, b.Maxs.x ), Math.Clamp( p.y, b.Mins.y, b.Maxs.y ),
			Math.Clamp( p.z, b.Mins.z, b.Maxs.z ) ).Distance( p );

	/// <summary>
	/// `nz_unclip "Clip Brush 83"` — take a named invisible clip out of the RUNNING map, so a removal made in the map scene can
	/// be felt at once instead of after a reload (2026-10-05, Live Wire). LIVE ONLY: the map scene file is what keeps it out.
	/// Refuses anything that draws, so a typo cannot delete a visible piece of the map.
	/// </summary>
	[ConCmd( "nz_unclip" )]
	public static void Unclip( string name = "" )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() || string.IsNullOrWhiteSpace( name ) )
		{
			Log.Warning( "[nz-under] nz_unclip \"<object name>\" — e.g. \"Clip Brush 83\" (nz_under names them)" );
			return;
		}

		var hits = scene.GetAllObjects( true ).Where( o => o.IsValid() && o.Name == name ).ToList();
		if ( hits.Count == 0 )
		{
			Log.Warning( $"[nz-under] nothing named '{name}' in the running scene" );
			return;
		}

		foreach ( var o in hits )
		{
			if ( Draws( o ) )
			{
				Log.Warning( $"[nz-under] '{PathOf( o )}' draws something — not removing a visible object" );
				continue;
			}

			Log.Info( $"[nz-under] removed '{PathOf( o )}' from the running map (live only — the map scene keeps it out for good)" );
			o.Destroy();
		}
	}

	/// <summary>
	/// What this object's model renderer is doing right now: its model (error? how many meshes? bounds), its scene object
	/// (does it exist, is rendering on, its bounds and tint), and its materials — the questions behind "it collides but I
	/// cannot see it".
	/// </summary>
	static void Renderer( GameObject go )
	{
		var r = go.Components.Get<ModelRenderer>( FindMode.EverythingInSelf );
		if ( !r.IsValid() )
		{
			Log.Info( "[nz-under]     no ModelRenderer on it" );
			return;
		}

		var m = r.Model;
		var so = r.SceneObject;
		Log.Info( $"[nz-under]     renderer enabled {r.Enabled} · type {r.RenderType} · tint {r.Tint} · object scale {go.WorldScale}"
			+ $" · tags [{string.Join( ",", go.Tags.TryGetAll() )}]" );
		Log.Info( $"[nz-under]     model '{m?.ResourcePath ?? "none"}' · error {m?.IsError} · meshes {m?.MeshCount}"
			+ $" · bounds {(m is null ? "-" : $"{Fmt( m.Bounds.Mins )}..{Fmt( m.Bounds.Maxs )}")}"
			+ $" · render bounds {(m is null ? "-" : $"{Fmt( m.RenderBounds.Mins )}..{Fmt( m.RenderBounds.Maxs )}")}" );
		Log.Info( $"[nz-under]     scene object {(so.IsValid() ? $"rendering {so.RenderingEnabled} · bounds {Fmt( so.Bounds.Mins )}..{Fmt( so.Bounds.Maxs )} · tint {so.ColorTint}" : "NONE")}"
			+ $" · materials [{(m is null ? "" : string.Join( ", ", m.Materials.Select( x => x?.ResourcePath ?? "null" ) ))}]"
			+ $" · override '{r.MaterialOverride?.ResourcePath ?? "-"}'" );
	}

	/// <summary>Does anything on this object draw? A mesh not hidden in game, or a model renderer that is not shadow-only.</summary>
	static bool Draws( GameObject go )
	{
		foreach ( var c in go.Components.GetAll() )
		{
			if ( c is MeshComponent m && m.Enabled && !m.HideInGame ) return true;
			if ( c is ModelRenderer r && r.Enabled && r.Model is not null
				&& r.RenderType != ModelRenderer.ShadowRenderType.ShadowsOnly ) return true;
		}
		return false;
	}

	static string Describe( GameObject go )
		=> string.Join( ", ", go.Components.GetAll().Select( c => c is MeshComponent m
			? $"MeshComponent (hidden in game: {m.HideInGame}, collision {m.Collision})"
			: c.GetType().Name ) );

	static string PathOf( GameObject go )
	{
		var names = new List<string>();
		for ( var g = go; g.IsValid() && g is not Scene; g = g.Parent ) names.Insert( 0, g.Name );
		return string.Join( " / ", names );
	}

	static Vector3 Centre( (float MinX, float MaxX, float MinY, float MaxY, float Top, int Hits) e )
		=> new( (e.MinX + e.MaxX) * 0.5f, (e.MinY + e.MaxY) * 0.5f, e.Top );

	static string Fmt( Vector3 v ) => $"({v.x:0}, {v.y:0}, {v.z:0})";
}