Player/PlayerPresence.cs

Static helper that tracks whether the local player body should exist and finds/enables/disables the player GameObject accordingly. It locates the NZPlayer component in the active scene (including disabled objects), decides ownership (Mine/Theirs), applies character selection and pending spawn data when enabling, and caches a reference with a fallback to scene search.

NetworkingFile Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// Whether a body exists in the world at all.
///
/// ⚠️ THE LOBBY HAD A LIVE PLAYER STANDING IN THE MAP BEHIND IT. The scene ships
/// the Player Controller enabled, so the moment you pressed play a body spawned,
/// took mouse-look, and the lobby drew on top of it — which is why none of its
/// buttons could be clicked. The menu was never broken; something underneath it
/// owned the cursor.
///
/// So presence is a rule, not a scene setting:
///
///     Lobby      no body   — nothing to control, cursor is free for the menu
///     Spectator  no body   — watching, not playing
///     Creative   body      — you build from inside the map
///     Survival   body      — you play
/// </summary>
public static class PlayerPresence
{
	/// <summary>Should there be a player in the world right now?</summary>
	public static bool ShouldExist
		=> NZGame.Mode is GameMode.Creative or GameMode.Survival;

	/// <summary>
	/// Cached so the player can be found again AFTER being disabled.
	///
	/// ⚠️ GetAllComponents skips disabled objects. Without this the first switch
	/// to Lobby would hide the player permanently — there would be nothing left
	/// to search for and no way back into the map.
	/// </summary>
	static GameObject _player;

	/// <summary>Enable or disable the body to match the mode.</summary>
	public static void Apply()
	{
		var go = Find();
		if ( go is null ) return;

		var want = ShouldExist;
		if ( go.Enabled == want ) return;

		go.Enabled = want;
		Log.Info( $"[nz] player {(want ? "spawned into" : "removed from")} the map ({NZGame.Mode})" );

		// ⛔ RE-APPLIED ON EVERY ENTRY, because the object is DISABLED and re-enabled rather than
		// destroyed — and a component coming back from disabled can restore its serialised model.
		// Picking a character in the lobby and then finding a Citizen in the map is the failure this
		// prevents, and it would look like the selection never worked.
		//
		// ⚠️ HERE RATHER THAN IN `NZPlayer.OnStart`, which does not run again on an already-spawned
		// player — the same trap the `Slide` component's comment records.
		if ( want )
			PlayerCharacters.ApplyBody( go.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndDescendants ) );

		if ( !want ) return;

		// ⛔ THE FIRST INSTANT A BODY CAN BE MOVED AND MADE TO STAY MOVED. The host deals the
		// spawns inside `StartGame`, which is the same instant this machine is still leaving the
		// lobby — so its placement message lands with nothing to apply it to and is held. This
		// is where it gets taken. See `PlayerSpawner.Defer`.
		PlayerSpawner.TakePending( go );

		// ⚠️ AND EVERYONE ELSE GETS DRESSED. Character choices replicate as a table, not as a
		// property on the body, so a body that has just come up is wearing whatever the scene
		// shipped until something reads the table — see `NZPlayers.RefreshBodies`.
		NZPlayers.RefreshBodies();
	}

	/// <summary>The player object, enabled or not.</summary>
	public static GameObject Find()
	{
		// ⛔ A CACHED PROXY IS WORSE THAN NO CACHE. With two players the scene holds several
		// bodies and only one of them is ours; a stale entry pointing at somebody else's would
		// send every `PlayerCharacters.Local()` caller — twenty of them — at the wrong player,
		// silently and plausibly.
		if ( _player.IsValid() && Mine( _player ) ) return _player;

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

		// Enabled first — cheap and correct while a body exists.
		var live = scene.GetAllComponents<NZPlayer>()
			.FirstOrDefault( p => p.IsValid() && Mine( p.GameObject ) );

		if ( live.IsValid() ) return _player = live.GameObject;

		// ⚠️ `false` = include DISABLED objects. This is the branch that runs
		// once the player has been removed, and the only way to get them back.
		_player = scene.GetAllObjects( false )
			.FirstOrDefault( o => o.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is not null
				&& Mine( o ) );

		return _player;
	}

	/// <summary>
	/// Is this body THIS machine's to drive?
	///
	/// ⚠️ THIS IS THE CHOKEPOINT THAT MAKES 20 CALL SITES CORRECT AT ONCE.
	/// `PlayerCharacters.Local()` resolves through here, and before it existed "local" meant
	/// "whichever came first" — the right name over the wrong behaviour, which is worse than a
	/// raw lookup because it reads as already solved.
	///
	/// ⛔ IT ASKED `IsProxy` AND THAT WAS NOT SPECIFIC ENOUGH. A proxy is "a network object
	/// that is not being simulated on the local pc", which is exactly right for a body handed to
	/// somebody by `NetworkSpawn( connection )` — and says nothing useful about an object NOBODY
	/// owns. The host's body is the scene's own `Player Controller` at `NetworkMode.Snapshot`;
	/// nothing ever claimed it. If an unowned object reports `IsProxy == false` on a client then
	/// on that machine BOTH bodies answered "mine", and one wrong predicate produced three
	/// separate symptoms at once: the host's body was never shown (you could not see each
	/// other), `Find` could return the HOST'S body as the local one (a character model drawn
	/// over your own hands), and every zombie ran a full local AI against a replicated transform.
	///
	/// ⚠️ SO IT ASKS THE QUESTION DIRECTLY: is the owner me? Ownership is an id, and an id
	/// compares the same on every machine — there is nothing to interpret.
	///
	/// ⚠️ THE UNOWNED CASE IS NAMED RATHER THAN GUESSED AT. An object with no owner belongs
	/// to the host by convention, which is also what `NZPlayers.EnsureHostBody` now makes
	/// explicit — this clause is what keeps the host correct in the frames before it does.
	///
	/// ⚠️ AND IT STILL READS CORRECTLY SOLO. `Connection.Local` is null with nothing
	/// connected, so the last clause hands single player its one scene body, unchanged.
	/// </summary>
	public static bool Mine( GameObject go )
	{
		if ( !go.IsValid() ) return false;

		// ⚠️ SOLO: nothing is connected, so the one body in the scene is ours.
		var me = Connection.Local?.Id.ToString();
		if ( string.IsNullOrEmpty( me ) ) return true;

		// ⛔ THE WRITTEN-DOWN ANSWER FIRST, AND IT IS THE ONLY ONE THAT HAS EVER BEEN RIGHT ON
		// BOTH MACHINES. See `NZPlayer.OwningConnection` for the two derived answers that were
		// tried before it and how each of them failed.
		var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
		var said = np.IsValid() ? np.OwningConnection : "";

		if ( !string.IsNullOrEmpty( said ) ) return said == me;

		// ⚠️ NOTHING RECORDED — FALL BACK TO THE ENGINE, WHICH IS WHAT THIS DID ORIGINALLY.
		// It is reached only by a body created before anybody claimed it, and on the machine that
		// is simulating it that answer is correct. Being no worse than the previous version in
		// the one unhandled case is the point of having a fallback at all.
		return !go.Network.IsProxy;
	}

	/// <summary>Somebody else's body — the exact complement of <see cref="Mine"/>.</summary>
	public static bool Theirs( GameObject go ) => go.IsValid() && !Mine( go );

	/// <summary>Forget the cache — for a scene reload, where the old object is
	/// gone but the static survives.</summary>
	public static void Forget() => _player = null;
}