Player/ThirdPersonWeapon.cs

Component that manages showing a third-person weapon model on a player body. It publishes what the local player holds, builds a non-networked world-model GameObject parented to the body, aligns it to a chosen weapon bone vs body hand bone, handles tuning offsets, melee/reload gestures and reporting/debug console commands.

NetworkingFile Access
using Sandbox;
using System;
using System.Linq;
using SWB.Shared;

namespace NZombies;

/// <summary>
/// PUT THE GUN IN THE PLAYER'S HANDS, ON EVERY SCREEN.
///
/// ⛔ NOBODY HAS EVER VISIBLY HELD A WEAPON IN THIS PROJECT, IN ANY MODE. `NZPlayer.HoldType` has
/// carried its own confession since long before there were two players:
///
///     ⚠️ Write-only in the interface. It drives the THIRD-person hold pose, which nothing
///     renders yet; stored so a future world model can use it without SWB needing to change.
///
/// Nothing read it, and SWB builds its `WorldModelRenderer` on the WEAPON object — which is
/// `NetworkMode.Never` and exists only on its owner's machine. So a remote player walked and idled
/// correctly and held nothing, because there was nothing to hold. User: *"players are kind of just
/// standing, we do not see other players shooting, holding their weapon etc."*
///
/// ⚠️ THIS IS THE FUTURE THAT COMMENT WAS WAITING FOR, and it needed the prefab work first:
/// `[Sync]` does nothing on a `NetworkMode.Snapshot` object (`SBOX_MULTIPLAYER.md` §2), and the
/// player only became `NetworkMode.Object` when spawning moved to `prefabs/player.prefab`.
///
/// ⚠️ IT TOUCHES NO SWB CODE. The owner POLLS its own held weapon rather than SWB pushing — one
/// comparison a frame against a property SWB already maintains, and no edit to a file that has
/// twice now had a networking change go wrong inside it.
/// </summary>
public sealed class ThirdPersonWeapon : Component
{
	/// <summary>
	/// Bone names to try, in order, for where the gun sits.
	///
	/// ⛔ A LIST BECAUSE THE BODIES ARE PORTED AND RETARGETED, and this project has been bitten by
	/// assuming a rig before. `dempsey_rigged.vmdl` and friends were rebound onto s&box's human
	/// skeleton by `Tools/retarget_to_human.py`, while the fallback body is the stock Citizen —
	/// so the same code has to find a hand on two different lineages, and `nz_arms` already showed
	/// ValveBiped names surviving on the viewmodel meshes.
	///
	/// ⚠️ IT SAYS WHICH ONE IT FOUND, ONCE. A gun that silently fails to appear is exactly the
	/// class of bug this session has spent a day on.
	/// </summary>
	static readonly string[] HandBones =
	{
		"hold_R", "hand_R", "hand_r", "R_Hand",
		"ValveBiped.Bip01_R_Hand", "ValveBiped_Bip01_R_Hand",
	};

	/// <summary>
	/// The bone IN THE WEAPON MESH that should end up sitting on the body's hand.
	///
	/// ⛔ THIS IS WHY THE GUN DOES NOT NEED HAND-TUNED OFFSETS. `nz_3p_bones` on any of the 418
	/// viewmodels shows the entire Call of Duty first-person rig still inside the mesh — 103 bones
	/// on `v_m1911`, including `ValveBiped_Bip01_R_Hand`, `j_wrist_ri` and `tag_weapon`. The gun's
	/// position relative to that hand IS the authored grip: an artist placed it there, per weapon,
	/// and it is already correct for every weapon in the pack.
	///
	/// ⚠️ SO THE JOB IS NOT "WHERE DOES THIS GUN GO", IT IS "PUT THAT HAND ON THIS HAND". A
	/// per-weapon offset would be 418 numbers to find; an anchor is zero, and a weapon added later
	/// works without anybody touching this file.
	///
	/// ⛔ `j_gun`, AND BOTH OBVIOUS ALTERNATIVES WERE TRIED AND WERE WRONG. A viewmodel's BIND pose
	/// is not its idle pose: `v_m1911` binds with the arms nowhere near the weapon, so anchoring
	/// `ValveBiped_Bip01_R_Hand` put the viewmodel's wrist precisely on the citizen's hand — 0.2u,
	/// measured; the arithmetic was never in question — and left the pistol hanging a metre away.
	/// `tag_weapon` and `tag_weapon1` are no better: both belong to the ARMS rig, marking where the
	/// arms EXPECT a weapon, and sit 58u from the gun in a mesh whose entire bounding box is 10
	/// inches across.
	///
	/// ⚠️ THE MESH ITSELF SETTLED IT, WHICH NO BONE NAME COULD HAVE. `nz_3p_bones` with no filter
	/// prints `Bounds` and ranks bones by distance to it, and the ranking is unambiguous:
	///
	///     mesh centre 1.43,-0.01,-1.28  size 10.25,1.53,6.38
	///     nearest: j_gun (1.9u)  Joints (1.9u)  j_gun1 (1.9u)  tag_brass (2.6u)  tag_origin (3.6u)
	///
	/// ⚠️ SO THE GUN IS BUILT AT ITS OWN ORIGIN and `j_gun` is the bone inside it. Everything
	/// after this is a rigid offset, not a search.
	/// </summary>
	static readonly string[] GunAnchors =
	{
		"j_gun", "j_gun1", "tag_origin", "tag_weapon1", "tag_weapon",

		// ⛔ THE HL2 VIEWMODEL RIGS END HERE, AND THE NAME HAS AN UNDERSCORE. Every CoD viewmodel
		// carries a `j_gun`; an HL2 `c_` rig does not — the C-CE Prisma is a kit baked onto HL2's
		// AR2 arms and its only meaningful joint is the hand itself. Without this the anchor search
		// fails, the gun falls back to its MESH ORIGIN, and in third person it floats beside the
		// player instead of sitting in their fist.
		//
		// ⚠️ UNDERSCORE, NOT DOT, AND THAT IS NOT A TYPO. Source names the bone
		// `ValveBiped.Bip01_R_Hand`, but Blender Source Tools sanitises the dot on export — so the
		// COMPILED model has `ValveBiped_Bip01_R_Hand` while every source file, .qc and lua says
		// otherwise. `nz_3p_anchor` with the dotted name silently finds nothing.
		"ValveBiped_Bip01_R_Hand", "ValveBiped.Bip01_R_Hand",
	};

	/// <summary>
	/// Force a specific pair of bones instead of letting the lists decide. `nz_3p_anchor`.
	///
	/// ⚠️ FOR FINDING THE RIGHT PAIR WITHOUT A REBUILD. Two rigs meet here and only one of them
	/// is ours; which joint reads best is a judgement made by looking, not by reasoning, and every
	/// rebuild-and-rejoin cycle to look at one is minutes.
	/// </summary>
	public static string BodyBoneOverride { get; set; } = "";
	public static string GunBoneOverride { get; set; } = "";

	/// <summary>
	/// The correction that turns a VIEWMODEL mesh into something that sits in a fist.
	///
	/// ⛔ STATIC AND LIVE-TUNABLE, EXACTLY LIKE `ViewModelHandler.VMYaw`, AND FOR THE SAME REASON:
	/// this is a hunt for six numbers shared by 418 weapons, and a property on each prefab would be
	/// 418 edits and a restart per attempt. Once it is right it gets baked and this goes away.
	///
	/// ⚠️ A SURVIVING STATIC — INSTRUCTIONS PATTERN 1. It does NOT reset on hotload, so a value
	/// typed in one session is still there in the next; `nz_3p_tune` with no argument prints the
	/// current numbers so a session can never be tuning blind.
	/// </summary>
	public static float X { get; set; }
	public static float Y { get; set; }

	/// <summary>Down into the fist. The gun rides on the knuckles at 0 and disappears into the palm by -3.</summary>
	public static float Z { get; set; } = -1.5f;

	public static float Pitch { get; set; }
	public static float Yaw { get; set; }

