A static utility with two console commands for debugging bullet impact decals. nz_impact_check traces from the player to report the surface, impact prefab, bullet sound and counts live bullet_decal objects. nz_impact_test traces and calls the weapon system to spawn a bullet impact at the hit point, logging the result.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// Does the thing you are looking at supply a bullet hole?
///
/// ⛔ DECALS ARE NOT DRAWN BY THE WEAPON — THEY COME FROM THE SURFACE.
/// `Weapon.CreateBulletImpact` clones `tr.Surface.PrefabCollection.BulletImpact` and
/// returns early when there is none. So "no bullet holes" has two completely
/// different causes with one symptom: the impact code never ran, or it ran and the
/// surface had nothing to give. On a ported BSP most faces resolve to `default`,
/// which may or may not carry an impact prefab — and guessing which costs a round of
/// building the wrong fix.
/// </summary>
public static class BulletImpactCheck
{
/// <summary>
/// What the crosshair is on, and whether it can produce a hole:
/// `nz_impact_check`.
/// </summary>
[ConCmd( "nz_impact_check" )]
public static void Check()
{
var player = NZPlayer.Local;
var controller = player?.Components.Get<PlayerController>();
if ( !player.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var eye = controller?.EyePosition ?? player.WorldPosition + Vector3.Up * 64f;
var fwd = (controller?.EyeAngles.ToRotation() ?? player.WorldRotation).Forward;
var tr = Game.ActiveScene.Trace.Ray( eye, eye + fwd * 4096f )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
if ( !tr.Hit ) { Log.Info( "[nz] impact: nothing in front of you" ); return; }
var surf = tr.Surface;
var impact = surf?.PrefabCollection.BulletImpact;
var sound = surf?.SoundCollection.Bullet;
Log.Info( $"[nz] impact @ {tr.Distance:0}u — {tr.GameObject?.Name}" );
Log.Info( $"[nz] surface '{surf?.ResourceName ?? "NONE"}'" );
Log.Info( $"[nz] BulletImpact {(impact.IsValid() ? impact.Name : "NULL — no decal possible")}" );
Log.Info( $"[nz] bullet sound {(sound is null ? "none (falls back to generic)" : sound.ResourceName)}" );
// ⚠️ Counts what is actually in the scene, because "the code ran" and "a hole
// is on the wall" are still different claims — the clone could be spawning and
// being destroyed, or spawning somewhere else entirely.
var live = Game.ActiveScene?.GetAllObjects( true )
.Count( o => o.Name == "bullet_decal" ) ?? 0;
Log.Info( $"[nz] decals alive in scene: {live} (each lasts 30s)" );
if ( !impact.IsValid() )
Log.Warning( "[nz] -> this surface cannot make holes; a fallback decal is needed" );
}
/// <summary>
/// Fire one bullet's worth of impact where you are looking: `nz_impact_test`.
///
/// ⚠️ Calls the SAME method the bullet path calls, so a hole appearing here proves
/// the decal pipeline end to end without needing to hit anything while shooting.
/// </summary>
[ConCmd( "nz_impact_test" )]
public static void Test()
{
var player = NZPlayer.Local;
var controller = player?.Components.Get<PlayerController>();
if ( !player.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var eye = controller?.EyePosition ?? player.WorldPosition + Vector3.Up * 64f;
var fwd = (controller?.EyeAngles.ToRotation() ?? player.WorldRotation).Forward;
var tr = Game.ActiveScene.Trace.Ray( eye, eye + fwd * 4096f )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
if ( !tr.Hit ) { Log.Info( "[nz] nothing to hit" ); return; }
var go = SWB.Base.Weapon.CreateBulletImpact( tr );
Log.Info( go.IsValid()
? $"[nz] decal spawned '{go.Name}' at {tr.HitPosition} on '{tr.Surface?.ResourceName}'"
: $"[nz] NO decal — surface '{tr.Surface?.ResourceName}' has no BulletImpact prefab" );
}
}