Net/NZPlayers.cs

Static NZPlayers helper for managing player GameObjects and NZPlayer components in a multiplayer session. It clones either a prefab or the scene player, assigns ownership, network-spawns bodies, ensures the host gets an owned body, strips weapons from cloned bodies, manages which controller/camera/input belongs to which body, tracks movement timestamps, enforces a single viewmodel camera, and updates body appearance from the lobby selection.

NetworkingFile AccessNative Interop
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// ONE BODY PER CONNECTION.
///
/// ⛔ THE SCENE SHIPS EXACTLY ONE `Player Controller`, AND IT IS `NetworkMode.Snapshot`. So a
/// joining client receives a PROXY of the host's body and nothing of its own: it stands in the
/// map unable to move, watching a copy of somebody else. That is the "ghost" — not a missing
/// player, a borrowed one.
///
/// ⚠️ CLONED, NOT INSTANTIATED FROM A PREFAB. There is no player prefab in this project and
/// making one means editing the scene, which this project has a standing rule against doing from
/// code. `GameObject.Clone()` copies the object that is already there, components and all, so the
/// bodies cannot drift from the one a mapper actually sees in the editor.
///
/// ⚠️ AND THE CLONE HAPPENS ON THE HOST. `NetworkSpawn( connection )` is what hands ownership to
/// the joiner — "the owner will be the connection" — which is the difference between a body a
/// client can drive and one it can only watch.
/// </summary>
public static class NZPlayers
{
	/// <summary>The prefab every body is cloned from once <see cref="FromPrefab"/> is on.</summary>
	public const string PlayerPrefabPath = "prefabs/player.prefab";

	/// <summary>
	/// CLONE THE PREFAB INSTEAD OF THE BODY STANDING IN THE MAP.
	///
	/// ⛔ THIS IS THE ENGINE'S OWN PATTERN AND WE ARE THE ONES WHO DEVIATED. Facepunch's
	/// documented spawn is `PlayerPrefab.Clone( spawn )` then `NetworkSpawn( connection )`, in
	/// `INetworkListener.OnActive`. We clone a LIVE SCENE OBJECT — and a live object carries
	/// runtime state that a prefab cannot:
	///
	///   RenderType = ShadowsOnly   → every body invisible          (fixed by asserting it back)
	///   the `viewer` tag           → every body invisible AGAIN     (fixed by stripping it)
	///   a viewmodel + its handler  → floating arms, throwing update (fixed by null-guarding it)
	///
	/// Three separate bugs, three separate fixes, ONE cause. A prefab makes all three impossible
	/// rather than survivable. See `SBOX_MULTIPLAYER.md` §9.1.
	///
	/// ⚠️ DEFAULT OFF, AND A SWITCH RATHER THAN A REPLACEMENT. The old path is the one that
	/// currently gets through a whole game; this one has never run. Both are in the build so the
	/// two can be compared in ONE session, on one map, instead of across two builds a test cycle
	/// apart — which is how the last three "regressions" turned out to be different conditions
	/// rather than different code.
	///
	/// ⚠️ IT IS A STATIC, SO IT SURVIVES HOTLOAD AND SURVIVES A PLAY SESSION. That has cost
	/// this project more time than any other single thing (INSTRUCTIONS pattern 1) and cost it
	/// again this afternoon (pattern 24). The mitigation is that **every spawn says out loud
	/// which path it took** — a surviving static that announces itself cannot silently contaminate
	/// a test.
	/// </summary>
	/// <remarks>
	/// ✅ DEFAULT ON SINCE 2026-09-10, after a session where both players saw each other, wore
	/// their own characters and carried working weapons. It was default-off while unproven; leaving
	/// a proven fix behind a switch that has to be re-typed after every editor restart is a trap,
	/// and the user hit it once already ("do i just start or do i run anything").
	///
	/// ⚠️ THE COMMAND STAYS, so the old path is one word away if something turns up.
	/// </remarks>
	public static bool FromPrefab { get; set; } = true;

	static PrefabFile _prefab;

	/// <summary>
	/// `nz_prefab_players [0/1]` — switch the source of new bodies. No argument reports.
	/// </summary>
	[ConCmd( "nz_prefab_players" )]
	public static void SetFromPrefab( int on = -1 )
	{
		if ( on >= 0 ) FromPrefab = on != 0;

		Log.Info( $"[nz-net] new bodies come from {(FromPrefab ? $"THE PREFAB '{PlayerPrefabPath}'" : "the scene's own player object")}"
			+ $"{(on < 0 ? "  ·  nz_prefab_players 0/1 to change" : "")}" );

		if ( !FromPrefab ) return;

		// ⚠️ SAY NOW WHETHER THE ASSET IS EVEN THERE. Finding out at the spawn means finding out
		// with a joiner already waiting for a body.
		if ( ResourceLibrary.Get<PrefabFile>( PlayerPrefabPath ) is null )
			Log.Warning( $"[nz-net] ⛔ '{PlayerPrefabPath}' DOES NOT EXIST — every spawn will fall "
				+ "back to cloning the scene body." );
		else
			Log.Info( "[nz-net] the prefab is present and loadable" );
	}