	/// <summary>
	/// A quarter turn about the barrel.
	///
	/// ⚠️ WITHOUT IT EVERY WEAPON IS HELD FLAT, gangster-style, slide facing the sky — the CoD
	/// weapon rigs and the s&amp;box citizen's `hold_R` simply disagree about which way is up. It is
	/// the same class of thing as `ViewModelHandler.VMYaw`: one constant, shared by the whole pack,
	/// because they all came out of the same exporter.
	/// </summary>
	public static float Roll { get; set; } = 90f;

	/// <summary>How much to shrink the viewmodel mesh. Viewmodels are authored oversized.</summary>
	public static float Scale { get; set; } = 1f;

	/// <summary>
	/// `nz_3p_anchor &lt;body bone&gt; &lt;weapon bone&gt;` — try a different pair of joints.
	///
	/// ⚠️ EMPTY EITHER ARGUMENT TO GO BACK TO AUTOMATIC. `nz_3p_anchor` on its own resets both.
	/// ⚠️ IT DROPS EVERY GUN SO THEY REBUILD, because the anchor is resolved once when the gun
	/// is created — without this the command would appear to do nothing until a weapon switch.
	/// </summary>
	[ConCmd( "nz_3p_anchor" )]
	public static void Anchor( string bodyBone = "", string gunBone = "" )
	{
		BodyBoneOverride = bodyBone ?? "";
		GunBoneOverride = gunBone ?? "";

		var n = 0;

		foreach ( var go in PlayerSpawner.AllBodies() )
		{
			var tp = go.Components.Get<ThirdPersonWeapon>( FindMode.EverythingInSelf );
			if ( !tp.IsValid() ) continue;

			tp.Drop();

			// ⛔ `Drop()` ALONE LOOKS LIKE IT WORKS AND DOES NOTHING. The gun is only ever built
			// when the published path stops matching `_shownPath`, so dropping the object without
			// also forgetting the path leaves a component that is holding nothing and believes it is
			// up to date — `nz_3p` reported `gun object no` for a weapon still in the player's hands.
			tp._shownPath = "";
			n++;
		}

		Log.Info( $"[nz-3p] anchor: body '{(string.IsNullOrEmpty( BodyBoneOverride ) ? "auto" : BodyBoneOverride)}'"
			+ $" ← weapon '{(string.IsNullOrEmpty( GunBoneOverride ) ? "auto" : GunBoneOverride)}'"
			+ $"   ({n} rebuilding)" );
	}

	/// <summary>
	/// `nz_3p_tune [x] [y] [z] [pitch] [yaw] [roll] [scale]` — place the gun in the hand.
	///
	/// ⚠️ NO ARGUMENT REPORTS AND CHANGES NOTHING, so it is safe to type first.
	/// </summary>
	[ConCmd( "nz_3p_tune" )]
	public static void Tune( float x = float.NaN, float y = 0f, float z = -1.5f,
		float pitch = 0f, float yaw = 0f, float roll = 90f, float scale = 1f )
	{
		if ( !float.IsNaN( x ) )
		{
			X = x; Y = y; Z = z; Pitch = pitch; Yaw = yaw; Roll = roll; Scale = scale;
		}

		Log.Info( $"[nz-3p] gun in hand:  pos {X:0.##} {Y:0.##} {Z:0.##}"
			+ $"   ang {Pitch:0.##} {Yaw:0.##} {Roll:0.##}   scale {Scale:0.###}" );
		Log.Info( $"[nz-3p]   nz_3p_tune {X:0.##} {Y:0.##} {Z:0.##} {Pitch:0.##} {Yaw:0.##} {Roll:0.##} {Scale:0.###}" );
	}

	/// <summary>
	/// The gun object in this player's hand ON THIS MACHINE, or null when they hold nothing.
	///
	/// ⚠️ THIS IS WHAT LETS A REMOTE PLAYER'S MUZZLE FLASH EXIST AT ALL. `PapMuzzleFlash.Spawn`
	/// needs a muzzle to hang off, and the real one belongs to a weapon that is
	/// `NetworkMode.Never` — it is on exactly one machine. This object is on every machine, at the
	/// right hand, following the animation, which is the only place a remote flash can honestly go.
	/// </summary>
	public GameObject Gun => _go;

	/// <summary>The in-hand gun object for a given player on this machine.</summary>
	public static GameObject GunOf( NZPlayer player )
		=> player.IsValid()
			&& player.Components.Get<ThirdPersonWeapon>( FindMode.EverythingInSelf ) is { } tp
			? tp.Gun
			: null;

	GameObject _go;
	SkinnedModelRenderer _renderer;
	string _shownPath = "";
	string _shownHold = "";
	Model _posedModel;
	string _bone;
	string _anchor;
	bool _warned;

	/// <summary>The last `Build` found no hand, so `Apply` looks again every <see cref="HandRetry"/> seconds.</summary>
	bool _noHand;
	TimeSince _sinceBuild;

	/// <summary>The body model the gun was hung on: a different one (a character picked, a respawn) hangs it again.</summary>
	Model _handModel;

	/// <summary>How often a gun with no hand to sit in looks for one again.</summary>
	const float HandRetry = 0.25f;

	// ══ the knife swing (2026-10-05) ═══════════════════════════════════════════════════════

	/// <summary>
	/// What `Knife` sends through `NZNet.PlayerAnim` for a swing. Not an animgraph parameter: a request this component turns into
	/// one, so every machine plays the same swing — the melee hold, the knife in the hand, then `b_attack`.
	/// </summary>
	public const string MeleeGesture = "nz_melee";

	static float? _meleeSeconds;
	/// <summary>
	/// How long a third-person knife swing keeps the melee hold and the knife before the gun comes back. 1.0s: the length of the
	/// attack clip itself (`Citizen@Melee_Weapons_2H_Attack_01.fbx`, and the punches, all 1.0s). It was 0.6s, which put the gun's
	/// hold back mid-swing and cut the swing off — the user: *"i hardly see it"* (2026-10-05).
	/// </summary>
	public static float MeleeSeconds { get => _meleeSeconds ?? 1f; set => _meleeSeconds = value; }

	/// <summary>
	/// A hold as the body's animgraph names it.
	///
	/// ⛔ NOT ALWAYS THE LOWERCASED `HoldTypes` NAME (2026-10-05). The graph's `holdtype` options are `none, pistol, rifle,
	/// shotgun, holditem, melee_punch, melee_weapons, rpg` (+ `physgun` on the Citizen graphs), read out of
	/// `citizen_human_reuse.vanmgrph` and `citizen.vanmgrph`. `Punch` and `Swing` are NOT options, so setting them did nothing:
	/// no melee weapon ever posed, and the knife's swing played in whatever hold the body was already in. The user, of the
	/// third-person knife: *"i hardly see it"*.
	/// </summary>
	static string AnimHold( HoldTypes hold ) => hold switch
	{
		HoldTypes.Punch => "melee_punch",
		HoldTypes.Swing => "melee_weapons",
		_ => hold.ToString().ToLowerInvariant(),
	};

	/// <summary>The knife's own attach bone in its BO2 mesh, which has no `j_gun` (`Build`).</summary>
	const string KnifeAnchor = "tag_knife";

	/// <summary>A knife swing is playing on this body until this runs out (`PlayBodyAnim`).</summary>
	TimeUntil _meleeUntil;

	/// <summary>The swing's `b_attack`, held back a frame so the melee hold is in place when the animgraph reads it.</summary>
	bool _meleeAttackPending;

	bool MeleeActive => _meleeUntil > 0f;

	/// <summary>The last gun the "is now holding" and "grip" lines were written for: a swing hangs the knife and then the gun
	/// again, and writing both lines every swing, on every machine, would bury the log.</summary>
	string _loggedHold, _loggedGrip;

	// ══ the reload (2026-10-05) ═══════════════════════════════════════════════════════════

