Diagnostics/HoldPhoto.cs

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).

File AccessNetworking
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>();
}