	/// <summary>
	/// A FRESH, UNOWNED BODY — from the prefab, or from the scene, whichever is switched on.
	///
	/// ⚠️ THE FALLBACK IS THE OLD PATH, NOT NULL. A missing or unloadable prefab must not mean
	/// "this player gets no body"; it means "we are back where we were this morning", which is a
	/// game that works.
	///
	/// ⚠️ CLONED AT THE TEMPLATE'S TRANSFORM either way, so `PlayerSpawner.PlaceAll` does
	/// exactly the same job afterwards and this change alters WHERE THE COMPONENTS COME FROM and
	/// nothing else.
	/// </summary>
	static GameObject NewBody( GameObject template )
	{
		if ( FromPrefab )
		{
			_prefab ??= ResourceLibrary.Get<PrefabFile>( PlayerPrefabPath );

			if ( _prefab is not null )
			{
				var fresh = GameObject.Clone( _prefab, new CloneConfig
				{
					Transform = template.IsValid() ? template.WorldTransform : global::Transform.Zero,
					StartEnabled = true,
				} );

				Log.Info( $"[nz-net] body cloned from THE PREFAB ({PlayerPrefabPath})" );
				return fresh;
			}

			Log.Warning( $"[nz-net] ⛔ '{PlayerPrefabPath}' would not load — falling back to "
				+ "cloning the scene body" );
		}

		if ( !template.IsValid() ) return null;

		Log.Info( "[nz-net] body cloned from THE SCENE'S OWN PLAYER (the old path)" );
		return template.Clone();
	}

	/// <summary>
	/// Give this connection a body of its own. Host only; no-op for the host itself.
	///
	/// ⛔ THE HOST KEEPS THE SCENE'S OWN PLAYER. It is already there, already enabled by
	/// `PlayerPresence`, and already the thing every single-player code path resolves to. Cloning
	/// a second one for the host would leave two bodies on the machine that has the most code
	/// assuming there is one.
	/// </summary>
	public static void SpawnFor( Connection channel )
	{
		if ( channel is null ) return;
		if ( NZGame.IsClient ) return;

		if ( channel == Connection.Local )
		{
			Log.Info( "[nz-net] host keeps the scene's own body" );
			return;
		}

		var template = PlayerPresence.Find();

		if ( !template.IsValid() )
		{
			Log.Warning( "[nz-net] no player object to copy — cannot give "
				+ $"{channel.DisplayName} a body" );
			return;
		}

		var go = NewBody( template );
		if ( !go.IsValid() ) { Log.Warning( "[nz-net] could not make a body" ); return; }

		go.Name = $"Player ({channel.DisplayName})";

		// ⛔ WHOSE BODY THIS IS, WRITTEN DOWN BEFORE IT IS SPAWNED. This is the one moment the
		// answer is known for certain — the `Connection` is right here — and a `[Property]` set
		// before `NetworkSpawn` is serialised into the spawn, so it arrives on the client as part
		// of the object rather than having to be derived there. Two attempts to derive it on the
		// far end both failed; see `NZPlayer.OwningConnection`.
		//
		// ⚠️ BEFORE `NetworkSpawn`, NOT AFTER. Afterwards it is a local edit to a property that
		// replicates to nobody, and the client would receive a body with no owner recorded —
		// which is exactly the state that left it standing at the scene's default position with
		// no body it would admit to owning.
		var claim = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
		if ( claim.IsValid() ) claim.OwningConnection = channel.Id.ToString();

		Disarm( go );

		// ⚠️ NOT SAVED WITH THE SCENE. These exist for the length of a session; a mapper opening
		// the scene later should find the one body it ships with, not everyone who ever joined.
		go.Flags |= GameObjectFlags.NotSaved;

		// ⛔ ENABLED FOR THE SPAWN, WHATEVER THE MODE. A game is started FROM THE LOBBY, where
		// `PlayerPresence` has every body switched off — so a clone made there is born disabled,
		// and network-spawning a disabled object is not something this project can show works.
		// The cost of being wrong about that is total: nobody receives the body, and both players
		// stand in an empty map. Enabling first removes the question.
		//
		// ⚠️ AND NOTHING HAS TO PUT IT BACK. `PlayerPresence.Apply` owns my own body's enabled
		// state and `RefreshBodies` owns everybody else's, both from the twice-a-second tick, so
		// a body enabled here is switched off again within half a second if the mode says so.
		go.Enabled = true;

		go.NetworkSpawn( channel );

		Log.Info( $"[nz-net] gave {channel.DisplayName} a body — owner={go.Network.OwnerId}" );

		if ( !go.Network.Active )
			Log.Warning( $"[nz-net] ⛔ {channel.DisplayName}'s body did NOT become a network "
				+ "object — they will have no body at all. nz_see there will say so." );

		// ⚠️ AND THE HOST CLAIMS ITS OWN, so every body in the scene has a named owner and
		// "whose is this" stops being a question anybody has to interpret.
		EnsureHostBody();

		// ⛔ AND IT IS MOVED OFF THE HOST IMMEDIATELY. A clone starts exactly where its template
		// stands, so without this every joiner arrives inside the host — which killed the client
		// on contact and read as a spawn-point bug, because it was one. Placing everybody rather
		// than just the newcomer is deliberate IN THE LOBBY: the spawn list is dealt as a hand, so
		// adding a player changes where the others should be standing too.
		//
		// ⛔ BUT NOT ONCE A GAME IS ON — the newcomer alone (the co-op audit, 2026-09-27). Every join moved every body to the
		// map's spawns, pulling the whole team out of basalt's boss arena. The newcomer goes into the arena if the fight is on
		// (*"make the new player spawn in the boss arena"*), beside a standing teammate while the lava is out of its bed (the
		// spawns lie under it), and to a spawn otherwise (`HexPlatforms.PlaceJoiner`).
		if ( NZGame.Mode == GameMode.Survival )
		{
			if ( !HexPlatforms.PlaceJoiner( go ) )
				PlayerSpawner.PlaceAll( only: b => b == go );
		}
		else PlayerSpawner.PlaceAll();

		// ⚠️ THE NEW BODY WEARS THE CHARACTER ITS OWNER PICKED IN THE LOBBY, which the net table
		// already knows — the clone would otherwise wear whatever the host is wearing.
		RefreshBodies();
	}

