Debug utility for taking diagnostic third-person and first-person photos and inspecting weapon/hand transforms. It creates a non-networked unsaved camera in the game scene and provides console commands to place it (nz_3p_photo, nz_3p_face, nz_3p_eye) and to print axes and IK diagnostics (nz_3p_axes, nz_3p_ik).
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// `nz_3p_photo` — A CAMERA OF ITS OWN ON YOUR BODY, for judging how a gun sits in the hand (2026-10-05). The user:
/// *"at the moment players are hardly holding their weapons, a lot of times the weapon is far from the player / To test this
/// grab a random sample of 10 weapons from different classes and packs, and render a player holding each"*.
///
/// ⛔ IN THE GAME, NOT THROUGH THE EDITOR'S SCENE TOOLS. In play the editor holds two copies of the scene with the SAME object
/// ids, so a camera made over MCP "in the scene" landed in the editor's copy and shot empty sky, and the player it found by
/// id was the editor's template body, 1,400 units up. This runs in `Game.ActiveScene`.
///
/// ⛔ ITS OWN CAMERA, NOT THE PLAYER'S. Moving the main camera with `UseCameraControls` off did nothing you could see: the
/// game hands the camera back to your controller every frame (`NZPlayers.Control`), so eleven shots came out as the same
/// third-person view from behind. This camera is a separate, unsaved, never-networked object that nothing else drives; it
/// is not the main camera, so your own view is untouched, and MCP's `camera_screenshot` shoots it by the id this prints.
///
/// nz_3p_photo spawn stand on the config's first player spawn, facing its way
/// nz_3p_photo [yaw] [dist] [height] place the photo camera on your body: `yaw` degrees round from your face toward your
/// gun hand (35), `dist` units out (80), `height` up from your feet (58)
/// nz_3p_photo off remove the photo camera
///
/// ⚠️ TURN THIRD PERSON ON FIRST (`nz_thirdperson 1`): in first person your own body is hidden from the cameras.
/// </summary>
public static class HoldPhoto
{
const string CameraName = "nz 3p photo camera";
[ConCmd( "nz_3p_photo" )]
public static void Photo( string what = "", float dist = 80f, float height = 58f )
{
var scene = Game.ActiveScene;
var bodies = PlayerSpawner.AllBodies();
var go = bodies.FirstOrDefault( PlayerPresence.Mine ) ?? bodies.FirstOrDefault();
var ctrl = go.IsValid() ? go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants ) : null;
var w = (what ?? "").Trim().ToLowerInvariant();
if ( w == "off" )
{
foreach ( var c in Existing( scene ) ) c.Destroy();
Log.Info( "[nz-3p] photo: camera removed" );
return;
}
if ( !go.IsValid() || !ctrl.IsValid() )
{
Log.Warning( "[nz-3p] photo: no body or player controller — in play, after Creative?" );
return;
}
if ( w == "spawn" )
{
// ⛔ THE CONFIG'S SPAWN, ELSE THE GROUND STRAIGHT BELOW. Entering Creative the moment play starts drops the body
// before the map has collision, and it falls through the world for good: measured at z −90,000 twelve seconds in,
// with the photo camera placed right each time and the body already out of frame when the shot was taken. An
// editor session's config often has no player spawn at all, so a trace down at the body's own x, y is the fallback.
// `MoveTo` lands it with no velocity (`PlayerSpawner.PlaceLocal`), so the fall speed it built up goes too.
var sp = ActiveConfig.Current?.PlayerSpawns?.FirstOrDefault();
if ( sp is not null )
{
PlayerSpawner.MoveTo( go, sp.Position + Vector3.Up * 4f, Rotation.FromYaw( sp.Yaw ) );
Log.Info( $"[nz-3p] photo: standing on the first player spawn, {sp.Position}, facing {sp.Yaw:0}" );
return;
}
var top = go.WorldPosition.WithZ( 4000f );
var tr = scene.Trace.Ray( top, top.WithZ( -4000f ) )
.WithoutTags( "player", "zombie", "trigger", "corpse", "ragdoll" ).Run();
if ( !tr.Hit )
{
Log.Warning( "[nz-3p] photo: no spawn in this config and no ground under you yet — is the map still loading?" );
return;
}
var yaw = ctrl.EyeAngles.yaw;
PlayerSpawner.MoveTo( go, tr.HitPosition + Vector3.Up * 4f, Rotation.FromYaw( yaw ) );
Log.Info( $"[nz-3p] photo: standing on the ground below, {tr.HitPosition}, facing {yaw:0}" );
return;
}
var round = float.TryParse( w, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture,
out var y ) ? y : 35f;
// ⚠️ THE BODY'S FACING, NOT THE EYES'. The controller turns the body after the eyes, so straight after a turn the two
// disagree, and the gun is on the body.
var body = ctrl.Renderer.IsValid() ? ctrl.Renderer.GameObject : go;
var face = body.WorldRotation.Angles().yaw;
var at = go.WorldPosition;
// +y is LEFT in s&box, so "toward the gun hand" (the right) is a negative turn.
var from = Rotation.FromYaw( face - round );
var right = Rotation.FromYaw( face - 90f ).Forward;
var eye = at + from.Forward * dist + Vector3.Up * height;
var look = at + Vector3.Up * 46f + right * 6f;
var cam = PhotoCamera( scene );
cam.WorldPosition = eye;
cam.WorldRotation = Rotation.LookAt( look - eye, Vector3.Up );
Log.Info( $"[nz-3p] photo: camera {cam.Id} placed {round:0}° round toward the gun hand, {dist:0}u out, {height:0}u up" );
}
/// <summary>
/// `nz_3p_face [yaw] [dist]` — the photo camera on your FACE instead of the gun hand, for judging a character model
/// (2026-10-05: *"Richthofen has a red thing in the model's face that seems like missing textures"*). Aimed at the head
/// bone, `yaw` degrees round from straight in front (0) toward the gun hand, `dist` units out (24). Same camera as
/// `nz_3p_photo`, so `nz_3p_photo off` removes it.
/// </summary>
[ConCmd( "nz_3p_face" )]
public static void Face( float yaw = 0f, float dist = 24f )
{
var bodies = PlayerSpawner.AllBodies();
var go = bodies.FirstOrDefault( PlayerPresence.Mine ) ?? bodies.FirstOrDefault();
var ctrl = go.IsValid() ? go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants ) : null;
if ( !ctrl.IsValid() || !ctrl.Renderer.IsValid() || !ctrl.Renderer.TryGetBoneTransform( "head", out var head ) )
{
Log.Warning( "[nz-3p] face: no body, or no head bone on it — in play, after Creative, third person on?" );
return;
}
// The face is a little above and in front of the head bone, which sits at the top of the neck.
var facing = ctrl.Renderer.GameObject.WorldRotation.Angles().yaw;
var look = head.Position + Vector3.Up * 3f + Rotation.FromYaw( facing ).Forward * 2f;
var eye = look + Rotation.FromYaw( facing - yaw ).Forward * dist;
var cam = PhotoCamera( Game.ActiveScene );
cam.WorldPosition = eye;
cam.WorldRotation = Rotation.LookAt( look - eye, Vector3.Up );
Log.Info( $"[nz-3p] photo: camera {cam.Id} placed on the face, {yaw:0}° round toward the gun hand, {dist:0}u out"
+ $" · body '{ctrl.Renderer.Model?.ResourcePath}'" );
}
/// <summary>
/// `nz_3p_eye` — the photo camera exactly where your own view is, with its field of view, for first-person shots: the
/// hands a character wears (2026-10-05, Takeo given Nikolai's). MCP's shot of the main camera answers from the editor's
/// copy of the scene, which draws no viewmodel; this camera lives only in the game's scene.
/// </summary>
[ConCmd( "nz_3p_eye" )]
public static void Eye()
{
var main = Game.ActiveScene?.Camera;
if ( !main.IsValid() )
{
Log.Warning( "[nz-3p] eye: no main camera" );
return;
}
var cam = PhotoCamera( Game.ActiveScene );
cam.WorldPosition = main.WorldPosition;
cam.WorldRotation = main.WorldRotation;
if ( cam.Components.Get<CameraComponent>() is { } c ) c.FieldOfView = main.FieldOfView;
Log.Info( $"[nz-3p] photo: camera {cam.Id} placed at your eye, fov {main.FieldOfView:0}" );
}
/// <summary>The one photo camera: made on first use, unsaved and never networked, and never the main camera.</summary>
static GameObject PhotoCamera( Scene scene )
{
var cam = Existing( scene ).FirstOrDefault();
if ( cam.IsValid() ) return cam;
cam = scene.CreateObject();
cam.Name = CameraName;
cam.Flags |= GameObjectFlags.NotSaved;
cam.NetworkMode = NetworkMode.Never;
var c = cam.Components.Create<CameraComponent>();
c.IsMainCamera = false;
c.FieldOfView = 50f;
return cam;
}
/// <summary>
/// `nz_3p_axes` — WHERE THE GUN IN YOUR HAND POINTS, in your body's frame and in the gun model's own axes (2026-10-05).
///
/// ⚠️ THE MODEL AXIS IS THE ONE THE CHALK WORRIES ABOUT. `WallBuyManager.PreTweaks` turns five packs 90° before flattening
/// because their meshes run the barrel down another axis than the ARC9 packs' +X. Printing the barrel's model axis for each
/// gun in the hand says whether the third-person misses group the same way.
///
/// The muzzle is the model's `muzzle` attachment (what SWB fires from), else the far end of the mesh's longest side from the
/// hand. Directions are from the hand (`hold_R`), where the grip is put.
/// </summary>
[ConCmd( "nz_3p_axes" )]
public static void Axes()
{
var bodies = PlayerSpawner.AllBodies();
var go = bodies.FirstOrDefault( PlayerPresence.Mine ) ?? bodies.FirstOrDefault();
var np = go.IsValid() ? go.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) : null;
var ctrl = go.IsValid() ? go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants ) : null;
var gun = ThirdPersonWeapon.GunOf( np );
var r = gun.IsValid() ? gun.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelf ) : null;
if ( !r.IsValid() || !ctrl.IsValid() || !ctrl.Renderer.IsValid()
|| !ctrl.Renderer.TryGetBoneTransform( "hold_R", out var hand ) )
{
Log.Warning( "[nz-3p] axes: no gun in the hand, no body, or no hold_R bone" );
return;
}
var body = ctrl.Renderer.GameObject.WorldRotation;
string InBody( Vector3 v ) => $"fwd {Vector3.Dot( v, body.Forward ):+0.00;-0.00} right {Vector3.Dot( v, body.Right ):+0.00;-0.00}"
+ $" up {Vector3.Dot( v, body.Up ):+0.00;-0.00}";
Vector3 muzzle;
string from;
if ( r.GetAttachment( "muzzle" ) is Transform m )
{
muzzle = m.Position;
from = "muzzle attachment";
}
else
{
var b = r.Model.Bounds;
var size = b.Size;
var axis = size.x >= size.y && size.x >= size.z ? Vector3.Forward : size.y >= size.z ? Vector3.Left : Vector3.Up;
var half = Vector3.Dot( size, axis ) * 0.5f;
var a = gun.WorldTransform.PointToWorld( b.Center + axis * half );
var c = gun.WorldTransform.PointToWorld( b.Center - axis * half );
muzzle = a.Distance( hand.Position ) >= c.Distance( hand.Position ) ? a : c;
from = "far end of the mesh";
}
var dir = (muzzle - hand.Position).Normal;
var local = gun.WorldRotation.Inverse * dir;
var ax = MathF.Abs( local.x ) >= MathF.Abs( local.y ) && MathF.Abs( local.x ) >= MathF.Abs( local.z )
? (local.x > 0 ? "+X" : "-X")
: MathF.Abs( local.y ) >= MathF.Abs( local.z ) ? (local.y > 0 ? "+Y" : "-Y") : (local.z > 0 ? "+Z" : "-Z");
var off = MathF.Acos( Math.Clamp( Vector3.Dot( dir, body.Forward ), -1f, 1f ) ) * 180f / MathF.PI;
Log.Info( $"[nz-3p] axes: {r.Model.ResourceName} barrel along model {ax} ({from}, {muzzle.Distance( hand.Position ):0}u from the hand)" );
Log.Info( $"[nz-3p] axes: muzzle points {InBody( dir )} · {off:0}° off the body's forward" );
Log.Info( $"[nz-3p] axes: gun +X {InBody( gun.WorldRotation.Forward )} · +Y {InBody( gun.WorldRotation.Left )}"
+ $" · +Z {InBody( gun.WorldRotation.Up )}" );
Log.Info( $"[nz-3p] axes: hand +X {InBody( hand.Rotation.Forward )} · +Z {InBody( hand.Rotation.Up )}" );
// The left hand: did GunGrip.Hands get as far as an IK target, and is the hand on it?
var st = gun.Components.Get<GunGripState>( FindMode.EverythingInSelf );
var hasL = ctrl.Renderer.TryGetBoneTransform( "hand_L", out var handL );
var tgt = GunGrip.LeftTarget( gun );
Log.Info( "[nz-3p] axes: left hand: "
+ (st is null ? "⛔ no GunGripState on the gun (GunGrip.Hands never ran)"
: $"shape {(st.Shape is null ? "⛔ none" : "ok")} · turn {(st.LeftTurn is null ? "⛔ not captured" : "captured")}")
+ (hasL ? "" : " · ⛔ no hand_L bone on the body")
+ (tgt is Transform t && hasL ? $" · hand_L {handL.Position.Distance( t.Position ):0.0}u from its target" : "") );
// The muzzle attachment's own forward, in the gun's space, against the hand→muzzle line: does it run down the barrel?
if ( r.GetAttachment( "muzzle" ) is Transform ma )
{
var mf = gun.WorldRotation.Inverse * ma.Rotation.Forward;
var ang = MathF.Acos( Math.Clamp( Vector3.Dot( mf.Normal, local.Normal ), -1f, 1f ) ) * 180f / MathF.PI;
Log.Info( $"[nz-3p] axes: muzzle attachment forward (model) {mf.x:0.00},{mf.y:0.00},{mf.z:0.00}"
+ $" · {ang:0}° from the hand→muzzle line" );
}
}
/// <summary>
/// `nz_3p_ik [world|raw x y z | off]` — WHERE THE LEFT HAND'S IK PUTS THE HAND, against where it was asked to (2026-10-05).
///
/// ⚠️ IT EXISTS BECAUSE THE HAND WENT OVER THE HEAD. With its target under the handguard the IK lifted the left hand above the
/// player's head, 23–39 u from the target, and neither the engine docs nor the controller say which space the graph reads
/// `ik.hand_left.position` in (the controller was ruled out: `UseAnimatorControls` off changed nothing). `world x y z` sends
/// a point in the body's frame (forward, left, up) through `SetIk` as a world transform; `raw x y z` writes the three graph
/// parameters as they are; `off` gives the hand back to `GunGrip`. Every form prints what the graph holds and where the arm is.
/// </summary>
[ConCmd( "nz_3p_ik" )]
public static void Ik( string mode = "", float x = 0f, float y = 0f, float z = 0f )
{
mode = (mode ?? "").ToLowerInvariant();
if ( mode is "world" or "raw" )
{
GunGrip.Probe = new Vector3( x, y, z );
GunGrip.ProbeRaw = mode == "raw";
}
else if ( mode == "off" ) GunGrip.Probe = null;
var bodies = PlayerSpawner.AllBodies();
var go = bodies.FirstOrDefault( PlayerPresence.Mine ) ?? bodies.FirstOrDefault();
var ctrl = go.IsValid() ? go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants ) : null;
var r = ctrl.IsValid() ? ctrl.Renderer : null;
if ( !r.IsValid() || !r.SceneModel.IsValid() ) { Log.Warning( "[nz-3p] ik: no body" ); return; }
var tx = r.GameObject.WorldTransform;
string Local( Vector3 world ) { var p = tx.PointToLocal( world ); return $"({p.x:0.0}, {p.y:0.0}, {p.z:0.0})"; }
var sm = r.SceneModel;
var pos = sm.GetVector3( "ik.hand_left.position" );
Log.Info( $"[nz-3p] ik: probe {(GunGrip.Probe is Vector3 pr ? $"{(GunGrip.ProbeRaw ? "raw" : "world")} {pr}" : "off")}"
+ $" · the graph holds enabled {sm.GetBool( "ik.hand_left.enabled" )} position ({pos.x:0.0}, {pos.y:0.0}, {pos.z:0.0})"
+ $" · body at {r.WorldPosition} scale {r.WorldScale.x:0.00}" );
foreach ( var bone in new[] { "clavicle_L", "arm_upper_L", "arm_lower_L", "hand_L", "hold_L", "hand_R", "hold_R" } )
if ( r.TryGetBoneTransform( bone, out var b ) ) Log.Info( $"[nz-3p] ik: {bone,-12} at {Local( b.Position )} in the body's frame (fwd, left, up)" );
// The left hand's own axes and its fingers, to turn it deliberately: which local axis runs down the fingers, which way the
// palm faces. Bone names from the body itself — every left-hand bone, by position in the hand's own frame.
if ( r.TryGetBoneTransform( "hand_L", out var hl ) && r.Model?.Bones is { } bones )
{
string H( Vector3 world ) { var p = hl.Rotation.Inverse * (world - hl.Position); return $"({p.x:0.0}, {p.y:0.0}, {p.z:0.0})"; }
string Axis( Vector3 v ) { var p = tx.Rotation.Inverse * v; return $"({p.x:+0.00;-0.00}, {p.y:+0.00;-0.00}, {p.z:+0.00;-0.00})"; }
Log.Info( $"[nz-3p] ik: hand_L axes in the body's frame: X {Axis( hl.Rotation.Forward )} · Y {Axis( hl.Rotation.Left )} · Z {Axis( hl.Rotation.Up )}" );
foreach ( var bone in bones.AllBones.Where( b => b.Name.EndsWith( "_L", StringComparison.Ordinal )
&& (b.Name.StartsWith( "finger", StringComparison.Ordinal ) || b.Name.StartsWith( "hold", StringComparison.Ordinal )) ) )
if ( r.TryGetBoneTransform( bone.Name, out var fb ) ) Log.Info( $"[nz-3p] ik: {bone.Name,-20} at {H( fb.Position )} in hand_L's own frame" );
}
var gun = ThirdPersonWeapon.GunOf( go.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) );
if ( GunGrip.LeftTarget( gun ) is Transform t ) Log.Info( $"[nz-3p] ik: gun's target at {Local( t.Position )}" );
if ( gun.IsValid() ) Log.Info( $"[nz-3p] ik: {GunGrip.Describe( gun )}" );
// The gun's measured shape: the one kept on it at its first frame, and one measured now, while it stands still.
if ( gun.IsValid() && gun.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelf ) is { } gr && gr.Model is not null )
{
static string V( Vector3 v ) => $"({v.x:0.0}, {v.y:0.0}, {v.z:0.0})";
static string S( GunGrip.Shape? s ) => s is GunGrip.Shape v
? $"muzzle {V( v.Muzzle )} barrel {V( v.Barrel )} up {V( v.Up )} · t {v.TBack:0.0} … {v.TMuzzle:0.0} · below {v.Below:0.0}"
: "none";
var st = gun.Components.Get<GunGripState>( FindMode.EverythingInSelf );
Log.Info( $"[nz-3p] ik: gun {gr.Model.ResourceName} · bounds {V( gr.Model.Bounds.Mins )} … {V( gr.Model.Bounds.Maxs )}"
+ $" · object scale {gun.WorldScale.x:0.00}" );
Log.Info( $"[nz-3p] ik: shape kept {S( st?.Shape )}" );
Log.Info( $"[nz-3p] ik: shape now {S( GunGrip.Measure( gr ) )}" );
if ( gr.GetAttachment( "muzzle" ) is Transform mz ) Log.Info( $"[nz-3p] ik: muzzle now at {Local( mz.Position )} in the body's frame" );
}
}
static System.Collections.Generic.List<GameObject> Existing( Scene scene )
=> scene.IsValid()
? scene.GetAllObjects( false ).Where( o => o.IsValid() && o.Name == CameraName ).ToList()
: new System.Collections.Generic.List<GameObject>();
}