A network listener component for the NZombies game that runs on the host. It ensures a single listener exists, refreshes player bodies periodically, warns when the local machine has no body, handles a new connection becoming active by pushing map/config and spawning a body for them, responds to disconnects by cleaning up state, and provides a console command to list runtime-built objects that would be included in a joining client's snapshot.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// THE JOIN HOOK — the host notices somebody arrived and hands them the world.
///
/// ⛔ THIS IS WHAT `nz_sync` WAS STANDING IN FOR, AND WHY THE CLIENT KEPT ENDING UP IN THE WRONG
/// MAP. Every broadcast in `NZNet` fires on a CHANGE, so a client that joins after the host has
/// already chosen its map and config hears nothing at all: the map it is standing in is whatever
/// the scene shipped with, and the first config that arrives gets built into it. No amount of
/// re-sending on change fixes that, because by definition nothing changed.
///
/// ⚠️ `OnActive` IS THE RIGHT MOMENT, NOT `OnConnected`. The engine's own wording: "called when
/// someone is all loaded and entered the game". `OnConnected` fires while they are still coming
/// up, and a map load pushed at a machine that is not ready yet is a load with nowhere to go.
///
/// ⚠️ HOST-ONLY BY THE ENGINE'S DESIGN — all three of these only ever fire on the host, which is
/// exactly the authority model this wants anyway.
///
/// ⛔ AN OBJECT A MACHINE BUILDS FOR ITSELF IS `NetworkMode.Never`, OR A JOINER GETS A FROZEN COPY OF IT (2026-09-28). A joiner's
/// scene is the host's snapshot, and the snapshot takes every object in the scene that is not `Never` or `NotNetworked` —
/// `NotSaved` keeps an object off DISK only (`GameObject.SerializeOptions.ShouldSave`, read from the engine's IL). Every world
/// manager builds its objects on every machine from the config, so the host's copies arrived on top of the joiner's own and
/// never changed again: the user loaded basalt, a friend joined, and the door the host bought stayed shut on the friend's
/// screen, while the junctions' coloured circles — a mesh built in code, which only exists in the machine that made it — drew
/// as the error model. Each builder now marks what it makes `Never`, beside its `NotSaved`; the managers themselves still come
/// in the snapshot as before, and each builds its own world on the joiner. `nz_net_snapshot` lists what would still travel.
///
/// ⛔ BUT WHAT A MACHINE DOES TO A MAP OBJECT TRAVELS WITH IT, AND `Never` CANNOT HELP THERE (2026-09-29). A map object is always
/// in the snapshot (the map itself, below), exactly as this machine left it:
/// - SWITCHED OFF: the arena's floor and the shields split a world mesh into two copies and switch the original off. With the
/// copies `Never`, a joiner got the original switched off and no copies — a hole, since `Scene.GetAllComponents` finds only
/// what is on. So those COPIES travel too (the map's own geometry and materials, which draw anywhere), and the joiner takes
/// them up by tag and name, as a manager after a hotload does (`HexPlatforms.IsMapMeshCopy`).
/// - A MATERIAL MADE IN CODE on its faces (the light strips' copies, `StripLights`): a PolygonMesh saves a face's material as its
/// NAME, and a joiner loads that name. With no extension it is refused and the face comes back with no material at all; ending
/// in `.vmat`, it comes back as the error material under that name, which the joiner can find and replace.
/// `nz_strips joinsim` shows both.
/// </summary>
public sealed class NZNetListener : Component, Component.INetworkListener
{
/// <summary>
/// Make sure the listener exists in this scene.
///
/// ⚠️ CREATED AT RUNTIME RATHER THAN PLACED IN THE SCENE, deliberately: this project has a
/// standing rule against rewriting `.scene` files from a script, and the managers already
/// establish `Ensure` as the way a component that must simply EXIST comes into being.
/// </summary>
public static NZNetListener Ensure( Scene scene = null )
{
scene ??= Game.ActiveScene;
if ( !scene.IsValid() ) return null;
var found = scene.GetAllComponents<NZNetListener>().FirstOrDefault();
if ( found.IsValid() ) return found;
var go = scene.CreateObject();
go.Name = "Net Listener";
go.Flags |= GameObjectFlags.NotSaved;
// ⚠️ LOCAL ONLY. It is a listener, not shared state — every machine wants its own and
// none of them should replicate it.
go.NetworkMode = NetworkMode.Never;
return go.Components.Create<NZNetListener>();
}
// ⛔ DO NOT SET THE MAP OBJECT TO `NetworkMode.Never`. It was tried, at 06:43, and it
// DELETED THE MAP FROM EVERY CLIENT: "[nz-map] this scene has no MapInstance — is it the
// gamemode scene?", with `name is ''` and `world is ''`.
//
// ⚠️ BECAUSE A JOINING CLIENT'S SCENE **IS** THE HOST'S SNAPSHOT. Objects the host excludes
// from it do not exist on the client at all — they are not left alone, they are absent. The
// `Snapshot` mode on that object is not the bug; it is the only reason a client has a map
// object to talk about.
/// <summary>
/// Keep the other players' bodies looking right, twice a second.
///
/// ⛔ A ONE-SHOT ON MODE CHANGE LOSES EVERY RACE IT IS IN, and there are several. The host
/// enables its body when IT leaves the lobby; a client applies what it knows when IT leaves
/// the lobby; a character message can land in the gap; a body can be network-spawned after
/// both. Any single ordering can be argued for and none of them is guaranteed — so rather
/// than pick one and be wrong on the frame it does not hold, this re-asserts the whole
/// picture until it is true.
///
/// ⚠️ IT COSTS A LIST OF BODIES AND TWO COMPARISONS. `RefreshBodies` writes only when
/// something differs, so the steady state is a walk over two or four objects, twice a second,
/// changing nothing and logging nothing.
/// </summary>
TimeUntil _nextRefresh;
protected override void OnUpdate()
{
if ( _nextRefresh > 0f ) return;
_nextRefresh = 0.5f;
NZPlayers.RefreshBodies();
NZPlayers.NoteMovement();
NZPlayers.EnsureHostBody();
// ⛔ AND THE HELD PLACEMENT IS TAKEN HERE, NOT ONLY WHEN A BODY IS SWITCHED ON.
// `PlayerPresence.Apply` returns early when the body is ALREADY in the state the mode
// wants — which is the common case, not a rare one — so hanging the pickup off that
// transition meant a placement that arrived a moment too late was never applied at all,
// and the player stayed exactly where the bug puts them: on top of the host.
//
// ⚠️ FREE WHEN THERE IS NOTHING PENDING. `TakePending` is a null check first.
PlayerSpawner.TakePending( PlayerPresence.Find() );
WarnIfBodiless();
}
/// <summary>Warned once per spell of having no body, so a persistent fault is not a spam loop.</summary>
bool _warnedBodiless;
/// <summary>
/// SAY IT OUT LOUD WHEN THIS MACHINE HAS NO BODY IT CALLS ITS OWN.
///
/// ⛔ THIS IS THE INVARIANT THAT WOULD HAVE NAMED THE LAST TWO FAULTS IMMEDIATELY, AND ITS
/// ABSENCE COST TWO TEST CYCLES. "Mine returns false for every body" is a single, checkable
/// condition — but from inside the game it surfaces as an unrelated-looking pile: no weapon,
/// no animations, standing somewhere with no players and no zombies, while the other machine
/// watches a perfectly good copy of you on the spawn point. The user's own words were *"still
/// dont know how to describe"*, which is what a missing invariant sounds like from the far
/// side of the screen.
///
/// ⚠️ IT NAMES EVERY CANDIDATE AND WHY EACH WAS REJECTED, because "no body" and "a body
/// that was not recognised" are opposite problems with opposite fixes and look identical.
///
/// ⚠️ ONLY WHILE A BODY SHOULD EXIST. The lobby and spectator have none on purpose.
/// </summary>
void WarnIfBodiless()
{
if ( !PlayerPresence.ShouldExist )
{
_warnedBodiless = false;
return;
}
if ( PlayerPresence.Find().IsValid() )
{
_warnedBodiless = false;
return;
}
if ( _warnedBodiless ) return;
_warnedBodiless = true;
var all = PlayerSpawner.AllBodies();
Log.Warning( $"[nz-net] ⛔ I HAVE NO BODY. Mode is {NZGame.Mode}, so there should be one."
+ $" {all.Count} body/bodies in the scene and none of them answered as mine:" );
foreach ( var go in all )
{
var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
Log.Warning( $"[nz-net] '{go.Name}' said='{(np.IsValid() ? np.OwningConnection : "?")}'"
+ $" owner={go.Network.OwnerId} proxy={go.Network.IsProxy} enabled={go.Enabled}" );
}
Log.Warning( $"[nz-net] my connection is {Connection.Local?.Id.ToString() ?? "(none)"}"
+ " — a body whose `said` matches that is mine. nz_bodies for the full picture." );
}
/// <summary>Someone finished loading and is in the game. Hand them the map and config.</summary>
public void OnActive( Connection channel )
{
Log.Info( $"[nz-net] {channel?.DisplayName} is in — pushing the map and config" );
// ⛔ TO THE JOINER ALONE. This went to every client, on the belief that a client "only takes a config it asked for" —
// it took every one, and re-taking it rebuilt its world: every soul box empty, every bench and table bare, and
// `BuildParts.Reset` broadcast "no parts" from each client, wiping the team's Prisma parts on every join (the co-op
// audit, 2026-09-27). Everybody already here is up to date; only the newcomer needs the catch-up.
NZNet.PushStateTo( channel );
// ⛔ AND A BODY OF THEIR OWN. Without this the joiner gets a PROXY of the host's
// player from the scene snapshot and nothing else — standing in the map, unable to move,
// watching a copy of somebody else.
NZPlayers.SpawnFor( channel );
}
/// <summary>
/// `nz_net_snapshot` — what a player joining now would be handed as a FROZEN copy: every object in the scene that is built
/// at runtime (`NotSaved`), in the snapshot (not `Never`, not `NotNetworked`) and not a live network object — grouped by
/// name. The managers themselves belong here; a wall, a light or a machine does not (see the class note).
/// </summary>
[ConCmd( "nz_net_snapshot" )]
public static void SnapshotCmd()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz-net] no scene" ); return; }
var travel = scene.GetAllObjects( false )
.Where( go => go.Flags.Contains( GameObjectFlags.NotSaved ) && !go.Flags.Contains( GameObjectFlags.NotNetworked )
&& go.NetworkMode == NetworkMode.Snapshot && !go.Network.Active
// an object inside one that stays home stays home with it
&& !AncestorStaysHome( go ) )
.ToList();
// ⚠️ COPIES OF THE MAP'S OWN MESHES TRAVEL ON PURPOSE (see the class note): counted apart, not as frozen copies
var mapCopies = travel.Count( HexPlatforms.IsMapMeshCopy );
travel = travel.Where( go => !HexPlatforms.IsMapMeshCopy( go ) ).ToList();
if ( mapCopies > 0 )
Log.Info( $"[nz-net] {mapCopies} copy(ies) of the map's own meshes travel on purpose — the joiner takes them up (the arena's floor, the shields)" );
Log.Info( $"[nz-net] a joiner would get {travel.Count} runtime-built object(s) as frozen copies:" );
foreach ( var g in travel.GroupBy( go => System.Text.RegularExpressions.Regex.Replace( go.Name ?? "", @"[\d#]+", "N" ) )
.OrderByDescending( g => g.Count() ).Take( 30 ) )
Log.Info( $"[nz-net] {g.Count(),4} × {g.Key}" );
}
static bool AncestorStaysHome( GameObject go )
{
for ( var p = go.Parent; p.IsValid() && p is not Sandbox.Scene; p = p.Parent )
if ( p.NetworkMode == NetworkMode.Never || p.Flags.Contains( GameObjectFlags.NotNetworked ) ) return true;
return false;
}
/// <summary>Someone left. Forget what was remembered about them.</summary>
public void OnDisconnected( Connection channel )
{
Log.Info( $"[nz-net] {channel?.DisplayName} left" );
// ⛔ OR THE LOBBY COUNTS A GHOST. Ready is stored per connection id, and the gate is a
// PERCENTAGE of the people present — a stale ready entry for someone who left would make
// the remaining players' share of "everyone ready" wrong in whichever direction the
// leaver had chosen.
NZNet.Forget( channel );
// ⛔ AND THE PRISMA THEY HELD GOES BACK ON ITS BENCH (the co-op audit, 2026-09-27). Before the body goes, so the host
// still knows who they were. Host only, inside.
BuildTable.HolderLeft( channel?.Id.ToString() ?? "" );
NZPlayers.RemoveFor( channel );
}
}