	/// <summary>
	/// GIVE THE HOST A BODY OF THE SAME KIND EVERYBODY ELSE HAS.
	///
	/// ⛔ THE SCENE'S `Player Controller` DOES NOT REPLICATE, AND THAT IS MEASURED, NOT INFERRED.
	/// It is `NetworkMode.Snapshot` and nobody owns it — `TakeOwnership` was tried and does not
	/// take — so no machine transmits its transform. `nz_bodies`, both ends, same moment:
	///
	///     HOST    'Player Controller'  pos=2211,406,160   still=26s   char='richtofen'
	///     CLIENT  'Player Controller'  pos=-60,1504,0     still=86s ⚠ NOT MOVING   char=''
	///
	/// −60,1504,0 is the map's own default spawn: the client was watching the frozen instant the
	/// join snapshot captured, and had been for the whole game.
	///
	/// ⚠️ SO THE FIX IS SYMMETRY, NOT ANOTHER SPECIAL CASE. A `NetworkSpawn( connection )`-ed
	/// clone is the one arrangement already PROVEN to replicate here — the client's body does it
	/// correctly in the same log (`still=9s`, position agreeing with its owner). The host now gets
	/// one too, and every body in the game is the same kind of object with the same owner rule.
	///
	/// ⛔ AND THE SCENE'S ORIGINAL IS DESTROYED. Leaving it would keep a second, stale
	/// `Player Controller` in every client's scene — the object that has caused a camera hijack, a
	/// hidden player and a frozen body across three test rounds. Destroying a scene object at
	/// runtime does not touch the `.scene` file; the next play starts from it unchanged.
	///
	/// ⚠️ HOST ONLY, AND ONLY WHILE NETWORKING IS ACTIVE. Single player keeps the scene's body
	/// exactly as it always has — no clone, no destroy, nothing to go wrong.
	///
	/// ⚠️ IT MUST RUN BEFORE ANYBODY CONNECTS, which is why the tick calls it: a joining
	/// client's scene IS the host's snapshot, so the swap has to have happened before the snapshot
	/// is taken. `NZNetListener` ticks twice a second from the moment the game enters any mode.
	/// A client that somehow joins inside that window is covered from the other side — see
	/// `DropUnclaimed`.
	/// </summary>
	public static void EnsureHostBody()
	{
		if ( !Networking.IsActive || NZGame.IsClient ) return;

		var me = Connection.Local;
		if ( me is null ) return;

		var mine = me.Id.ToString();

		// ⚠️ ALREADY DONE IS THE COMMON CASE — this runs twice a second forever.
		var bodies = PlayerSpawner.AllBodies();
		if ( bodies.Any( b => OwnerOf( b ) == mine ) ) return;

		// The scene's own body: the one nobody has claimed.
		var original = bodies.FirstOrDefault( b => string.IsNullOrEmpty( OwnerOf( b ) ) );
		if ( !original.IsValid() ) return;

		var go = NewBody( original );
		if ( !go.IsValid() ) return;

		go.Name = $"Player ({me.DisplayName})";
		go.Flags |= GameObjectFlags.NotSaved;

		// ⚠️ WRITTEN BEFORE THE SPAWN, same as a joiner's body — a `[Property]` set beforehand
		// is serialised into the spawn, and set afterwards replicates to nobody.
		var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
		if ( np.IsValid() ) np.OwningConnection = mine;

		Disarm( go );

		// ⛔ ENABLED FOR THE SPAWN, WHATEVER THE MODE. A game is started FROM THE LOBBY, where
		// `PlayerPresence` has every body switched off — so a clone made there is born disabled,
		// and network-spawning a disabled object is not something this project can show works.
		// The cost of being wrong about that is total: nobody receives the body, and both players
		// stand in an empty map. Enabling first removes the question.
		//
		// ⚠️ AND NOTHING HAS TO PUT IT BACK. `PlayerPresence.Apply` owns my own body's enabled
		// state and `RefreshBodies` owns everybody else's, both from the twice-a-second tick, so
		// a body enabled here is switched off again within half a second if the mode says so.
		go.Enabled = true;

		go.NetworkSpawn( me );

		// ⛔ SAY WHETHER IT ACTUALLY TOOK, BEFORE DESTROYING THE ONLY ALTERNATIVE. The line
		// below removes the scene body that clients would otherwise have received, so "the spawn
		// silently did nothing" is the one outcome that must never pass unremarked.
		if ( !go.Network.Active )
			Log.Warning( "[nz-net] ⛔ the new host body did NOT become a network object — nobody "
				+ "will receive it. Keeping the scene's original rather than leaving everyone "
				+ "with nothing." );

		// ⛔ THE CACHE POINTS AT AN OBJECT THAT IS ABOUT TO NOT EXIST. `PlayerPresence` holds the
		// last body it resolved, and twenty call sites go through it.
		PlayerPresence.Forget();

		// ⚠️ AND ONLY IF THERE IS SOMETHING TO REPLACE IT WITH. A frozen mannequin every client
		// can see is a bad outcome; no body at all is a worse one.
		if ( go.Network.Active ) original.Destroy();

		Log.Info( $"[nz-net] the host's body is now a spawned, owned one — {mine}"
			+ " (the scene's unowned copy is gone; it never replicated)" );
	}