	/// <summary>
	/// One step of a reload, as the gun reports it (`Weapon.BodyReload.cs` → `NZNet.PlayerReload`). The values travel as ints.
	///
	/// ⛔ THE RELOAD WAS ONE BARE `b_reload`, sent through `PlayBodyAnim` before the reload had a length. The body played its clip at
	/// the authored 1.67 s whatever the gun took, a shotgun re-sent it every round to a graph that answers it once, nothing said
	/// when a reload ended, and the left hand stayed pinned to the gun (`GunGrip.Hands`). The user: *"I want the reload animations
	/// to also be seen in third person"*. Each step now carries its length, and the body fits its clip to it (`speed_reload`).
	/// `nz_anim b_reload` still fires the bare gesture, at whatever `speed_reload` was last set.
	/// </summary>
	public enum ReloadPhase
	{
		/// <summary>A whole magazine reload.</summary>
		Magazine = 0,

		/// <summary>A round-at-a-time reload opens, loading its first round as it does (`Weapon.OnShellReload`).</summary>
		ShellStart = 1,

		/// <summary>One more round goes in.</summary>
		ShellInsert = 2,

		/// <summary>A round-at-a-time reload closes, or stops part way.</summary>
		ShellEnd = 3,

		/// <summary>A magazine reload stopped part way (the knife, a power-up).</summary>
		Cancel = 4,
	}

	// ⛔ THE CLIP LENGTHS ARE READ FROM THE ENGINE'S FILES, NOT GUESSED (INSTRUCTIONS §41): frames at 30 fps, from
	// `addons/citizen/Assets/models/citizen/prefabs/citizen_animationlist.vmdl_prefab` and the clip FBXs' `TimeSpanStop`.

	/// <summary>`Pistol_2H_Reload_01`, `Pistol_RH_Reload_01`, and `SMG_2H_Reload_01`, which the rifle and rpg holds play: 50 frames.</summary>
	const float MagazineClip = 50f / 30f;

	/// <summary>`Shotgun_2H_Reload_01_entry`, frames 0–13: the gun brought up to load.</summary>
	const float ShellEntryClip = 13f / 30f;

	/// <summary>`Shotgun_2H_Reload_01_loop`, frames 12–20: one shell in.</summary>
	const float ShellInsertClip = 8f / 30f;

	/// <summary>`Shotgun_2H_Reload_01_exit`, frames 20–35: back up to aim.</summary>
	const float ShellExitClip = 15f / 30f;

	/// <summary>The body's closing when the gun has no closing clip of its own and is ready at once.</summary>
	const float ShellExitQuick = 0.3f;

	/// <summary>How long past a round's end `b_reloading` waits for the next step before letting go by itself.</summary>
	const float ShellGrace = 0.75f;

	/// <summary>How long a round-at-a-time reload stays shown past its step, so the left hand does not snap back between rounds.</summary>
	const float ShellMargin = 0.35f;

	/// <summary>A round-at-a-time reload on a hold with no round loop (a revolver, a tube-fed rifle) plays its one reload clip
	/// about this often, rather than once in slow motion across the whole reload.</summary>
	const float RepeatEvery = 2.5f;

	/// <summary>Steps the RPC handed over, put on the body in `OnUpdate` (`TickReload`), not inside the RPC.</summary>
	System.Collections.Generic.Queue<(ReloadPhase Phase, float Seconds, float Total)> _reloadSteps;

	/// <summary>The body shows a reload until this runs out, and the left hand is off the gun meanwhile (`Apply`).</summary>
	TimeUntil _reloadUntil;

	bool ReloadActive => _reloadUntil > 0f;

	/// <summary>`b_reloading` is held: the shotgun graph's round loop is open.</summary>
	bool _shellHeld;

	/// <summary>`b_reloading` lets go by then unless a step renews it: an owner who went down or switched away mid-reload sends no
	/// closing.</summary>
	TimeUntil _shellRelease;

	/// <summary>A magazine reload on the shotgun hold, played as the loop in three parts: its push and its closing, still to come.</summary>
	TimeUntil _magInsertAt, _magCloseAt;
	bool _magInsertDue, _magCloseDue;
	float _magPart;

	/// <summary>A round-at-a-time reload on a hold with no loop: how many more plays of the reload clip, how far apart, and when.</summary>
	int _repeatsLeft;
	float _repeatSlot;
	TimeUntil _repeatAt;

	/// <summary>The body whose animgraph tags this listens to (`WatchTags`).</summary>
	SkinnedModelRenderer _tagBody;

	/// <summary>The graph's `Reloading` tag came on since the watch began.</summary>
	bool _reloadTagSeen;

	/// <summary>A reload waiting to be reported, until the watch runs out (`ReportReload`).</summary>
	bool _reloadWatch;
	TimeUntil _reloadWatchUntil;
	string _reloadWatchHold, _reloadWatchLine;

	/// <summary>The holds a reload has been reported for on this body, once each; and how many failures have been.</summary>
	System.Collections.Generic.HashSet<string> _reloadSaid;
	int _reloadWarned;

	/// <summary>
	/// Where the weapon's anchor bone sits relative to the weapon OBJECT, in unscaled model units.
	///
	/// ⚠️ READ OFF THE LIVE RENDERER, NOT OUT OF THE MODEL. `Model.Bones[…].LocalTransform` is
	/// undocumented as to what it is relative to, and measuring it settled the question the wrong
	/// way round: composing the parent chain for `tag_weapon` gives (-23, -238, 8), a point twenty
	/// feet from a gun. The renderer's own `TryGetBoneTransform` has no such ambiguity.
	///
	/// ⚠️ RESOLVED BEFORE THE OBJECT IS MOVED, which is the whole reason it is a cached nullable
	/// rather than something read on demand: the renderer's bones were computed for the transform
	/// this object had LAST frame, so reading them is only meaningful while that transform is still
	/// what is on the object.
	/// </summary>
	Transform? _anchorLocal;

	NZPlayer Player => Components.Get<NZPlayer>( FindMode.EverythingInSelf );