	// ⛔ `DropUnclaimed` WAS HERE AND IT DELETED THE ENTIRE GAME. It destroyed any body whose
	// recorded owner read empty, on the theory that on a client such a body could only be the
	// stale scene copy. It had **no exclusion for the client's own body** — so the first time
	// that record was not readable, a client wiped every body it had, including its own:
	//
	//     [nz-see] I am the CLIENT · my body ⛔ I HAVE NONE
	//     [nz-see] ⛔ THERE IS NO OTHER PLAYER'S BODY IN MY SCENE AT ALL.
	//
	// ⚠️ IT WAS NEVER WORTH ITS RISK. It existed for one rare case — a client joining in the
	// half-second before the host swaps its own body — and the stale copy it was meant to catch
	// is now handled properly anyway: `EnsureHostBody` destroys the scene original ON THE HOST,
	// and that destroy replicates. A cleanup pass that can delete a player is not a cleanup pass.
	//
	// ⚠️ AND NOTHING ELSE MAY DESTROY A BODY ON A GUESS. `RemoveFor` destroys one, and it does
	// so on a fact: the engine told us that connection left.

	/// <summary>
	/// Whose body is this, as the host wrote it down. "" when nobody has said.
	///
	/// ⚠️ THE ONE READER, so `Mine`, the character lookup and the disconnect sweep cannot
	/// disagree about a body — which they did, because two of them asked the engine instead.
	/// </summary>
	public static string OwnerOf( GameObject go )
	{
		var np = go.IsValid() ? go.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) : null;
		return np.IsValid() ? np.OwningConnection ?? "" : "";
	}

	/// <summary>
	/// THE NAME TO SHOW A HUMAN FOR THIS PLAYER — their PROFILE name, never their character's.
	/// "" when this machine cannot work out whose body it is.
	///
	/// ⛔ A PLAYER IS NOT THEIR CHARACTER, AND THREE PLACES HAD CONFLATED THEM. The revive
	/// prompt read "Hold E - Revive Dempsey", which is the body's costume rather than the person
	/// wearing it — and two people can pick the same character, at which point the prompt names
	/// neither of them. Requested plainly: *"i dont want the player names to become the character
	/// names, they keep their profile names"*.
	///
	/// ⚠️ THROUGH `OwnerOf`, NOT `Network.Owner`. `NZPlayer.OwningConnection` carries a screen
	/// of comment about why the engine's own answer was wrong in both directions for these bodies,
	/// and the Scoreboard's private copy of this lookup asked `Network.Owner` — so it printed the
	/// object name for exactly the unowned host body that `OwningConnection` exists to describe.
	/// One reader, the same one `Mine` and the disconnect sweep already use.
	///
	/// ⚠️ IT RETURNS "" RATHER THAN A FALLBACK, because the right fallback differs by caller:
	/// the scoreboard wants the object's name in the cell, the revive prompt wants "your
	/// teammate" in the sentence. A helper that picked one would be wrong in the other.
	/// </summary>
	public static string NameOf( NZPlayer p )
	{
		if ( !p.IsValid() ) return "";

		var said = OwnerOf( p.GameObject );
		if ( !string.IsNullOrEmpty( said ) )
		{
			foreach ( var c in Connection.All )
				if ( c.Id.ToString() == said && !string.IsNullOrWhiteSpace( c.DisplayName ) )
					return c.DisplayName;
		}

		// ⚠️ MY OWN BODY IS THE CASE THAT HAS NO OWNER RECORDED IN SINGLE PLAYER, where
		// `Connection.All` is just me and nothing ever ran the claim path.
		if ( PlayerPresence.Mine( p.GameObject )
			&& !string.IsNullOrWhiteSpace( Connection.Local?.DisplayName ) )
			return Connection.Local.DisplayName;

		return "";
	}

	/// <summary>
	/// Whose body is this id's, on this machine. Null when they have none here yet.
	///
	/// ⚠️ THE INVERSE OF `OwnerOf`, AND IT LIVES BESIDE IT ON PURPOSE. A relay that names a
	/// player names them by connection id — that is the one handle every machine agrees on — and
	/// every such relay then needs the body. Three of them had already grown their own sweep.
	///
	/// ⚠️ RETURNS THE COMPONENT, not the GameObject, because every caller wants the player.
	/// </summary>
	public static NZPlayer BodyOf( string ownerId )
	{
		if ( string.IsNullOrEmpty( ownerId ) ) return null;

		foreach ( var go in PlayerSpawner.AllBodies() )
		{
			var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
			if ( np.IsValid() && np.OwningConnection == ownerId ) return np;
		}

		return null;
	}

	/// <summary>
	/// Give or take the things that make a body this machine's to drive.
	///
	/// ⛔ `UseCameraControls` IS THE ONE THAT BIT FIRST. A second controller that thinks it is
	/// local does not announce itself — it just moves `Scene.Camera` to its own eye, and
	/// whichever of the two ran last that frame wins. The player sees a plausible first-person
	/// view of the wrong body and has no way to tell.
	///
	/// ⛔ AND `HideBodyInFirstPerson` IS WHY THE OTHER PLAYER WAS INVISIBLE. It is on in the
	/// scene and the engine applies it to any body it considers LOCAL — which, on a client, the
	/// host's unowned body was. So the controller hid it, exactly as it would hide your own body
	/// in first person. Set from `mine` rather than forced false, so your own still disappears.
	///
	/// ⚠️ THE CONTROLLER STAYS ENABLED. It drives the animation and the renderer, which is how
	/// somebody else's body looks like a person rather than a statue.
	///
	/// ⚠️ ALL FOUR FIELDS ARE IN THE GUARD. Checking one and setting four is how a field
	/// silently stops being applied the moment anything else writes it.
	/// </summary>
	static void Control( GameObject go, bool mine )
	{
		var ctrl = go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
		if ( !ctrl.IsValid() ) return;

		// ⛔ SOMEBODY ELSE'S BODY IS ALWAYS DRAWN, AND THIS IS WHY IT WAS NOT.
		//
		// `HideBodyInFirstPerson` is true in the scene, so the engine sets YOUR OWN body renderer
		// to `ShadowsOnly` while you are in first person — that is how you do not see your own
		// torso. `RenderType` is a serialised property on the renderer, so **`Clone()` copies it**:
		//
		//   host in first person  →  its renderer is ShadowsOnly
		//   EnsureHostBody clones the body   → the clone is ShadowsOnly
		//   SpawnFor clones that for a joiner → every body in the session is ShadowsOnly
		//
		// ShadowsOnly renders shadows and nothing else, so every check passes — enabled, right
		// model, right position, right scale — and nobody can see anybody. Hosting in THIRD person
		// happened to clone a visible renderer, which is exactly the difference the user found.
		//
		// ⚠️ AND NOTHING PUT IT BACK, because the engine only writes `RenderType` for the body it
		// considers yours. Setting `HideBodyInFirstPerson = false` on someone else's body stops it
		// being hidden; it does not un-hide one that arrived that way.
		//
		// ⚠️ ONLY THE BODY RENDERER, NAMED THROUGH `PlayerController.Renderer`. Three previous
		// attempts touched renderers found by searching and every one of them made bodies vanish;
		// this one writes to exactly the renderer the controller itself points at, and to nothing
		// else. Viewmodels are not touched.
		//
		// ⚠️ AND ONLY ON BODIES THAT ARE NOT MINE — my own is the engine's to hide and show, and
		// fighting it would put my own torso in my face in first person.
		if ( !mine && ctrl.Renderer.IsValid()
			&& ctrl.Renderer.RenderType != ModelRenderer.ShadowRenderType.On )
		{
			ctrl.Renderer.RenderType = ModelRenderer.ShadowRenderType.On;
			Log.Info( $"[nz-net] '{go.Name}' was ShadowsOnly — drawing it. A body cloned from a "
				+ "first-person player inherits its hidden renderer." );
		}

		// ⛔ AND THE SAME STORY A SECOND TIME, THROUGH A TAG INSTEAD OF A RENDER TYPE.
		//
		// `Sandbox.PlayerController` hides your own body by tagging its renderer's object
		// **`viewer`**, which the camera it drives excludes. The literal sits in the engine
		// assembly between "Body" and the type name itself — it is the controller's, not ours, and
		// nothing in this project writes it.
		//
		// ⚠️ A TAG IS PART OF A GameObject, SO `Clone()` COPIES IT, exactly as it copies
		// `RenderType` above. The chain is identical and it starts in the same place:
		//
		//   host in FIRST person   →  the engine tags its own 'Body' object `viewer`
		//   EnsureHostBody clones the body    → the clone is tagged
		//   SpawnFor clones that for a joiner → EVERY body in the session is tagged
		//
		// ⚠️ AND NOTHING TAKES IT OFF AGAIN. The controller only manages the tag on the body it
		// considers ITS OWN; a body that arrived already tagged is nobody's to untag. Which is
		// exactly why fixing `RenderType` alone did not fix the symptom — two mechanisms hide a
		// first-person body and the clone inherits both.
		//
		// ⚠️ THIS IS THE WHOLE OF "it matters whether third person was on when the server was
		// made". Hosting in third person clones an UNTAGGED body, so everyone sees everyone;
		// hosting in first person clones a tagged one, so nobody sees anybody. It was never the
		// build.
		//
		// ⚠️ NOT MINE ONLY, and only the object the controller itself points at. My own body is
		// the engine's to hide, and taking `viewer` off it would put my own torso in my face.
		if ( !mine && ctrl.Renderer.IsValid() && ctrl.Renderer.GameObject.Tags.Has( "viewer" ) )
		{
			ctrl.Renderer.GameObject.Tags.Remove( "viewer" );
			Log.Info( $"[nz-net] '{go.Name}' was tagged 'viewer' — untagged. A body cloned from a "
				+ "first-person player inherits the tag that hides it." );
		}

		// ⛔ A PLAYER WHO IS OUT OF THE ROUND DOES NOT STEER, AND THIS POLL IS WHY IT HAS TO BE
		// SAID HERE. `NZPlayer.ApplyOutOfRoundBody` takes their collider, gravity and body away
		// every frame — but `UseInputControls` is one of the four fields THIS method asserts twice
		// a second, so anything written elsewhere is put back within 500ms and reads as an
		// intermittent fault.
		//
		// ⚠️ INPUT ONLY. The camera and the look controls stay with them, which is what makes it
		// a spectator rather than a freeze: no body, no collision, cannot move, can still look
		// around from where they fell until the round brings them back.
		var sittingOut = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is { IsOutOfRound: true };

		// ⛔ NOR DOES ONE IN A PANZER'S CLAW (`PanzerGrab`, 2026-10-06 — no time limit, they must knife their way out): put back here, the
		// legs would return for a frame and this method would log twice a second for as long as he holds them
		// — nor one in the Mimic's tentacle (`MimicGrab`, 2026-10-07), the same hold
		var held = go.Components.Get<PanzerGrab>( FindMode.EverythingInSelf ).IsValid()
			|| go.Components.Get<MimicGrab>( FindMode.EverythingInSelf ).IsValid();
		var steers = mine && !sittingOut && !held;

		if ( ctrl.UseCameraControls == mine
			&& ctrl.UseInputControls == steers
			&& ctrl.UseLookControls == mine
			&& ctrl.HideBodyInFirstPerson == mine ) return;

		ctrl.UseCameraControls = mine;
		ctrl.UseInputControls = steers;
		ctrl.UseLookControls = mine;
		ctrl.HideBodyInFirstPerson = mine;

		Log.Info( $"[nz-net] '{go.Name}' is {(mine ? "MINE to drive" : "somebody else's — camera, look, input and first-person hiding taken off it")}" );
	}

	// ══ is that body actually moving? ═══════════════════════════════════════

	/// <summary>Where each body was last seen, and when it last actually moved.</summary>
	static readonly System.Collections.Generic.Dictionary<System.Guid, (Vector3 At, RealTimeSince Since)> _moved = new();

	/// <summary>
	/// Note whether a body has moved since the last check. Called from the twice-a-second tick.
	///
	/// ⛔ "I CANNOT SEE THE OTHER PLAYER" HAS TWO CAUSES THAT LOOK IDENTICAL FROM INSIDE THE
	/// GAME: their body is hidden, or their body is somewhere else entirely because its position
	/// never replicated and it is still standing where the join snapshot left it. The first is a
	/// render flag; the second is a networking fault. Nothing on screen separates them — in both
	/// cases you walk around an empty map.
	///
	/// ⚠️ A BODY THAT HAS NOT MOVED WHILE ITS OWNER IS PLAYING IS THE SECOND ONE. That is what
	/// `nz_see` reports as link 4, and it needs no second machine to interpret.
	/// </summary>
	public static void NoteMovement()
	{
		foreach ( var go in PlayerSpawner.AllBodies() )
		{
			var here = go.WorldPosition;

			if ( !_moved.TryGetValue( go.Id, out var last ) )
			{
				_moved[go.Id] = (here, 0f);
				continue;
			}

			// ⚠️ A UNIT OF SLACK. A body resting on a slope creeps, and a jitter of a hundredth
			// of an inch is not "moving" in the sense this question is asking.
			if ( last.At.Distance( here ) > 1f )
				_moved[go.Id] = (here, 0f);
		}
	}

	/// <summary>How long since this body last moved, or -1 if it has never been seen.</summary>
	public static float StillFor( GameObject go )
		=> go.IsValid() && _moved.TryGetValue( go.Id, out var last ) ? (float)last.Since : -1f;

	/// <summary>
	/// Take every weapon off a freshly cloned body, BEFORE it is network-spawned.
	///
	/// ⛔ A CLONE COPIES THE RUNTIME WEAPON OBJECTS, AND `NetworkSpawn` THEN SENDS THE WHOLE
	/// HIERARCHY. So the moment the host's body was cloned into a spawned one, every client
	/// started receiving the HOST'S GUN as a real object in their scene — which SWB then drew as
	/// a viewmodel: *"weird floating arms holding the gun that move around"*. `nz_bodies` had
	/// already been reporting `weapon: Weapon` on somebody else's body and it read as normal.
	///
	/// ⚠️ WEAPONS ARE PER-MACHINE STATE IN THIS PROJECT. `NZPlayer.GiveWeapon` arms whichever
	/// body is this machine's, locally, and `SERVER_SPLIT.md` files inventory under HOST for
	/// later. Nothing about a gun is supposed to cross the wire yet — so a body must arrive BARE
	/// and be armed where it lives.
	///
	/// ⚠️ AND A SECOND WEAPON IS WORSE THAN A VISIBLE ONE. A machine holding both a networked
	/// copy and its own would have SWB running two `Weapon` components over one body, one of them
	/// a proxy that skips every input branch — which is a very good way to be unable to shoot
	/// while the knife, a separate component, keeps working.
	///
	/// ⚠️ BEFORE THE SPAWN, NOT AFTER. Afterwards the objects have already been serialised into
	/// it and destroying them is a second message racing the first.
	/// </summary>
	static void Disarm( GameObject body )
	{
		var taken = 0;

		// ⚠️ MATERIALISED WITH ToList FIRST — destroying as you enumerate the live component
		// list is the classic way to miss half of them.
		foreach ( var w in body.Components
			.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants ).ToList() )
		{
			// ⚠️ NEVER THE BODY ITSELF. A weapon component is expected on a CHILD, but
			// `EverythingInSelfAndDescendants` includes self — and destroying the root here
			// would delete the player instead of disarming them.
			if ( !w.IsValid() || w.GameObject == body ) continue;
			w.GameObject.Destroy();
			taken++;
		}

		foreach ( var w in body.Components
			.GetAll<NZWeapon>( FindMode.EverythingInSelfAndDescendants ).ToList() )
		{
			if ( !w.IsValid() || !w.GameObject.IsValid() || w.GameObject == body ) continue;
			w.GameObject.Destroy();
			taken++;
		}

		if ( taken > 0 )
			Log.Info( $"[nz-net] stripped {taken} weapon object(s) off the new body — it gets armed "
				+ "where it lives, not over the wire" );
	}

	/// <summary>Take the body back when its owner leaves.</summary>
	public static void RemoveFor( Connection channel )
	{
		if ( channel is null || NZGame.IsClient ) return;

		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		// ⚠️ MATCHED ON THE OWNER ID, not the name. The name is for a human reading the hierarchy;
		// two players called the same thing would otherwise take each other's bodies away.
		var mine = PlayerSpawner.AllBodies( scene )
			.FirstOrDefault( o => OwnerOf( o ) == channel.Id.ToString() );

		if ( !mine.IsValid() ) return;

		Log.Info( $"[nz-net] removing {channel.DisplayName}'s body" );
		mine.Destroy();
	}

	/// <summary>
	/// Make every body in the scene right: exactly one of them mine to drive, and everybody
	/// else's present and wearing the character they picked.
	///
	/// ⛔ A COMPONENT PROPERTY DOES NOT REPLICATE UNLESS IT SAYS SO, AND NOT ONE IN THIS PROJECT
	/// DOES — there is no `[Sync]` anywhere in it. `NZPlayer.CharacterId` and the model
	/// `ApplyBody` writes into the renderer are therefore purely local: every machine sees its
	/// own player as the character it picked and everybody else as whatever the scene shipped.
	/// The lobby already replicates the choice through `NZNet.SetCharacter`, so the fix is to
	/// read it back out and apply it here rather than to network the body.
	///
	/// ⛔ PROXIES ONLY, AND THAT GUARD IS LOAD-BEARING. Our own body is authored locally — by the
	/// lobby, by `nz_character` — and the net table is empty in single player, so applying this to
	/// our own body would clear the character we just picked the moment nothing was connected.
	/// </summary>
	/// <summary>
	/// EXACTLY ONE VIEWMODEL CAMERA, AND IT HANGS OFF MY OWN BODY.
	///
	/// ⛔ MEASURED: `nz_vm_clear` on the host reported **"removed 2 viewmodel camera(s)"** while
	/// `nz_vm_priority`, run seven seconds earlier, had listed only one. So there were two, and one
	/// of them was invisible to a straight `GetAllComponents` sweep — disabled, or on a disabled
	/// object.
	///
	/// ⚠️ A SECOND VIEWMODEL CAMERA DRAWS THE ARMS A SECOND TIME. Both are Priority 2 with
	/// RenderTags { viewmodel, light }, so both composite the viewmodel layer, and the copy appears
	/// displaced by the gap between them — which is exactly the reported "a copy at double my
	/// distance from the host, playing my animations".
	///
	/// ⚠️ A SWEEP RATHER THAN ANOTHER GUARD. `Weapon.CreateModels` and `KnifeViewModel.Ensure` are
	/// now both blocked on remote bodies, which stops NEW ones — but a camera made before that
	/// guard existed, or by a path nobody has found yet, lives forever, because every creation site
	/// checks `Owner.ViewModelCamera` and a stray one is not it. This runs twice a second, costs a
	/// list walk, and cannot leave a duplicate standing.
	///
	/// ⚠️ DISABLED OBJECTS INCLUDED, which is the whole reason the earlier count disagreed.
	/// </summary>
	static void OneViewModelCamera()
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		var mine = PlayerPresence.Find();
		if ( !mine.IsValid() ) return;

		// ⛔ KEEP THE CAMERA MY OWN VIEWMODEL IS ACTUALLY USING. The first version of this kept
		// "the one under my body, else the first in the list", and the first in the list turned out
		// to be the DISABLED one — so it destroyed the live camera and the player was left with no
		// hands at all. The user's report was exact: *"all arms are gone for the client, however
		// this also removes its own hands."*
		//
		// ⚠️ THE HANDLER IS THE ONLY HONEST ANCHOR. `ViewModelHandler.Camera` is, by definition,
		// the camera drawing that viewmodel. Anything derived from parenting or list order is a
		// guess about which camera matters, and both guesses have now been wrong.
		var keep = mine.Components
			.GetAll<SWB.Base.ViewModelHandler>( FindMode.EverythingInSelfAndDescendants )
			.Where( h => h.IsValid() && h.Camera.IsValid() )
			.Select( h => h.Camera )
			.ToHashSet();

		var cams = scene.GetAllObjects( false )
			.SelectMany( o => o.Components.GetAll<CameraComponent>( FindMode.EverythingInSelf ) )
			.Where( c => c.IsValid() && c.RenderTags.Has( SWB.Shared.TagsHelper.ViewModel ) )
			.ToList();

		if ( cams.Count <= 1 ) return;

		// ⛔ NEVER DESTROY THE LAST ONE. If no handler of mine claims a camera — the moment before
		// a weapon deploys, or a frame after a rebuild — there is nothing to compare against, and
		// removing anything then is how a player ends up blind. Do nothing and try again in half a
		// second; a duplicate for one tick is survivable, no viewmodel is not.
		if ( keep.Count == 0 )
		{
			Log.Info( $"[nz-net] {cams.Count} viewmodel cameras, but none of my viewmodels claims "
				+ "one yet — leaving them alone this tick rather than risk taking the live one" );
			return;
		}

		var gone = 0;

		foreach ( var c in cams )
		{
			if ( keep.Contains( c ) ) continue;
			c.GameObject.Destroy();
			gone++;
		}

		if ( gone == 0 ) return;

		Log.Info( $"[nz-net] destroyed {gone} duplicate viewmodel camera(s), kept the {keep.Count} "
			+ "my own viewmodel is drawing through — each duplicate was drawing the arms again, "
			+ "displaced by the gap between it and the real one" );
	}

	public static void RefreshBodies()
	{
		OneViewModelCamera();

		foreach ( var go in PlayerSpawner.AllBodies() )
		{
			// ⛔ EXACTLY ONE CONTROLLER PER MACHINE MAY DRIVE THE CAMERA, AND UNTIL NOW TWO DID.
			// The scene's `Player Controller` is unowned as far as the engine is concerned —
			// `TakeOwnership` on a snapshot object is measured NOT to take — so on a client that
			// body's `PlayerController` believed it was local and took the camera, the look and
			// the input. The client was therefore looking out of the HOST'S body, frozen wherever
			// the join snapshot left it, while its own body walked to the spawn point the host had
			// dealt it. Both machines were right about where that player was; they were watching
			// different bodies.
			//
			// ⚠️ THE CONTROLLER STAYS ENABLED. It drives the animation and the renderer, which
			// is how somebody else's body looks like a person rather than a statue. Only the three
			// things that make a body YOURS are taken away.
			Control( go, PlayerPresence.Mine( go ) );

			// ⛔ `PlayerPresence.Theirs`, NOT `!IsProxy`. An UNOWNED object — which the host's
			// scene body was until `EnsureHostBody` replaced it — is not a proxy on anybody's machine, so
			// the old test skipped the host's player on the client entirely: never enabled, never
			// dressed, invisible. One predicate, and it is now the same one `PlayerPresence.Find`
			// uses, so "mine" and "theirs" cannot disagree.
			if ( !PlayerPresence.Theirs( go ) ) continue;

			// ⛔ A BODY THAT ARRIVED DISABLED STAYS DISABLED UNTIL SOMEBODY ENABLES IT. A client
			// joins during the LOBBY, where `PlayerPresence` has every body switched off, so the
			// host's player reaches it as a disabled object — and the host enabling its own copy
			// later is not something this machine is guaranteed to be told about. Whatever the
			// engine does or does not replicate, the rule here is local and unambiguous: if
			// bodies should exist in this mode, the other players' bodies exist.
			//
			// ⚠️ DRIVEN BY *MY* MODE, WHICH IS THE HONEST APPROXIMATION. Everyone leaves the
			// lobby on one broadcast (`NZNet.GameStarting`), so "I am in the map" and "they are in
			// the map" part company only for the moment in between.
			var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );

			if ( go.Enabled != PlayerPresence.ShouldExist )
			{
				go.Enabled = PlayerPresence.ShouldExist;
				Log.Info( $"[nz-net] '{go.Name}' {(go.Enabled ? "shown" : "hidden")}" );

				// ⚠️ SHOWN AGAIN, DRESSED AGAIN (2026-10-05). A renderer coming back from disabled can put back its serialised
				// model (the reason `PlayerPresence.Apply` re-applies MY body on every entry), so the check below re-applies this one.
				if ( go.Enabled && np.IsValid() ) np.BodyLook = null;
			}

			if ( !np.IsValid() ) continue;

			// ⛔ THE RECORDED OWNER, NOT `Network.OwnerId`. The host's body reads
			// `owner=········ (UNOWNED)` on both machines, so this looked the character up under
			// `Guid.Empty`, got "" back, and left the host wearing `citizen.vmdl` on every
			// client while the lobby had correctly agreed they were Richtofen. Measured:
			//
			//     [nz-bodies] 'Player Controller'  said=58a5f8fb  owner=········ (UNOWNED)
			//                 char=''  model=citizen.vmdl
			//
			// `said` was right there the whole time. Every "whose is this" question goes through
			// the recorded value now, including this one.
			// ⛔ THE OWNER IS THE KEY, NOT THE ANSWER. This briefly read `OwnerOf( go )` on its own
			// and dressed every remote player in their own CONNECTION ID —
			// `'Player (Ned)' is now wearing '62798f42-7ef2-47ec-8f05-3349101ebc49'` — which
			// `PlayerCharacters.Find` then failed to resolve, leaving the default body. Caught by
			// the first unattended two-machine run, seconds after it was introduced.
			var want = NZNet.CharacterOf( OwnerOf( go ) );

			// ⚠️ "" AND null ARE THE SAME STATE — no character, weapon default hands — and they
			// arrive from different places, so comparing the raw strings would re-dress the body
			// every call and log a change that never happened.
			// ⚠️ AND ONCE EVEN WITH NO CHARACTER (2026-10-05): `BodyLook` is what this machine last dressed the body as, so a
			// teammate who never picks anyone is put in their own s&box avatar instead of keeping the prefab's Citizen.
			if ( (np.CharacterId ?? "") == want && np.BodyLook == want ) continue;

			var changed = (np.CharacterId ?? "") != want;
			np.CharacterId = string.IsNullOrEmpty( want ) ? null : want;
			PlayerCharacters.ApplyBody( np );

			if ( changed ) Log.Info( $"[nz-net] '{go.Name}' is now wearing '{want}'" );
		}
	}
}