	/// <summary>The body's renderer — the thing the hold pose is set on and the hand belongs to.</summary>
	SkinnedModelRenderer Body
	{
		get
		{
			var ctrl = Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
			if ( ctrl.IsValid() && ctrl.Renderer.IsValid() ) return ctrl.Renderer;

			// ⛔ `PlayerController.Renderer` READS NULL MORE OFTEN THAN IT LOOKS.
			// `PlayerCharacters.ApplyBody` carries the same fallback and the same warning — it is a
			// component REFERENCE, and this project has never established that one survives every
			// path a body can arrive by. Measured here: it was null on the scene's own player, in
			// the editor, on the first frame of play.
			//
			// ⚠️ EXCLUDING VIEWMODELS BY TAG, which is what makes the fallback safe now: weapons
			// no longer cross the network at all, so on any body the only other skinned renderer is
			// the body — and a viewmodel is tagged.
			return Components
				.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants )
				.FirstOrDefault( r => r.IsValid()
					&& !r.GameObject.Tags.Has( SWB.Shared.TagsHelper.ViewModel ) );
		}
	}

	protected override void OnUpdate()
	{
		var np = Player;
		if ( !np.IsValid() ) return;

		// ⚠️ ONLY THE OWNER MAY WRITE. `[Sync]` silently discards a write from a proxy
		// (`SBOX_MULTIPLAYER.md` §5), so doing it anyway would fail quietly on every machine but
		// one — which is the shape of half the bugs this session.
		if ( !Networking.IsActive || PlayerPresence.Mine( GameObject ) ) Publish( np );

		// ⚠️ THE RELOAD BEFORE `Apply` (2026-10-05), so the left hand comes off the gun on the frame a reload starts.
		TickReload( np );

		Apply( np );
	}

	/// <summary>
	/// Say what I am holding, so every other machine can show it.
	///
	/// ⚠️ THE WORLD MODEL'S PATH, NOT THE WEAPON PREFAB'S. Any machine can `Model.Load` a path
	/// with no prefab, no `SWB.Weapon` component and no inventory — and the weapon prefabs are
	/// `NetworkMode.Never` precisely so they never reach another machine. Sending what to DRAW
	/// asks nothing of the thing that is not there.
	/// </summary>
	void Publish( NZPlayer np )
	{
		var wep = Rarity.HeldBy( np );

		// ⚠️ A DOWNED PLAYER HOLDS NOTHING. `GoDown` strips the weapon, and a gun left in a
		// crawling player's hand is the kind of detail that reads as a bug from across the map.
		//
		// ⛔ NOT ONE OF THE 418 WEAPON PREFABS HAS A `WorldModel`. Measured, not assumed — every
		// one of them is `WorldModel = None`, because the ARC9 port brought viewmodels and nothing
		// else. So "use the weapon's world model" would draw nothing, for every weapon, forever.
		//
		// ⚠️ THE VIEWMODEL MESH IS THE STAND-IN, AND IT IS A STAND-IN. `v_m1911.vmdl` is the gun
		// alone — the hands are a separate bone-merged renderer, which `nz_arms` shows — so the
		// geometry is right even though the ORIGIN is authored for a viewmodel camera rather than
		// for a fist. `nz_3p_tune` exists to dial that out once, globally, the same way
		// `ViewModelHandler.VMYaw` had to for the first-person case: *"every ARC9 BO1 viewmodel is
		// authored a quarter turn from the axis SWB expects."*
		//
		// ⚠️ WHEN REAL WORLD MODELS ARE PORTED, THIS LINE IS THE ONLY PLACE THAT CHANGES — the
		// `?? ` falls away and everything downstream already works on a path.
		var model = wep.IsValid() ? wep.WorldModel ?? wep.ViewModel : null;
		// ⚠️ ITS DISPLAY MODEL WHEN IT HAS ONE (`WeaponDisplay`, 2026-10-01): an MW viewmodel with no clip playing is its
		// bind pose, parts apart. The anchors there keep their bind transforms, so the grip below does not move.
		var path = model is not null && !np.IsDown ? WeaponDisplay.PathFor( model.ResourcePath ?? "" ) : "";
		// ⚠️ THROUGH `GunGrip.HoldFor` (2026-10-05): a long gun tagged with the pistol hold takes the rifle's, by its manifest class.
		var hold = wep.IsValid() && !np.IsDown ? GunGrip.HoldFor( wep, wep.HoldType ) : HoldTypes.None;

		if ( np.WorldModelPath != path ) np.WorldModelPath = path;
		if ( np.HoldTypeId != (int)hold ) np.HoldTypeId = (int)hold;
	}

	/// <summary>
	/// Fire a one-shot GESTURE on this player's body — the reload, the swing. Runs on every
	/// machine, called by <see cref="NZNet.PlayerAnim"/>.
	///
	/// ⛔ NOTHING IN THIS PROJECT EVER DID THIS. `holdtype` was the ONLY animgraph parameter any
	/// code set on a player body, so a reload or a knife swing moved the viewmodel and left the
	/// body standing there holding its pose — for everyone, including the player themselves in
	/// third person. User: *"the players do not see each other do the reload and knifing
	/// animations."* It reads as a networking fault and is not one: there was no animation to fail
	/// to replicate.
	///
	/// ⚠️ THE NAMES COME FROM THE ENGINE, NOT FROM A GUESS. `Sandbox.Engine.xml` documents
	/// `BaseCombatWeapon.OnShootEffects` as firing *"the holder's b_attack"* and `OnReloadStarted`
	/// as firing *"the b_reload gesture"*. A wrong parameter name compiles and animates nothing,
	/// which is the same silent failure the `holdtype` note above was written about.
	///
	/// ⚠️ A GESTURE, SO IT IS SET AND FORGOTTEN. The animgraph consumes the trigger; there is no
	/// matching "false" to send and no duration to keep in step across machines.
	/// </summary>
	public void PlayBodyAnim( string param )
	{
		if ( string.IsNullOrEmpty( param ) ) return;

		var body = Body;
		if ( !body.IsValid() ) return;

		// ⛔ THE KNIFE ASKS FOR A SWING, NOT FOR `b_attack` (2026-10-05). It sent `b_attack` alone, and the animgraph plays
		// `b_attack` for the hold the body is in — the GUN's, since the knife never puts the gun away — so a third-person knife
		// was a pistol or a rifle recoiling. The user: *"In third person, the players do not have a knifing animation"*. A swing
		// is the melee-weapons hold and then `b_attack`; `Apply` keeps the hold, with the knife in the hand, for `MeleeSeconds`.
		//
		// ⚠️ THE GRAPH HAS NO KNIFE STAB. GMod's quick-knife switched to a real knife weapon (`tfa_quickknife_base`, hold type
		// "knife") and played its own attack gesture; s&box's graphs have punches (left or right, at random) and one weapon swing
		// (`Melee_Weapons_2H_Attack_01`). The swing is the one that reads from across a room, with the knife in the right hand.
		//
		// ⚠️ A DOWNED PLAYER GETS THE OLD GESTURE ONLY: the crawl is its own pose, and a melee hold laid over it is not this fix.
		if ( param == MeleeGesture )
		{
			if ( Player is { } np && np.IsValid() && np.IsDown )
			{
				body.Set( "b_attack", true );
				return;
			}

			_meleeUntil = MeleeSeconds;
			_meleeAttackPending = true;
			return;
		}

		body.Set( param, true );
	}

	/// <summary>
	/// One step of this player's reload, on every machine (`NZNet.PlayerReload`). Queued: `TickReload` puts it on the body.
	/// </summary>
	public void PlayReload( int phase, float seconds, float total )
	{
		// ⚠️ MADE HERE, NOT BY AN INITIALISER: a component alive across a hotload gets a new field without its initialiser running.
		_reloadSteps ??= new();
		if ( _reloadSteps.Count < 8 ) _reloadSteps.Enqueue( ((ReloadPhase)phase, seconds, total) );
	}

	/// <summary>
	/// The reload's steps onto the body, and the parts it plays by itself. Every frame on every machine, BEFORE `Apply`.
	/// </summary>
	void TickReload( NZPlayer np )
	{
		var body = Body;
		if ( !body.IsValid() )
		{
			_reloadSteps?.Clear();
			return;
		}

		WatchTags( body );

		// ⚠️ THE HOLD AS THE GRAPH NAMES IT (`AnimHold`). None while downed or mid knife swing: neither has a reload.
		var hold = MeleeActive || np.IsDown ? null : AnimHold( (HoldTypes)np.HoldTypeId );

		while ( _reloadSteps is { Count: > 0 } )
		{
			var step = _reloadSteps.Dequeue();
			ApplyReloadStep( body, hold, step.Phase, step.Seconds, step.Total );
		}

		if ( _magInsertDue && _magInsertAt <= 0f )
		{
			_magInsertDue = false;
			body.Set( "speed_reload", ReloadRate( ShellInsertClip, _magPart ) );
			body.Set( "b_reloading_insert", true );
		}

		if ( _magCloseDue && _magCloseAt <= 0f )
		{
			_magCloseDue = false;
			CloseShell( body, _magPart );
		}

		if ( _repeatsLeft > 0 && _repeatAt <= 0f )
		{
			_repeatsLeft--;
			_repeatAt = _repeatSlot;
			body.Set( "speed_reload", ReloadRate( MagazineClip, _repeatSlot * 0.9f ) );
			body.Set( "b_reload", true );
		}

		// ⚠️ NOTHING RENEWED THE LOOP, OR THE PLAYER WENT DOWN: let it go, or the body would stand in the loading pose for good.
		if ( _shellHeld && (_shellRelease <= 0f || np.IsDown) ) CloseShell( body, ShellExitQuick );

		ReportReload( body );
	}

	/// <summary>Put one step on the body, as its hold plays it.</summary>
	void ApplyReloadStep( SkinnedModelRenderer body, string hold, ReloadPhase phase, float seconds, float total )
	{
		seconds = MathF.Max( 0f, seconds );
		total = MathF.Max( seconds, total );

		// ⚠️ ONLY THE SHOTGUN HOLD HAS A ROUND LOOP (`b_reloading` held, `b_reloading_insert` per round, read out of
		// `citizen_holdtype_shotgun.vsubgrph`). The pistol, rifle and rpg holds have one reload clip, on `b_reload`. The other holds
		// (none, an item, the melee ones) have no reload at all, so a step on one shows nothing.
		var shotgun = hold == "shotgun";
		var clip = hold is "pistol" or "rifle" or "rpg";
		if ( !shotgun && !clip && (phase is ReloadPhase.Magazine or ReloadPhase.ShellStart or ReloadPhase.ShellInsert) ) return;

		var wasShowing = ReloadActive;

		switch ( phase )
		{
			case ReloadPhase.Magazine:
				StopReloadParts();

				if ( shotgun )
				{
					// ⚠️ A MAGAZINE ON THE SHOTGUN HOLD IS THE ROUND LOOP IN THREE PARTS, each fitted to its share: up to load, one push
					// in, back to aim. The hold's own `b_reload` clip holds a 1.2 s loop no `speed_reload` shortens.
					_magPart = seconds * 0.35f;
					OpenShell( body, seconds * 0.3f, seconds + ShellGrace );
					_magInsertAt = seconds * 0.3f;
					_magInsertDue = true;
					_magCloseAt = seconds * 0.65f;
					_magCloseDue = true;
				}
				else
				{
					body.Set( "speed_reload", ReloadRate( MagazineClip, seconds ) );
					body.Set( "b_reload", true );
				}

				Showing( hold, seconds, 0.15f, "a magazine", wasShowing );
				break;

			case ReloadPhase.ShellStart:
				StopReloadParts();

				if ( shotgun )
				{
					OpenShell( body, seconds, seconds + ShellGrace );
					Showing( hold, seconds, ShellMargin, "round by round", wasShowing );
					break;
				}

				// ⚠️ NO LOOP ON THIS HOLD: ITS ONE CLIP ACROSS THE WHOLE RELOAD, played again every `RepeatEvery` or so when that is
				// long, rather than in slow motion or once per round.
				var plays = Math.Max( 1, (int)MathF.Round( total / RepeatEvery ) );
				_repeatSlot = total / plays;
				_repeatsLeft = plays - 1;
				_repeatAt = _repeatSlot;
				body.Set( "speed_reload", ReloadRate( MagazineClip, _repeatSlot * 0.9f ) );
				body.Set( "b_reload", true );
				Showing( hold, total, ShellMargin, $"round by round, as {plays} clip(s)", wasShowing );
				break;

			case ReloadPhase.ShellInsert:
				if ( shotgun )
				{
					if ( !_shellHeld ) OpenShell( body, seconds, seconds + ShellGrace );
					else
					{
						body.Set( "speed_reload", ReloadRate( ShellInsertClip, seconds ) );
						body.Set( "b_reloading_insert", true );
						_shellRelease = seconds + ShellGrace;
					}
				}

				_reloadUntil = MathF.Max( _reloadUntil, seconds + ShellMargin );
				break;

			case ReloadPhase.ShellEnd:
				_repeatsLeft = 0;

				if ( _shellHeld ) CloseShell( body, seconds > 0.05f ? seconds : ShellExitQuick );

				// ⚠️ THE ESTIMATE RAN LONG (fewer rounds went in than it counted): the clip still playing is hurried to its end.
				else if ( clip && _reloadUntil > seconds + 0.5f )
				{
					body.Set( "speed_reload", 5f );
					_reloadUntil = MathF.Max( seconds, 0.15f );
				}
				break;

			case ReloadPhase.Cancel:
				StopReloadParts();

				// ⚠️ THE CLIP ALREADY PLAYING RUNS OUT AT ONCE: the graph's Reload state only leaves when its clip finishes.
				if ( _shellHeld ) CloseShell( body, 0.1f );
				else body.Set( "speed_reload", 5f );

				_reloadUntil = 0.15f;
				break;
		}
	}

	/// <summary>Forget the parts still to play by themselves: the shotgun-hold magazine's push and closing, the clip's repeats.</summary>
	void StopReloadParts()
	{
		_magInsertDue = _magCloseDue = false;
		_repeatsLeft = 0;
	}

	/// <summary>Open the shotgun graph's round loop: up to load, the entry clip over <paramref name="entrySeconds"/>.</summary>
	void OpenShell( SkinnedModelRenderer body, float entrySeconds, float release )
	{
		body.Set( "speed_reload", ReloadRate( ShellEntryClip, entrySeconds ) );
		body.Set( "b_reloading", true );
		_shellHeld = true;
		_shellRelease = release;
	}

	/// <summary>Close it: back up to aim, the exit clip over <paramref name="exitSeconds"/>.</summary>
	void CloseShell( SkinnedModelRenderer body, float exitSeconds )
	{
		body.Set( "speed_reload", ReloadRate( ShellExitClip, exitSeconds ) );
		body.Set( "b_reloading", false );
		_shellHeld = false;
		_magInsertDue = _magCloseDue = false;
		_reloadUntil = exitSeconds + 0.1f;
	}

	/// <summary>The `speed_reload` that plays a clip of <paramref name="clip"/> seconds in <paramref name="seconds"/>, within the
	/// graph's own 0.05–5.</summary>
	static float ReloadRate( float clip, float seconds ) => Math.Clamp( clip / MathF.Max( 0.01f, seconds ), 0.05f, 5f );

	/// <summary>
	/// A reload is on the body for <paramref name="seconds"/>: the left hand comes off the gun for it, and the graph's answer is
	/// watched for (`ReportReload`).
	/// </summary>
	void Showing( string hold, float seconds, float margin, string kind, bool wasShowing )
	{
		_reloadUntil = seconds + margin;

		// ⚠️ NOT WATCHED WHEN ONE WAS ALREADY SHOWING: the graph's tag would not come on afresh, and the line would be a false alarm.
		if ( wasShowing ) return;

		_reloadTagSeen = false;
		_reloadWatch = true;
		_reloadWatchUntil = MathF.Min( seconds, 1f ) + 0.5f;
		_reloadWatchHold = hold;
		_reloadWatchLine = $"{hold} hold, {kind}, {seconds:0.00}s";
	}

	/// <summary>
	/// SAY WHETHER THE BODY TOOK IT, ON EVERY MACHINE. Five things stand between a gun's reload and a body showing it — the send,
	/// the RPC, finding the body, the graph accepting it, nothing pinning the arms — and four of them fail silently.
	///
	/// ⚠️ ONCE PER HOLD PER BODY when it works, and the first three per body that do not. "Did not" means the graph never reported
	/// its `Reloading` tag: either it refused the reload, or tag events do not reach code on this build. Which one is a matter of
	/// watching the body.
	/// </summary>
	void ReportReload( SkinnedModelRenderer body )
	{
		if ( !_reloadWatch || (!_reloadTagSeen && _reloadWatchUntil > 0f) ) return;
		_reloadWatch = false;

		if ( _reloadTagSeen )
		{
			_reloadSaid ??= new();
			if ( _reloadSaid.Add( _reloadWatchHold ?? "" ) )
				Log.Info( $"[nz-3p] '{GameObject.Name}' reloads on its body ({_reloadWatchLine}): its animgraph entered 'Reloading' ✔"
					+ " and the left hand is off the gun for it" );
			return;
		}

		if ( _reloadWarned++ < 3 )
			Log.Warning( $"[nz-3p] ⚠️ '{GameObject.Name}': a reload went to its body ({_reloadWatchLine}, model "
				+ $"{Leaf( body.Model?.ResourceName )}) but its animgraph never reported 'Reloading'. Either the graph refused it, or"
				+ " tag events do not reach code here: watch the body to tell which." );
	}

	/// <summary>Listen to this body's animgraph tags, and stop listening to the last one's.</summary>
	void WatchTags( SkinnedModelRenderer body )
	{
		if ( ReferenceEquals( body, _tagBody ) ) return;

		if ( _tagBody.IsValid() ) _tagBody.OnAnimTagEvent -= OnAnimTag;
		_tagBody = body;
		body.OnAnimTagEvent += OnAnimTag;
	}

	void OnAnimTag( SceneModel.AnimTagEvent e )
	{
		if ( e.Name == "Reloading" && e.Status == SceneModel.AnimTagStatus.Start ) _reloadTagSeen = true;
	}

	/// <summary>Show whatever this player says they are holding. Runs on every machine.</summary>
	void Apply( NZPlayer np )
	{
		var body = Body;
		if ( !body.IsValid() ) return;

		// ── the pose ──────────────────────────────────────────────────────────────────────
		//
		// ⚠️ AN OPTION NAME, NOT A NUMBER. `SkinnedModelRenderer.Set( "holdtype", … )` is
		// documented as taking the enum's OPTION NAME — `Set( "holdtype", "pistol" )`. Sending an
		// int would compile and animate nothing.
		//
		// ⛔ AND THE GRAPH'S OPTION NAME, WHICH IS NOT ALWAYS OURS LOWERCASED (2026-10-05) — see `AnimHold`. This read
		// "our `HoldTypes` is a copy of `CitizenAnimationHelper.HoldTypes`, so the lowercased member name is the option",
		// which was true of six of the eight and false of both melee holds.
		var hold = AnimHold( (HoldTypes)np.HoldTypeId );

		// ⚠️ A KNIFE SWING HOLDS THE MELEE POSE (2026-10-05, `PlayBodyAnim`). The gun's comes back by itself when it runs out: the
		// next frame's hold no longer matches the one shown.
		if ( MeleeActive ) hold = AnimHold( HoldTypes.Swing );

		// ⛔ THE RENDERER IS PART OF THE COMPARISON, NOT JUST THE VALUE. `PlayerCharacters.ApplyBody`
		// swaps `rend.Model` when a player picks a character, and the new model starts on its
		// animgraph's DEFAULT holdtype — while this component still believes it has already sent the
		// right one and would never send it again. The result would be a player who holds their
		// weapon correctly until they change character and empty-handed-looking after, which is
		// exactly the sort of "only sometimes" bug that costs a session to find.
		var posed = false;

		if ( hold != _shownHold || !ReferenceEquals( body.Model, _posedModel ) )
		{
			_shownHold = hold;
			_posedModel = body.Model;
			body.Set( "holdtype", hold );
			posed = true;
		}

		// ⚠️ THE SWING ITSELF A FRAME AFTER ITS HOLD (2026-10-05). Set in the same frame, the animgraph could read `b_attack` while
		// still in the gun's pose and play the gun's attack after all. A swing that runs out before it fires is dropped.
		if ( _meleeAttackPending )
		{
			if ( !MeleeActive ) _meleeAttackPending = false;
			else if ( !posed )
			{
				_meleeAttackPending = false;
				body.Set( "b_attack", true );
			}
		}

		// ── the gun ───────────────────────────────────────────────────────────────────────
		//
		// ⚠️ THE KNIFE STANDS IN FOR IT DURING A SWING (2026-10-05) — its viewmodel mesh, as every gun's is (`Publish`) — and the
		// gun comes back with the hold. The first-person knife puts the gun out of sight the same way (`Knife.ShowGuns`).
		var path = MeleeActive ? KnifeViewModel.ModelPath : np.WorldModelPath;

		if ( path != _shownPath )
		{
			_shownPath = path;
			Drop();

			if ( !string.IsNullOrEmpty( _shownPath ) ) Build( body );
		}
		// ⛔ AND HUNG AGAIN WHEN THE HAND WAS NOT THERE, OR THE BODY CHANGED (2026-10-05). This ran on a new path only, so a gun
		// shown at the wrong moment stayed missing: a body that has just respawned has no bones for a frame, and one still on
		// the citizen model has no hand by these names (`[nz-3p] ⛔ no hand bone on 'dempsey_rigged'` and `on 'citizen'`, both
		// at the 23:45:55 respawn). The player looked empty-handed to everyone until they switched guns. The warning stays at
		// one per gun; the retry is six bone lookups, four times a second, only while a hand is missing.
		else if ( !string.IsNullOrEmpty( _shownPath )
			&& ( _noHand ? _sinceBuild > HandRetry : _go.IsValid() && !ReferenceEquals( body.Model, _handModel ) ) )
		{
			var warned = _warned;
			Drop();
			_warned = warned;

			Build( body );
		}

		if ( !_go.IsValid() )
		{
			GunGrip.Clear( body );
			return;
		}

		// ⛔ READ THE ANCHOR *BEFORE* MOVING ANYTHING. The renderer's bone transforms were computed
		// for the transform this object had LAST frame, so this is the one moment in the frame where
		// `_go.WorldTransform` and the bone agree — reading after the write below would measure the
		// offset against a transform the bones know nothing about, and the gun would walk away from
		// the hand a little further every frame.
		if ( _anchor is not null && _anchorLocal is null && _renderer.IsValid()
			&& _renderer.TryGetBoneTransform( _anchor, out var anchorWorld ) )
		{
			_anchorLocal = _go.WorldTransform.ToLocal( anchorWorld );

			// ⚠️ ONCE PER GUN AND NEVER FOR THE KNIFE (2026-10-05) — see `_loggedGrip`.
			if ( _shownPath != KnifeViewModel.ModelPath && _shownPath != _loggedGrip )
			{
				_loggedGrip = _shownPath;
				Log.Info( $"[nz-3p] '{GameObject.Name}' grip: '{_anchor}' sits at "
					+ $"{_anchorLocal.Value.Position} in {Leaf( _shownPath )}" );
			}
		}

		// ⛔ A GUN WITH NO GUN BONE TO TRUST IS SEATED BY ITS OWN SHAPE (2026-10-05, `GunGrip.Frame`): the TFA-format packs, whose
		// only anchor was the first-person ARMS' hand, so the gun sat wherever those arms held it in their bind pose — Destiny's
		// pointed back past the hip, 28 u from the hand. Read in this same moment, before the object moves, once per gun (the mark is
		// on the gun object), and never for the knife: `tag_knife` is trusted. Retried a few frames while the bones are not up yet.
		if ( GunGrip.MeshGrip && GunGrip.Untrusted( _anchor ) && _shownPath != KnifeViewModel.ModelPath && _renderer.IsValid() )
		{
			var st = _go.Components.Get<GunGripState>( FindMode.EverythingInSelf ) ?? _go.Components.Create<GunGripState>();
			if ( !st.FrameDone && ( _anchor is null || _anchorLocal is not null ) )
			{
				if ( GunGrip.Frame( _renderer, _anchorLocal, np.HoldTypeId == (int)HoldTypes.Pistol ) is Transform seat )
				{
					_anchorLocal = seat;
					st.FrameDone = true;
				}
				else if ( ++st.FrameTries > 30 ) st.FrameDone = true;
			}
		}

		// ⚠️ POSITIONED FROM THE BONE EVERY FRAME RATHER THAN PARENTED TO A BONE OBJECT. Bone
		// GameObjects only exist when `CreateBoneObjects` is on, and turning that on for every
		// player's whole skeleton to hang one gun off it is a hundred objects for one.
		// `TryGetBoneTransform` is the same answer without them.
		if ( _bone is null || !body.TryGetBoneTransform( _bone, out var hand ) ) return;

		// ⚠️ THE TUNING IS APPLIED IN THE HAND'S OWN FRAME, not in world space — an offset along
		// world axes would swing the gun around the player as they turned. It moves the GRIP
		// relative to the hand, which is what anybody adjusting this is actually looking at.
		var target = new Transform(
			hand.Position + hand.Rotation * new Vector3( X, Y, Z ),
			hand.Rotation * Rotation.From( Pitch, Yaw, Roll ) );

		// ⚠️ SOLVING FOR THE OBJECT, NOT PLACING IT. We know where the grip must END UP, and
		// where the grip sits inside the mesh; the object transform is whatever puts one on the
		// other. With no anchor this collapses to identity and the mesh origin goes to the hand,
		// which is the old behaviour and still the right fallback for a real world model.
		var grip = _anchorLocal ?? new Transform( Vector3.Zero, Rotation.Identity, 1f );

		var rot = target.Rotation * grip.Rotation.Inverse;

		_go.WorldRotation = rot;
		_go.WorldPosition = target.Position - rot * (grip.Position * Scale);
		_go.WorldScale = Vector3.One * Scale;

		// ⚠️ THE LEFT HAND ONTO A LONG GUN (2026-10-05, `GunGrip.Hands`), now the gun is where it is drawn this frame. Off during a
		// knife swing: the body is in the two-handed melee swing then, with no gun to hold.
		// ⚠️ AND OFF FOR A RELOAD (2026-10-05, `ReloadActive`): pinned to the handguard, the reload's own left hand (the magazine
		// out and in, the shells) never moved.
		GunGrip.Hands( body, _go, _renderer, (HoldTypes)np.HoldTypeId,
			MeleeActive || _shownPath == KnifeViewModel.ModelPath || ReloadActive,
			GunGrip.Untrusted( _anchor ) ? _anchorLocal : null );
	}

	/// <summary>Throw the current gun away so the next frame builds a fresh one.</summary>
	void Drop()
	{
		if ( _go.IsValid() ) _go.Destroy();

		_go = null;
		_renderer = null;
		_anchor = null;
		_anchorLocal = null;
		_warned = false;
		_noHand = false;
	}

	void Build( SkinnedModelRenderer body )
	{
		_sinceBuild = 0f;
		_noHand = false;
		_handModel = body.Model;

		var model = Model.Load( _shownPath );

		// ⚠️ `IsError` TOO, NOT JUST NULL — `Model.Load` returns the ERROR MODEL for a path it
		// cannot resolve, and a checkerboard box in a player's hand looks like a rendering fault
		// rather than a missing asset. The same trap `ZombieAI.EnsureBody` and `Powerup.Spawn`
		// both document.
		if ( model is null || model.IsError )
		{
			if ( !_warned )
			{
				_warned = true;
				Log.Warning( $"[nz-3p] world model '{_shownPath}' will not load — "
					+ "this player will appear empty-handed" );
			}
			return;
		}

		_bone = !string.IsNullOrEmpty( BodyBoneOverride ) && body.TryGetBoneTransform( BodyBoneOverride, out _ )
			? BodyBoneOverride
			: HandBones.FirstOrDefault( b => body.TryGetBoneTransform( b, out _ ) );

		if ( _bone is null )
		{
			_noHand = true;

			if ( !_warned )
			{
				_warned = true;
				Log.Warning( $"[nz-3p] ⛔ no hand bone on '{body.Model?.ResourceName}' — tried "
					+ string.Join( ", ", HandBones ) + ". The gun has nowhere to sit, so it is not "
					+ "being drawn rather than being dropped at the player's feet." );
			}
			return;
		}

		// ⛔ A CHILD OF THE BODY, WHICH IS WHAT HIDES IT IN FIRST PERSON. Tags inherit downward —
		// proven this session — and the engine hides your own body by tagging that object
		// `viewer`. A gun parented anywhere else would be excluded from nothing and would hang in
		// front of your own camera, which is the floating-arms bug with a different mesh.
		//
		// ⚠️ THE TRANSFORM IS STILL WRITTEN IN WORLD SPACE every frame, so the parenting is only
		// there for the tag and for cleanup.
		// ⚠️ CHOSEN AGAINST THE MODEL, NOT THE RENDERER, because the renderer does not exist yet
		// — and `Model.Bones.AllBones` answers "is this name in here" without any of the ambiguity
		// that made `LocalTransform` unusable for the position.
		var names = model.Bones?.AllBones?.Select( b => b.Name ).ToHashSet()
			?? new System.Collections.Generic.HashSet<string>();

		// ⚠️ THE KNIFE BY ITS OWN TAG (2026-10-05). Its BO2 mesh has no `j_gun`, and the ValveBiped hand the list would reach next
		// is the arms' BIND pose, which for the guns sat a metre from the weapon (see `GunAnchors`).
		_anchor = !string.IsNullOrEmpty( GunBoneOverride ) && names.Contains( GunBoneOverride )
			? GunBoneOverride
			: _shownPath == KnifeViewModel.ModelPath && names.Contains( KnifeAnchor )
				? KnifeAnchor
				: GunAnchors.FirstOrDefault( names.Contains );

		_go = new GameObject( true, "Weapon (third person)" );
		_go.SetParent( body.GameObject, false );
		_go.NetworkMode = NetworkMode.Never;
		_go.Flags |= GameObjectFlags.NotSaved;

		_renderer = _go.Components.Create<SkinnedModelRenderer>();
		_renderer.Model = model;

		// ⚠️ ONCE PER GUN AND NEVER FOR THE KNIFE (2026-10-05) — see `_loggedHold`. After a missing-hand warning it is always said.
		if ( _shownPath == KnifeViewModel.ModelPath || (_shownPath == _loggedHold && !_warned) ) return;
		_loggedHold = _shownPath;

		Log.Info( $"[nz-3p] '{GameObject.Name}' is now holding {Leaf( _shownPath )}"
			+ $" — body bone '{_bone}' ← weapon bone '{_anchor ?? "⚠️ none, mesh origin"}'"
			+ ( _warned ? $" (found on '{body.Model?.ResourceName}' after the warning above)" : "" ) );
	}

	protected override void OnDestroy()
	{
		Drop();

		// ⚠️ AND STOP LISTENING TO THE BODY'S TAGS (2026-10-05, `WatchTags`): the body can outlive this component.
		if ( _tagBody.IsValid() ) _tagBody.OnAnimTagEvent -= OnAnimTag;
	}

	/// <summary>
	/// `nz_3p` — WHAT EVERY PLAYER IS HOLDING, AND WHETHER IT FOUND A HAND.
	///
	/// ⛔ THIS FEATURE FAILS IN FOUR PLACES AND THREE OF THEM ARE SILENT: the owner never
	/// published, the value never arrived, the model would not load, or no hand bone matched. Only
	/// a line per player with all four columns tells them apart — and the bone list is a GUESS
	/// about a retargeted rig, so "which bone" is the column most likely to be the answer.
	///
	/// Client output routes to the host, so one capture holds both sides.
	/// </summary>
	[ConCmd( "nz_3p" )]
	public static void Report()
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		void Tell( string line )
		{
			if ( NZGame.IsHost || !Networking.IsActive ) Log.Info( line );
			else NZNet.Say( line );
		}

		var who = NZGame.IsHost ? "HOST  " : "CLIENT";

		foreach ( var go in PlayerSpawner.AllBodies() )
		{
			var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
			if ( !np.IsValid() ) continue;

			var tp = go.Components.Get<ThirdPersonWeapon>( FindMode.EverythingInSelf );
			var body = tp.IsValid() ? tp.Body : null;

			// ⚠️ EVERY CANDIDATE, NOT JUST THE ONE THAT WON. If none matched, the list of what was
			// tried against the model that was actually loaded is the entire diagnosis.
			var found = body.IsValid()
				? HandBones.Where( b => body.TryGetBoneTransform( b, out _ ) ).ToList()
				: new System.Collections.Generic.List<string>();

			Tell( $"[nz-3p] {who} '{go.Name}' mine={PlayerPresence.Mine( go ),-5}"
				+ $" holdtype={(HoldTypes)np.HoldTypeId}"
				+ $" model={(string.IsNullOrEmpty( np.WorldModelPath ) ? "⛔ NONE PUBLISHED" : Leaf( np.WorldModelPath ))}" );

			Tell( $"[nz-3p] {who}   body={(body.IsValid() ? Leaf( body.Model?.ResourceName ) : "⛔ no renderer")}"
				+ $" · hand bones present: {(found.Count == 0 ? "⛔ NONE OF " + string.Join( "/", HandBones ) : string.Join( ", ", found ))}"
				+ $" · gun object {(tp.IsValid() && tp._go.IsValid() ? "yes" : "no")}"
				+ $" · grip {(tp.IsValid() && tp._anchor is not null ? $"'{tp._anchor}' → '{tp._bone}'" : "⚠️ mesh origin")}"
				+ $" {(tp.IsValid() && tp._anchorLocal is not null ? $"@ {tp._anchorLocal.Value.Position}" : "⚠️ NOT MEASURED YET")}"
				+ (tp.IsValid() ? "" : "   ⛔ NO ThirdPersonWeapon COMPONENT ON THIS BODY") );

			// ⛔ THE THREE POSITIONS THAT SAY WHETHER THE SOLVE WORKED, and they are the only thing
			// that does. A gun 300 units from its owner and a gun 3 units out look identical in every
			// column above: same bones found, same offset measured, same object alive. Only the
			// distance between where the grip ENDED UP and where the hand IS separates "the
			// arithmetic is wrong" from "the arithmetic is right and the mesh is authored oddly".
			if ( tp.IsValid() && tp._go.IsValid() && body.IsValid() && tp._bone is not null
				&& body.TryGetBoneTransform( tp._bone, out var handAt ) )
			{
				var gripAt = tp._anchor is not null && tp._renderer.IsValid()
					&& tp._renderer.TryGetBoneTransform( tp._anchor, out var gw )
						? gw.Position
						: tp._go.WorldPosition;

				Tell( $"[nz-3p] {who}   hand at {handAt.Position}   grip at {gripAt}"
					+ $"   → {gripAt.Distance( handAt.Position ):0.#}u apart"
					+ $"   (object {tp._go.WorldPosition.Distance( handAt.Position ):0.#}u out)" );
			}
		}
	}

	/// <summary>
	/// `nz_3p_bones &lt;model path&gt; [filter]` — WHAT BONES DOES THIS MODEL ACTUALLY HAVE.
	///
	/// ⛔ THE HAND BONE LIST IN THIS FILE IS A GUESS, AND SO IS EVERY ASSUMPTION ABOUT WHAT IS IN
	/// A VIEWMODEL. `TryGetBoneTransform` answers "is this exact name here", which can only ever
	/// confirm a name already guessed — it cannot show a name nobody thought of. That is exactly the
	/// blind spot INSTRUCTIONS pattern 25 is about, and this is the command that has no blind spot.
	///
	/// ⚠️ IT WORKS ON ANY MODEL, not just weapons: `nz_3p_bones models/citizen/citizen.vmdl hand`
	/// answers the body half of the same question.
	/// </summary>
	/// <summary>
	/// `nz_anim [param]` — fire a body gesture on YOUR player, broadcast like the real thing.
	/// Default `b_reload`; `nz_anim b_attack` for the knife swing.
	///
	/// ⛔ A GESTURE NOBODY ELSE IS AROUND TO SEE CANNOT BE CHECKED, which is the whole reason this
	/// exists. Reload and swing animations are the definition of a thing that only matters on
	/// SOMEBODY ELSE'S screen, and the bug they were added for went unnoticed for months precisely
	/// because solo play never shows it. This fires the identical broadcast the weapon does, so a
	/// second account is needed to confirm the fix but not to confirm the plumbing.
	///
	/// ⚠️ IT TAKES THE PARAMETER NAME so a wrong one can be RULED OUT rather than argued about.
	/// `holdtype` proved a bad name compiles and animates nothing; `b_deploy` is the third the
	/// engine documents if a draw gesture is ever wanted.
	/// </summary>
	[ConCmd( "nz_anim" )]
	public static void AnimCmd( string param = "b_reload" )
	{
		var np = NZPlayer.Local;
		if ( !np.IsValid() )
		{
			Log.Info( "[nz-3p] no local player" );
			return;
		}

		NZNet.PlayerAnim( np.GameObject.Id, param );
		Log.Info( $"[nz-3p] sent '{param}' for {np.GameObject.Name} — every machine should play it" );
	}

	[ConCmd( "nz_3p_bones" )]
	public static void Bones( string path, string filter = "" )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
		{
			Log.Info( "[nz-3p] nz_3p_bones <model path> [filter]   e.g. nz_3p_bones weapons/m1911/v_m1911.vmdl" );
			return;
		}

		var model = Model.Load( path );

		if ( model is null || model.IsError )
		{
			Log.Warning( $"[nz-3p] '{path}' will not load" );
			return;
		}

		var all = model.Bones?.AllBones?.Select( b => b.Name ).ToList()
			?? new System.Collections.Generic.List<string>();

		var shown = string.IsNullOrEmpty( filter )
			? all
			: all.Where( n => n.Contains( filter, StringComparison.OrdinalIgnoreCase ) ).ToList();

		// ⛔ WHERE THE MESH IS, WHICH IS THE QUESTION EVERY BONE NAME IS A PROXY FOR. Anchoring is
		// only ever trying to put the GEOMETRY in a hand; a bone is just a handle on it, and two
		// plausible handles — the viewmodel's own wrist and `tag_weapon1` — have now each been
		// tried and each left the pistol somewhere else. `Bounds` cannot be posed, renamed or
		// merged into a second skeleton, so it is the one reading that settles it.
		var b = model.Bounds;

		Log.Info( $"[nz-3p] {Leaf( path )} · {all.Count} bones · mesh centre {b.Center} size {b.Size}"
			+ (string.IsNullOrEmpty( filter ) ? "" : $", {shown.Count} matching '{filter}'") );

		// ⚠️ WHICH BONES ARE NEAR THE MESH, when nothing was filtered for. The bone the gun is
		// actually built around is the one sitting inside its own geometry.
		if ( string.IsNullOrEmpty( filter ) )
		{
			var near = model.Bones.AllBones
				.OrderBy( x => x.LocalTransform.Position.Distance( b.Center ) )
				.Take( 8 )
				.Select( x => $"{x.Name} ({x.LocalTransform.Position.Distance( b.Center ):0.#}u)" );

			Log.Info( "[nz-3p]   nearest the mesh centre: " + string.Join( "  ", near ) );
		}

		if ( shown.Count == 0 )
		{
			Log.Info( "[nz-3p]   (none)" );
			return;
		}

		// ⚠️ A FEW MATCHES GET THEIR BIND POSE PRINTED, many get listed. Once a filter has
		// narrowed it to the bone actually in question, WHERE that bone sits is the next thing
		// wanted every single time — and both readings are printed because `Bone.LocalTransform` is
		// undocumented as to whether it is parent-relative or already model-space, and guessing
		// wrong there would silently misplace every gun.
		if ( shown.Count <= 4 )
		{
			foreach ( var name in shown )
			{
				var bone = model.Bones.AllBones.First( b => b.Name == name );

				// ⚠️ `LocalTransform` IS MODEL SPACE, despite the name, and this is the reading that
				// proved it: `ValveBiped_Bip01_R_Hand` reads (-22.69, 2.35, 40.77) here and the live
				// renderer measured the identical figure against the object it was parented to.
				// Composing the parent chain — which the name invites — gave (80.7, -168.8, -11.9),
				// a point fourteen feet from a pistol, and that number is why this line prints the
				// distance to the mesh rather than another transform to be misread.
				Log.Info( $"[nz-3p]   {name}"
					+ $"   parent={(bone.Parent is null ? "—" : bone.Parent.Name)}"
					+ $"   at {bone.LocalTransform.Position}"
					+ $"   → {bone.LocalTransform.Position.Distance( b.Center ):0.#}u from the mesh centre" );
			}

			return;
		}

		// ⚠️ IN ROWS, because a 200-bone rig one name per line buries everything after it and the
		// console buffer this session reads from holds a fixed number of ENTRIES, not lines.
		for ( var i = 0; i < shown.Count; i += 6 )
			Log.Info( "[nz-3p]   " + string.Join( "  ", shown.Skip( i ).Take( 6 ) ) );
	}

	static string Leaf( string path )
		=> string.IsNullOrEmpty( path ) ? "?" : path[(path.LastIndexOf( '/' ) + 1)..];
}