Debugging/diagnostic console commands for the NZombies game. Provides commands to inspect player bodies, weapons, viewmodels, prefab integrity and to attempt fixes (reapply hands, respawn), printing detailed state about scene objects, networking ownership, renderers, cameras and tags to help root-cause visibility/replication issues.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// `nz_bodies` — EVERY PLAYER BODY IN THIS SCENE, FROM THIS MACHINE'S POINT OF VIEW.
///
/// ⛔ WRITTEN BECAUSE "THE CLIENT SEES NO ONE" HAS SIX CAUSES THAT ARE INDISTINGUISHABLE FROM
/// OUTSIDE, and one round was already spent guessing between them:
///
/// 1. the other body is not in this machine's scene at all → it will not be listed
/// 2. it is there but DISABLED (cloned out of the lobby, never re-enabled)
/// 3. it is enabled but its RENDERER is off, or `ShadowsOnly` — first-person hiding applied
/// to a body that is not ours
/// 4. it has no model, or the error model
/// 5. it is at the wrong position — inside geometry, or on top of us
/// 6. it is all of the above and correct, and the problem is the camera
///
/// Each of those prints differently here, so the failure names itself instead of being argued
/// about. §7 of INSTRUCTIONS.md: instrument, do not theorise.
///
/// ⚠️ RUN IT ON BOTH MACHINES. The whole question is an ASYMMETRY — the host sees the client and
/// the client does not see the host — so one machine's answer is half the evidence by definition
/// (INSTRUCTIONS.md §22).
/// </summary>
public static class BodyCheck
{
[ConCmd( "nz_bodies" )]
public static void Report()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz-bodies] no scene" ); return; }
var me = Connection.Local?.Id ?? default;
Log.Info( "════ nz_bodies ════════════════════════════════════════════════" );
Log.Info( $"[nz-bodies] I am the {(NZGame.IsHost ? "HOST" : "CLIENT")}"
+ $" · networking {(Networking.IsActive ? "ACTIVE" : "off")}"
+ $" · my connection {Short( me )}"
+ $" · mode {NZGame.Mode}" );
// ── who is connected ──────────────────────────────────────────────────────────────
foreach ( var c in Connection.All )
{
Log.Info( $"[nz-bodies] connection {Short( c.Id )} {c.DisplayName}"
+ $"{(c == Connection.Local ? " (me)" : "")}"
+ $" · ready {NZNet.IsReady( c )}"
+ $" · character '{NZNet.CharacterOf( c )}'" );
}
// ⚠️ `GetAllObjects( false )` = INCLUDE DISABLED. A disabled body is cause (2) above and
// is the single most likely one, so a listing that skipped them would hide the answer.
var bodies = scene.GetAllObjects( false )
.Where( o => o.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is not null )
.ToList();
Log.Info( $"[nz-bodies] {bodies.Count} body/bodies in the scene:" );
foreach ( var go in bodies )
{
var owner = go.Network.OwnerId;
var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
var ctrl = go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var rend = ctrl.IsValid() && ctrl.Renderer.IsValid()
? ctrl.Renderer
: go.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
var model = rend.IsValid()
? (rend.Model is null ? "NULL" : rend.Model.IsError ? "ERROR MODEL" : Leaf( rend.Model.ResourcePath ))
: "no renderer";
Log.Info( $"[nz-bodies] '{go.Name}'" );
// ⛔ `MINE` AND `proxy` ARE PRINTED SIDE BY SIDE ON PURPOSE, and the moment they
// disagree is the answer. `IsProxy` describes SIMULATION and says nothing useful
// about an object NOBODY owns — an unowned body is not a proxy on any machine — so
// a row reading `proxy=False MINE=no` is a body the old predicate would have handed
// to the wrong machine. That mismatch produced three symptoms at once and is worth
// being able to see directly.
// ⛔ THREE ANSWERS TO ONE QUESTION, SIDE BY SIDE, BECAUSE TWO OF THEM HAVE ALREADY
// BEEN WRONG. `said` is what the host WROTE DOWN before spawning the body; `owner` is
// the engine's ownership id; `proxy` is the engine's simulation answer. When they
// disagree, the disagreement IS the bug — and each previous version of `Mine` failed
// in a different one of those columns.
var said = np.IsValid() ? np.OwningConnection : "";
Log.Info( $"[nz-bodies] enabled={go.Enabled} MINE={(PlayerPresence.Mine( go ) ? "yes" : "no")}"
+ $" said={Short( said )}{(said == me.ToString() ? " (me)" : string.IsNullOrEmpty( said ) ? " (NOT RECORDED)" : "")}"
+ $" owner={Short( owner )}{(owner == me ? " (me)" : owner == default ? " (UNOWNED)" : "")}"
+ $" proxy={go.Network.IsProxy}"
+ $" netmode={go.NetworkMode}" );
// ⚠️ `still` ANSWERS "why can I not see them" WITHOUT A SECOND MACHINE. A body that
// has not moved for thirty seconds while its owner says they are running around is a
// transform that is not arriving — a different fault from a body that is simply
// hidden, and indistinguishable from inside the game.
var still = NZPlayers.StillFor( go );
Log.Info( $"[nz-bodies] still={(still < 0f ? "?" : $"{still:0}s")}{(still > 30f ? " ⚠ NOT MOVING" : "")}"
+ $" · pos={go.WorldPosition} · hp={(np.IsValid() && np.Hp.IsValid() ? np.Hp.Current.ToString( "0" ) : "?")}"
+ $" · down={(np.IsValid() ? np.IsDown.ToString() : "?")}"
+ $" · char='{(np.IsValid() ? np.CharacterId ?? "" : "?")}'" );
// ⚠️ THE ROUND STATE AS THIS MACHINE SEES IT, AND AS THE OWNER PUBLISHED IT (2026-09-29). `bledOut` is this copy's own
// latch and means something only on a body this machine drives; `out`/`outNet` and `downNet` are what the owner
// says. A copy reading bledOut=True while its owner is up is the stale latch that teleported clients to a spawn
// every round — see `NZPlayer.Revive`.
if ( np.IsValid() )
Log.Info( $"[nz-bodies] round: out={np.IsOutOfRound} outNet={np.OutOfRoundNet} downNet={np.DownedNet}"
+ $" bledOut={np.HasBledOut} · perks={np.Perks.Count} · points={np.Points}" );
Log.Info( $"[nz-bodies] renderer: model={model}"
+ $" enabled={(rend.IsValid() ? rend.Enabled.ToString() : "-")}"
+ $" type={(rend.IsValid() ? rend.RenderType.ToString() : "-")}"
+ $" objEnabled={(rend.IsValid() ? rend.GameObject.Enabled.ToString() : "-")}" );
// ⚠️ THE WEAPON TOO. "No weapon" and "no body" were reported together and they are
// different failures with different fixes.
var wep = go.Components.Get<NZWeapon>( FindMode.EverythingInSelfAndDescendants );
Log.Info( $"[nz-bodies] weapon: {(wep.IsValid() ? wep.GameObject.Name : "NONE")}" );
// ⛔ WHO IS DRIVING THE CAMERA. Exactly one row per machine may say `drives=yes`.
// Two controllers both believing they are local is silent — each moves `Scene.Camera`
// to its own eye and the last one that frame wins, so the player gets a plausible
// first-person view of the WRONG BODY with nothing to say so. That is precisely what
// "clientside I see myself somewhere else" turned out to be.
Log.Info( $"[nz-bodies] drives={(ctrl.IsValid() && ctrl.UseCameraControls ? "yes" : "no")}"
+ $" input={(ctrl.IsValid() && ctrl.UseInputControls ? "yes" : "no")}"
+ (ctrl.IsValid() && ctrl.UseCameraControls && go.Network.IsProxy
? " ⚠ A PROXY IS DRIVING THE CAMERA" : "") );
}
// ── the rest of the world ─────────────────────────────────────────────────────────
var rm = RoundManager.Instance;
Log.Info( $"[nz-bodies] rounds: {(rm.IsValid() ? $"round {rm.Round} · {rm.State} · {rm.Remaining} left" : "NO ROUND MANAGER")}" );
var zombies = ZombieAI.All.Where( z => z.IsValid() ).ToList();
var puppets = zombies.Count( z => z.IsPuppet );
// ⚠️ PUPPET COUNT, NOT JUST A TOTAL. "Zombies are seen on both machines but not
// synced" and "zombies are correctly replicated" look identical as a number; what
// separates them is whether the client's copies are THINKING. On a client every zombie
// must be a puppet, and any that is not is running an AI against a transform the host is
// simultaneously sending — two opinions, one body.
Log.Info( $"[nz-bodies] zombies in this scene: {zombies.Count}"
+ $" · puppets {puppets}/{zombies.Count}"
+ (NZGame.IsClient && puppets != zombies.Count
? " ⚠ A CLIENT'S ZOMBIES MUST ALL BE PUPPETS" : "") );
Log.Info( $"[nz-bodies] map: {NZMap.Current ?? "(none)"} · config '{ActiveConfig.Current?.Name ?? "(none)"}'" );
Log.Info( "═══════════════════════════════════════════════════════════════" );
}
/// <summary>
/// `nz_respawn` — deal the spawn points out again, right now. Host only.
///
/// ⚠️ EXISTS SO "DID THE SEPARATION WORK" IS ONE KEYPRESS RATHER THAN A WHOLE RESTART.
/// Placement happens inside `StartGame` and on join, so every attempt to check it otherwise
/// costs a full lobby round trip with two instances — which is exactly the kind of friction
/// that makes a fix get tested once and believed thereafter.
/// </summary>
// ⚠️ NO [ConCmd] HERE SINCE 2026-10-05: `nz_respawn` is `PlayerCommands.Respawn`, which also takes a spawn index and now
// has this host check. Both registered the name, and the engine kept whichever it met first.
public static void Respawn()
{
if ( NZGame.IsClient )
{
Log.Warning( "[nz-bodies] the HOST deals the spawns — run this there" );
return;
}
var moved = PlayerSpawner.PlaceAll();
Log.Info( $"[nz-bodies] re-dealt {moved} player(s) — nz_spawn_where to see where they landed" );
}
/// <summary>
/// `nz_gun_why` — the whole first-person weapon chain on THIS machine, one link per line.
///
/// ⛔ "NO ANIMATIONS, SHOOTING SOMETIMES DOES NOT WORK, AND THE TRACER GOES SOMEWHERE ODD"
/// IS THREE SYMPTOMS OF UP TO FIVE CAUSES, and they are indistinguishable while holding the
/// gun:
///
/// 1. the weapon is not the local player's → every `!IsProxy` branch in SWB is skipped
/// 2. `WeaponSettings.Instance` is null → `OnUpdate` throws above the input code
/// 3. no viewmodel, or its renderer is off → nothing to animate
/// 4. `IsFirstPerson` is false → the handler hides it deliberately
/// 5. THE CAMERA IS NOT AT YOUR EYE → bullets leave from somewhere else
///
/// ⚠️ (5) IS THE ONE WORTH READING FIRST, and it is the last line here. SWB traces the shot
/// from `Scene.Camera`, so a camera sitting on another body fires from THAT body: the tracer
/// starts at your muzzle and ends wherever somebody else's ray landed, and most shots stop in
/// a wall you cannot see. `eye→cam` is that distance. Anything over a few units is the answer.
/// </summary>
[ConCmd( "nz_gun_why" )]
public static void GunWhy()
{
var player = PlayerCharacters.Local();
if ( !player.IsValid() )
{
Log.Warning( "[nz-gun] no local player — nz_bodies" );
return;
}
Log.Info( "════ nz_gun_why ══════════════════════════════════════════════" );
Log.Info( $"[nz-gun] I am the {(NZGame.IsHost ? "HOST" : "CLIENT")}"
+ $" · body '{player.GameObject.Name}' mine={PlayerPresence.Mine( player.GameObject )}"
+ $" · first person={player.IsFirstPerson}" );
var wep = player.Components.Get<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants );
if ( !wep.IsValid() )
{
Log.Warning( "[nz-gun] this body is holding no SWB weapon at all" );
}
else
{
var vm = wep.ViewModelRenderer;
Log.Info( $"[nz-gun] weapon '{wep.ClassName}'"
+ $" · proxy={wep.IsProxy}{(wep.IsProxy ? " ⚠ SWB SKIPS EVERY INPUT BRANCH" : "")}"
+ $" · owner={(wep.Owner.IsValid() ? "yes" : "NULL ⚠ OnUpdate returns immediately")}" );
Log.Info( $"[nz-gun] settings singleton: "
+ (SWB.Base.WeaponSettings.Instance.IsValid()
? "present"
: "NULL ⚠ OnUpdate throws above the input code") );
Log.Info( $"[nz-gun] viewmodel: {(wep.ViewModel.IsValid() ? "object yes" : "NO OBJECT")}"
+ $" · renderer {(vm.IsValid() ? $"enabled={vm.Enabled} type={vm.RenderType} rate={vm.PlaybackRate:0.##}" : "MISSING ⚠ nothing to animate")}" );
// ⛔ HOW MANY WEAPONS IS THIS BODY HOLDING? A clone used to carry the template's
// runtime weapon objects across the wire, so a machine could end up with a networked
// copy AND its own — two `Weapon` components over one body, one of them a proxy that
// skips every input branch. That is a very good way to be unable to shoot while the
// knife, a separate component, keeps working.
var guns = player.Components
.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants )
.Where( w => w.IsValid() && w.GameObject.Enabled )
.ToList();
Log.Info( $"[nz-gun] enabled weapon components on this body: {guns.Count}"
+ (guns.Count > 1 ? " ⛔ MORE THAN ONE — they fight over the input" : "") );
// ⛔ THE GATE ITSELF, TERM BY TERM. `Weapon.OnUpdate` runs every input branch —
// aiming, firing, the fire animation — inside ONE `if`, so any single term being
// wrong looks exactly like "the gun does nothing". Printing the whole condition is
// the difference between naming the blocker and guessing at it again.
var gate = !wep.IsProxy && !wep.Owner.IsBot && !wep.IsDeploying;
Log.Info( $"[nz-gun] input gate: {(gate ? "OPEN" : "SHUT")}"
+ $" · !proxy={!wep.IsProxy} !bot={!wep.Owner.IsBot}"
+ $" !deploying={!wep.IsDeploying} (TimeSinceDeployed {(float)wep.TimeSinceDeployed:0.00}s)" );
Log.Info( $"[nz-gun] state: reloading={wep.IsReloading} customizing={wep.IsCustomizing}"
+ $" running={wep.IsRunning} aiming={wep.IsAiming}"
+ $" · CanPrimaryShoot={wep.CanPrimaryShoot()}" );
if ( wep.Primary is null )
Log.Warning( "[nz-gun] ⛔ Primary is NULL — CanShoot returns false before anything else" );
else
Log.Info( $"[nz-gun] primary: clip {wep.Primary.Ammo}/{wep.Primary.ClipSize}"
+ $" · since last shot {(float)wep.TimeSincePrimaryShoot:0.00}s"
+ $" · rate {wep.Primary.RPM} rpm" );
}
// ⛔ THE DECISIVE LINE. Everything above can be perfect and the gun still fires into a
// wall if the camera is not where the player is.
var cam = player.Scene?.Camera;
var eye = player.EyePos;
if ( !cam.IsValid() )
{
Log.Warning( "[nz-gun] NO ACTIVE CAMERA — shots have no origin at all" );
}
else
{
var gap = cam.WorldPosition.Distance( eye );
Log.Info( $"[nz-gun] camera '{cam.GameObject.Name}' at {cam.WorldPosition}"
+ $" · my eye at {eye} · eye→cam {gap:0.#}u"
+ (gap > 8f ? " ⛔ THE CAMERA IS NOT ON THIS BODY — bullets leave from there, not here" : "") );
}
Log.Info( "══════════════════════════════════════════════════════" );
}
/// <summary>
/// `nz_see` — ONE QUESTION: can I see the other player, and if not, which link is broken?
///
/// ⛔ `nz_bodies` PRINTS EVERYTHING AND THEREFORE ANSWERS NOTHING IN PARTICULAR. Seeing
/// another player is a chain of five conditions, and a page of correct-looking fields with one
/// wrong value buried in it is how three test rounds were spent on a body that was rendering
/// perfectly at the wrong coordinates. This walks the chain in order and stops at the first
/// broken link, because that link is the whole answer:
///
/// 1. is their body in my scene at all?
/// 2. is it enabled?
/// 3. does it have a real model, and is the renderer drawing it?
/// 4. is it MOVING — i.e. is their position actually reaching me?
/// 5. is it anywhere near me?
///
/// ⚠️ (4) IS THE ONE THAT HAS ACTUALLY BEEN WRONG, and it is invisible from inside the
/// game: a body frozen at the map's default spawn renders exactly as well as a live one. You
/// walk around an empty map and conclude the other player is not being drawn.
/// </summary>
[ConCmd( "nz_see" )]
public static void See()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Tell( "[nz-see] no scene" ); return; }
// ⚠️ THE HOST ASKS EVERYONE ELSE TOO, so one command answers for the whole session and
// every answer lands in one console. See `NZNet.SeeAsk`.
if ( NZGame.IsHost && Networking.IsActive ) NZNet.SeeAsk();
Tell( "════ nz_see ══════════════════════════════════════════════════" );
var meBody = PlayerPresence.Find();
var meAt = meBody.IsValid() ? meBody.WorldPosition : Vector3.Zero;
Tell( $"[nz-see] I am the {(NZGame.IsHost ? "HOST" : "CLIENT")}"
+ $" · my body {(meBody.IsValid() ? $"'{meBody.Name}' at {meAt}" : "⛔ I HAVE NONE")}" );
var others = PlayerSpawner.AllBodies().Where( PlayerPresence.Theirs ).ToList();
if ( others.Count == 0 )
{
Tell( "[nz-see] ⛔ THERE IS NO OTHER PLAYER'S BODY IN MY SCENE AT ALL." );
Tell( "[nz-see] Either nobody has joined, or their body was never spawned to "
+ "me — or it was destroyed here (look for 'dropping' in this console)." );
Tell( "════════════════════════════════════════════════════════" );
return;
}
foreach ( var go in others )
{
var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelf );
var ctrl = go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
// ⚠️ WHICH RENDERER THIS IS MATTERS. `PlayerCharacters.ApplyBody` writes the model
// through `ctrl.Renderer` and RETURNS SILENTLY if that reference is null — so a body
// whose reference did not survive the network is never given its model, while this
// check happily falls back to a descendant search and reports it as healthy.
var refOk = ctrl.IsValid() && ctrl.Renderer.IsValid();
var rend = refOk ? ctrl.Renderer
: go.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
var still = NZPlayers.StillFor( go );
var away = meBody.IsValid() ? go.WorldPosition.Distance( meAt ) : -1f;
Tell( $"[nz-see] ── '{go.Name}' ── owner {Short( NZPlayers.OwnerOf( go ) )}" );
// 1 — present. Reaching this loop already proves it.
Tell( "[nz-see] 1 in my scene .............. yes" );
// 2 — enabled
if ( !go.Enabled )
{
Tell( "[nz-see] 2 enabled .................. ⛔ NO — nothing below matters" );
continue;
}
Tell( "[nz-see] 2 enabled .................. yes" );
// 3 — drawable
if ( !rend.IsValid() )
{
Tell( "[nz-see] 3 renderer ................. ⛔ NONE" );
continue;
}
var model = rend.Model is null ? "NULL" : rend.Model.IsError ? "ERROR MODEL" : Leaf( rend.Model.ResourcePath );
var drawable = rend.Enabled && rend.GameObject.Enabled
&& rend.RenderType == ModelRenderer.ShadowRenderType.On
&& rend.Model is not null && !rend.Model.IsError;
Tell( $"[nz-see] 3 drawable ................. {(drawable ? "yes" : "⛔ NO")}"
+ $" (model {model}, renderer {(rend.Enabled ? "on" : "OFF")}, type {rend.RenderType})" );
Tell( $"[nz-see] 3a renderer reference ..... {(refOk ? "PlayerController.Renderer" : "⛔ NULL — fell back to a descendant search")}"
+ (refOk ? "" : " ApplyBody would have returned silently here") );
// ⛔ THE TAGS ON THE BODY'S RENDERER, BECAUSE A TAG DECIDES WHICH CAMERA DRAWS IT.
// Disabling every renderer tagged `viewmodel` on other people's bodies made those
// bodies VANISH — observed directly — which can only mean the body's own renderer
// carries that tag. Nothing in the project adds it to a body: the three places that
// add `ViewModel` all add it to a viewmodel object. So either it is inherited from an
// ancestor, or something adds it that a grep does not find. Either way it is a fact
// that can be read rather than argued about, and it decides whether the body is drawn
// by the world camera or by the viewmodel camera.
var vmTagged = rend.GameObject.Tags.Has( "viewmodel" );
// ⛔ AND `viewer` IS THE ONE THAT WAS ACTUALLY DOING IT. `Sandbox.PlayerController`
// tags its own body's renderer object `viewer` to hide it in first person, the tag is
// copied by `Clone()`, and this line is where that shows up as a fact instead of a
// theory: the run where nobody could see anybody had `viewer` on BOTH bodies, and the
// run before it, where one player was visible, had it on only one.
var viewerTagged = rend.GameObject.Tags.Has( "viewer" );
Tell( $"[nz-see] 3e renderer tags .......... [{string.Join( " ", rend.GameObject.Tags )}]"
+ $" · on '{rend.GameObject.Name}'"
+ (vmTagged ? " ⛔ TAGGED VIEWMODEL — the world camera excludes it; only the viewmodel camera draws it" : "")
+ (viewerTagged ? " ⛔ TAGGED 'viewer' — THAT IS WHY IT IS INVISIBLE. It is how the engine hides "
+ "your OWN body in first person, and it was cloned onto this one. NZPlayers.Control strips it." : "") );
Tell( $"[nz-see] 3f body tags .............. [{string.Join( " ", go.Tags )}]"
+ " (a tag here is worth checking against 3e — inheritance would explain both)" );
if ( !drawable ) continue;
// ⛔ WHERE IS THE MESH, ACTUALLY? "We can collide with each other but not see each
// other" is the signature that sent this check in: collision comes from the
// `Colliders` child and rendering from the `Body` child, so a body can be present,
// enabled and solid in exactly the right place while its MESH is somewhere else
// entirely — or scaled to nothing. Every field above reads perfectly in that state,
// which is why four rounds of checking them proved nothing.
//
// ⚠️ THE OFFSET IS WHAT MATTERS, not the absolute position. The `Body` child sits a
// little below the root by design (−51.7 in the scene); anything past a couple of
// hundred units means the mesh is not on the body at all.
var rendAt = rend.WorldPosition;
var off = rendAt.Distance( go.WorldPosition );
var scale = rend.WorldScale;
var tiny = scale.x * scale.y * scale.z < 0.001f;
Tell( $"[nz-see] 3b mesh position .......... {rendAt}"
+ $" · {off:0.#}u from the body"
+ (off > 200f ? " ⛔ THE MESH IS NOT ON THE BODY" : "") );
Tell( $"[nz-see] 3c mesh scale ............. {scale}"
+ (tiny ? " ⛔ SCALED TO NOTHING — present, solid, invisible" : "") );
// ⚠️ AND HOW MANY GUNS THIS BODY IS HOLDING. A second weapon on somebody else's body
// is what draws a first-person viewmodel out in the world — the "floating arms".
var guns = go.Components
.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants )
.Count( w => w.IsValid() && w.GameObject.Enabled );
var vms = go.Components
.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants )
.Count( r => r.IsValid() && r.GameObject.Tags.Has( "viewmodel" ) );
Tell( $"[nz-see] 3d weapons on it .......... {guns}"
+ $" · viewmodel renderers {vms}"
+ (vms > 0 ? " ⛔ SOMEBODY ELSE'S BODY IS CARRYING A VIEWMODEL — these are the floating arms"
: guns > 0 ? " ⚠ it has a weapon; weapons are not supposed to cross" : "") );
// 4 — THE ONE THAT HAS ACTUALLY BEEN WRONG
if ( still > 20f )
{
Tell( $"[nz-see] 4 moving ................... ⛔ NO — hasn't moved in {still:0}s" );
Tell( $"[nz-see] It is frozen at {go.WorldPosition}. Their position is not"
+ " reaching me, so I am looking at wherever they were when I joined." );
Tell( "[nz-see] ASK THEM TO WALK, then run this again. If it still says"
+ " frozen, their body is not replicating." );
continue;
}
Tell( $"[nz-see] 4 moving ................... yes (last moved {(still < 0f ? 0f : still):0}s ago)" );
// 5 — near enough to be on screen
Tell( $"[nz-see] 5 distance ................. {away:0}u"
+ (away > 1500f ? " ⚠ far away — walk toward them before concluding anything" : "") );
Tell( $"[nz-see] ✅ EVERYTHING CHECKS OUT — they are at {go.WorldPosition},"
+ $" wearing '{(np.IsValid() ? np.CharacterId ?? "(default)" : "?")}'. Go and look." );
}
Tell( "════════════════════════════════════════════════════════" );
}
/// <summary>
/// Print here if I am the host; send to the host if I am not.
///
/// ⛔ SO A CLIENT'S ANSWER IS NOT STRANDED IN A LOG NOBODY IS READING. Every previous round
/// ended with a person copying one machine's output into a chat window, which is where the
/// time went. Both answers belong in the same console.
/// </summary>
static void Tell( string line )
{
if ( NZGame.IsHost || !Networking.IsActive ) Log.Info( line );
else NZNet.Say( line );
}
static string Short( Guid id ) => id == default ? "········" : id.ToString()[..8];
static string Short( string id )
=> string.IsNullOrEmpty( id ) ? "········" : id[..Math.Min( 8, id.Length )];
static string Leaf( string path )
=> string.IsNullOrEmpty( path ) ? "?" : path[(path.LastIndexOf( '/' ) + 1)..];
/// <summary>
/// `nz_prefab_check` — CLONE THE PLAYER PREFAB, INSPECT IT, THROW IT AWAY.
///
/// ⛔ SO THE PREFAB IS PROVEN BEFORE A TWO-MACHINE SESSION IS SPENT ON IT. Every other
/// question about a body has needed two people, two logs and a full playthrough to answer.
/// This one does not: whether a clone of `prefabs/player.prefab` is a usable body is entirely
/// local, and knowing it here is the difference between "the prefab is fine, something else is
/// wrong" and half an evening.
///
/// ⚠️ THE TWO LINES THAT MATTER ARE THE TAG AND THE RENDER TYPE. A body cloned from the
/// LIVE scene player inherits the engine's first-person hiding both ways — `viewer` on the
/// renderer's object and `RenderType = ShadowsOnly` on the renderer. **A prefab has no runtime
/// state, so it should have neither**, and that is the entire claim being made here.
///
/// ⚠️ IT DESTROYS WHAT IT MAKES. The clone is `NotSaved` and disabled on arrival, exists
/// for the length of this method, and is destroyed at the end whatever happens.
/// </summary>
/// <summary>
/// `nz_arms` — WHOSE ARMS ARE THOSE, AND WHY ARE THEY THE WRONG ONES.
///
/// ⛔ TWO SEPARATE FAULTS WEAR THE SAME DESCRIPTION and every previous round conflated them:
///
/// "not using the arms of its model" → ONE viewmodel, on my own body, wrong HANDS MESH
/// "there are arms floating" → a viewmodel on SOMEBODY ELSE'S body, drawn in the world
///
/// The first is a model choice; the second is a parenting mistake. A fix for one does nothing
/// for the other, which is how three attempts in a row read as "no change".
///
/// ⚠️ WHY A VIEWMODEL ON SOMEBODY ELSE'S BODY FLOATS: `Weapon.CreateViewModelHandler` parents
/// it to `Owner.GameObject` — the BODY, not the camera. Mine sits where my camera is and looks
/// correct; one built against another player's weapon sits at THEIR body and is drawn by my
/// viewmodel camera, out in the world, parallaxing as I move. That is the whole symptom.
///
/// ⚠️ HANDS ARE CHOSEN ONCE, AT `Weapon.OnStart`, from `PlayerCharacters.HandsFor( Owner )`.
/// A character that arrives AFTER that — which on a client it does, from the net table on the
/// half-second tick — leaves the weapon's own generic `ViewModelHands` in place forever.
/// `RefreshCharacterHands()` is the rebuild, and this reports whether it is needed.
///
/// Client answers reach the host's console, same as `nz_see`.
/// </summary>
[ConCmd( "nz_arms" )]
public static void Arms()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz-arms] no scene" ); return; }
// ⚠️ THE HOST ASKS EVERYONE ELSE TOO — the same line `nz_see` has always had, and whose
// absence made the first capture describe only the machine that was already fine.
if ( NZGame.IsHost && Networking.IsActive ) NZNet.ArmsAsk();
Tell( "════ nz_arms ══════════════════════════════════════════" );
var who = NZGame.IsHost ? "HOST" : "CLIENT";
var mineGo = PlayerPresence.Find();
var mineNp = mineGo.IsValid() ? mineGo.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) : null;
var want = PlayerCharacters.HandsFor( mineNp );
Tell( $"[nz-arms] I am the {who} · my body '{(mineGo.IsValid() ? mineGo.Name : "⛔ NONE")}'"
+ $" · character '{(mineNp.IsValid() ? mineNp.CharacterId ?? "(none)" : "?")}'" );
// ⛔ MY OWN BODY, WHICH NOTHING HAS EVER CHECKED. Every arms hunt so far has looked at
// VIEWMODELS, and a "second pair of arms" does not have to be one.
//
// ⚠️ THE PORTED CHARACTER BODIES DO NOT ANIMATE — `PlayerCharacters.ApplyBody` says so
// outright: ValveBiped, 75 bones, against s&box's 94-bone human rig, so they render in
// BIND POSE and stay there. A bind-posed body drawn in first person is arms spread wide,
// below and around the camera — which is the user's original description word for word:
// *"the big one you see when i look down"*, and *"sharing the model with myself"*, because
// it IS their model.
//
// ⚠️ `HideBodyInFirstPerson` IS WHAT SUPPRESSES IT, and `NZPlayers.Control` sets it from
// `mine`. If it is off, or the renderer is drawing anyway, this line says so.
if ( mineGo.IsValid() )
{
var myCtrl = mineGo.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var myRend = myCtrl.IsValid() ? myCtrl.Renderer : null;
if ( !myRend.IsValid() )
{
Tell( "[nz-arms] my own body renderer: ⛔ NONE — PlayerController.Renderer is null" );
}
else
{
// ⛔ THE `viewer` TAG IS WHAT HIDES IT, AND THE FIRST VERSION OF THIS CHECK DID NOT
// LOOK AT IT. `Enabled=True` and `RenderType=On` are BOTH TRUE of a correctly hidden
// body — the engine excludes it from the camera by tag, not by render type. So this
// reported "⛔ MY OWN BODY IS BEING DRAWN IN FIRST PERSON" on both machines when
// neither was drawing it, and sent a whole test round after a fault that did not
// exist. A verdict is worse than no verdict when it is confidently wrong.
var hasViewer = myRend.GameObject.Tags.Has( "viewer" );
var drawn = myRend.Enabled
&& myRend.RenderType != ModelRenderer.ShadowRenderType.ShadowsOnly
&& !hasViewer;
var hidden = myCtrl.HideBodyInFirstPerson;
Tell( $"[nz-arms] my own body: {Leaf( myRend.Model?.ResourcePath )}"
+ $" enabled={myRend.Enabled} {myRend.RenderType}"
+ $" HideBodyInFirstPerson={hidden} thirdPerson={NZPlayer.ThirdPerson}"
+ $" tags=[{string.Join( " ", myRend.GameObject.Tags )}]" );
Tell( "[nz-arms] " + (drawn && !NZPlayer.ThirdPerson
? "⛔ MY OWN BODY IS BEING DRAWN IN FIRST PERSON. These ported bodies do not "
+ "animate — they sit in BIND POSE, arms spread — so this IS the second pair of arms."
: hasViewer
? "✅ not drawn to me — the 'viewer' tag excludes it from my camera, which is exactly "
+ "how the engine hides your own body. Whatever the extra arms are, they are not this."
: "✅ not drawn to me") );
}
}
Tell( "[nz-arms] hands my character SHOULD give me: "
+ (want is null
? "⛔ none — HandsFor returned null, so every weapon keeps its own generic gloves"
: Leaf( want.ResourcePath )) );
// ⛔ EVERY OBJECT TAGGED `viewmodel`, NOT EVERY `SWB.Base.ViewModelHandler`.
//
// This command's first version counted SWB handlers and reported "1 viewmodel" on both
// machines while the user was looking at a second pair of arms. THERE ARE THREE VIEWMODEL
// SYSTEMS IN THIS PROJECT and only one of them is SWB's:
//
// SWB.Base.ViewModelHandler guns
// KnifeViewModel its own GameObject, its own hands, its own camera
// GrenadeViewModel the same again
//
// The knife and the grenade each build `new GameObject( "Viewmodel - ..." )` and tag it
// `viewmodel`. **The tag is the only thing all three share**, so the tag is what to count.
//
// ⚠️ DISABLED OBJECTS AND DISABLED RENDERERS TOO. `KnifeViewModel` creates its renderers
// with `Enabled = false` and switches them on to swing, so "it exists but is off" and "it
// exists and is drawing" are different answers and only one of them is a bug.
var vms = scene.GetAllObjects( false )
.Where( o => o.IsValid() && o.Tags.Has( SWB.Shared.TagsHelper.ViewModel ) )
.ToList();
Tell( $"[nz-arms] {vms.Count} object(s) tagged 'viewmodel' in my scene" );
if ( vms.Count == 0 )
Tell( "[nz-arms] ✅ none at all — nothing can be floating, and nothing is in your hands either" );
var i = 0;
foreach ( var vm in vms )
{
i++;
// The body this viewmodel hangs off: the first ancestor that is a player.
var body = vm.Parent;
while ( body.IsValid() && !body.Components.Get<NZPlayer>( FindMode.EverythingInSelf ).IsValid() )
body = body.Parent;
var onMine = body.IsValid() && mineGo.IsValid() && body == mineGo;
var h = vm.Components.Get<SWB.Base.ViewModelHandler>( FindMode.EverythingInSelf );
var kind = h.IsValid() ? "SWB gun"
: vm.Name.Contains( "knife", StringComparison.OrdinalIgnoreCase ) ? "KNIFE"
: vm.Name.Contains( "grenade", StringComparison.OrdinalIgnoreCase ) ? "GRENADE"
: "unknown system";
Tell( $"[nz-arms] {i} '{vm.Name}' [{kind}] under '{(body.IsValid() ? body.Name : "⛔ NO PLAYER ABOVE IT")}'"
+ $" MINE={(onMine ? "yes" : "no")} objEnabled={vm.Enabled}"
+ (onMine ? "" : " ⛔ ON SOMEBODY ELSE'S BODY — THIS IS A FLOATING SET") );
// ⚠️ EVERY RENDERER UNDER IT, each with whether it is actually drawing. A viewmodel is
// two meshes — the weapon and the hands bone-merged to it — and either can be the one
// on screen when it should not be.
foreach ( var r in vm.Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
{
var isHands = r.BoneMergeTarget.IsValid();
var drawing = r.Enabled && r.RenderType != ModelRenderer.ShadowRenderType.ShadowsOnly;
var note = "";
if ( isHands && drawing && want is not null && r.Model?.ResourcePath != want.ResourcePath )
note = $" ⛔ WRONG HANDS — expected {Leaf( want.ResourcePath )}";
if ( isHands && drawing && !r.BoneMergeTarget.Enabled )
note += " ⛔ MERGED TO A DISABLED MESH — these sit at BIND POSE, spread wide "
+ "and below the camera. That is what a floating second pair of arms IS.";
Tell( $"[nz-arms] {(isHands ? "hands" : "mesh ")} {Leaf( r.Model?.ResourcePath )}"
+ $" enabled={r.Enabled} {r.RenderType}"
+ $" {(drawing ? "DRAWING" : "not drawn")}{note}" );
}
Tell( $"[nz-arms] at {vm.WorldPosition}"
+ (mineGo.IsValid() ? $" · {vm.WorldPosition.Distance( mineGo.WorldPosition ):0}u from my body" : "") );
}
// ⛔ EVERY CAMERA, BECAUSE A SECOND VIEWMODEL CAMERA DRAWS THE ARMS TWICE AND THERE IS NO
// SECOND OBJECT TO FIND.
//
// User: *"the client sees a copy of its arms and gun viewmodel, it seems to always be at
// double the distance i am from the host... they play the animations the client is playing
// so im sure its the same."*
//
// ⚠️ THAT GEOMETRY IS A PROOF, NOT A DESCRIPTION. A copy at `2·me − host` is the same
// geometry composited a second time, displaced by the gap between two cameras: one at my
// eye, one at the host's. Nothing is at that position — which is exactly why the object
// list above comes back clean and why three rounds of hunting for a second viewmodel found
// nothing. "Same animations" is the giveaway: it is not a copy, it is a second view.
//
// ⚠️ `EnsureCamera` PARENTS ITS CAMERA TO `owner.GameObject`, and a camera is NOT tagged
// `viewmodel` — so every version of this command has been blind to the one object that
// mattered. Both `Weapon.CreateViewModel` and `KnifeViewModel.EnsureCamera` make one, each
// only when `Owner.ViewModelCamera` is not already set, so a camera built against somebody
// else's body is both invisible here and permanent.
//
// ⚠️ PRIORITY AND TAGS ARE PART OF THE ANSWER. A viewmodel camera is Priority 2 with
// RenderTags { viewmodel, light }; the world camera is Priority 1. Two at Priority 2 both
// draw the viewmodel layer.
var cams = scene.GetAllObjects( false )
.SelectMany( o => o.Components.GetAll<CameraComponent>( FindMode.EverythingInSelf ) )
.Where( c => c.IsValid() )
.ToList();
var vmCams = cams.Count( c => c.RenderTags.Has( SWB.Shared.TagsHelper.ViewModel ) );
Tell( $"[nz-arms] {cams.Count} camera(s), {vmCams} of them drawing the viewmodel layer"
+ (vmCams > 1
? " ⛔ MORE THAN ONE VIEWMODEL CAMERA — the arms are drawn once per camera. THIS IS "
+ "THE DUPLICATE, and nz_vm_clear destroys them."
: "") );
foreach ( var c in cams )
{
var body = c.GameObject.Parent;
while ( body.IsValid() && !body.Components.Get<NZPlayer>( FindMode.EverythingInSelf ).IsValid() )
body = body.Parent;
var onMine = body.IsValid() && mineGo.IsValid() && body == mineGo;
Tell( $"[nz-arms] '{c.GameObject.Name}' priority={c.Priority} {(c.Enabled ? "on" : "OFF")}"
+ $" tags=[{string.Join( " ", c.RenderTags )}]"
+ $" excludes=[{string.Join( " ", c.RenderExcludeTags )}]"
+ $" under '{(body.IsValid() ? body.Name : "(no player)")}'"
+ (body.IsValid() && !onMine
? " ⛔ A CAMERA ON SOMEBODY ELSE'S BODY — it renders my viewmodel from THEIR eye"
: "") );
}
var held = Rarity.HeldBy( mineNp );
Tell( $"[nz-arms] holding: {(held.IsValid() ? held.GameObject.Name : "nothing")}"
+ $" · {(mineGo.IsValid() ? mineGo.Components.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants ).Count() : 0)} weapon(s) on my body" );
Tell( "══════════════════════════════════════════════════════" );
}
/// <summary>
/// `nz_hands_fix` — RE-APPLY MY OWN CHARACTER TO MY OWN BODY, AND REBUILD THE VIEWMODEL.
///
/// ⛔ THE CANDIDATE FIX, AS A COMMAND, SO IT CAN BE TRIED IN THE SESSION THAT FOUND THE FAULT.
/// If this makes the hands correct, the same three lines belong on the tick and this command
/// becomes redundant; if it does not, the theory is wrong and nothing has been shipped on it.
/// A build round trip per hypothesis is what has been expensive here.
///
/// The theory it tests: `RefreshBodies` dresses OTHER players' bodies from the net table and
/// never touches mine — my own character is applied by the lobby pick, to whatever body existed
/// AT PICK TIME. Under the prefab path my body is created afterwards, by the host, so nothing
/// ever writes the character onto the body I actually end up in. The world model would be wrong
/// too, but I am in first person and cannot see it; the hands are the part that shows.
/// </summary>
[ConCmd( "nz_hands_fix" )]
public static void HandsFix()
{
if ( NZGame.IsHost && Networking.IsActive ) NZNet.HandsFixAsk();
var who = NZGame.IsHost ? "HOST" : "CLIENT";
var go = PlayerPresence.Find();
var np = go.IsValid() ? go.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) : null;
if ( !np.IsValid() ) { Tell( $"[nz-arms] {who}: I have no body to fix" ); return; }
var me = Connection.Local;
var want = me is null ? "" : NZNet.CharacterOf( me.Id );
Tell( $"[nz-arms] {who}: my body '{go.Name}' says '{np.CharacterId ?? "(none)"}',"
+ $" the net table says '{(string.IsNullOrEmpty( want ) ? "(none)" : want)}'" );
if ( string.IsNullOrEmpty( want ) )
{
Tell( $"[nz-arms] {who}: ⛔ the net table has no character for me either — the fault is "
+ "further back, in the lobby pick never being broadcast. Nothing to apply." );
return;
}
np.CharacterId = want;
PlayerCharacters.ApplyBody( np );
np.RefreshCharacterHands();
Tell( $"[nz-arms] {who}: applied '{want}' and rebuilt the viewmodel — look at your hands, "
+ "then run nz_arms again" );
}
[ConCmd( "nz_prefab_check" )]
public static void PrefabCheck()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz-prefab] no scene" ); return; }
Log.Info( "════ nz_prefab_check ══════════════════════════" );
var path = NZPlayers.PlayerPrefabPath;
var file = ResourceLibrary.Get<PrefabFile>( path );
if ( file is null )
{
Log.Warning( $"[nz-prefab] ⛔ '{path}' DOES NOT LOAD. Nothing else can be checked." );
return;
}
Log.Info( $"[nz-prefab] 1 loads .................... yes ('{path}')" );
GameObject go = null;
try
{
go = GameObject.Clone( file, new CloneConfig
{
Transform = global::Transform.Zero,
StartEnabled = false,
} );
if ( !go.IsValid() )
{
Log.Warning( "[nz-prefab] ⛔ 2 clones ................... NO — Clone returned nothing" );
return;
}
go.Flags |= GameObjectFlags.NotSaved;
Log.Info( $"[nz-prefab] 2 clones ................... yes ('{go.Name}', {go.Children.Count} child object(s))" );
Log.Info( $"[nz-prefab] 3 network mode ............. {go.NetworkMode}"
+ (go.NetworkMode == NetworkMode.Object
? " ✅ Object — [Sync] and live replication are available"
: " ⛔ NOT Object — [Sync] will do nothing on this body (SBOX_MULTIPLAYER §2)") );
var ctrl = go.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var np = go.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndDescendants );
Log.Info( $"[nz-prefab] 4 PlayerController ......... {(ctrl.IsValid() ? "yes" : "⛔ MISSING")}"
+ $" · NZPlayer {(np.IsValid() ? "yes" : "⛔ MISSING")}" );
if ( !ctrl.IsValid() ) return;
var rend = ctrl.Renderer;
if ( !rend.IsValid() )
{
Log.Warning( "[nz-prefab] ⛔ 5 renderer reference ....... BROKEN — PlayerController.Renderer "
+ "points at nothing. The guid remap that built this prefab did not hold." );
return;
}
Log.Info( $"[nz-prefab] 5 renderer reference ....... resolves → '{rend.GameObject.Name}'"
+ $" · model {(rend.Model is null ? "⛔ NULL" : Leaf( rend.Model.ResourceName ))}" );
// ⛔ THE TWO LINES THIS COMMAND EXISTS FOR.
var tags = rend.GameObject.Tags;
var hasViewer = tags.Has( "viewer" );
Log.Info( $"[nz-prefab] 6 renderer tags ............ [{string.Join( " ", tags )}]"
+ (hasViewer
? " ⛔ CARRIES 'viewer' — the prefab itself is poisoned, which was not expected"
: " ✅ no 'viewer' — this body cannot arrive pre-hidden") );
Log.Info( $"[nz-prefab] 7 render type .............. {rend.RenderType}"
+ (rend.RenderType == ModelRenderer.ShadowRenderType.On
? " ✅ drawn"
: " ⛔ NOT 'On' — a body cloned from this would be invisible") );
var vms = go.Components.GetAll<SWB.Base.ViewModelHandler>( FindMode.EverythingInSelfAndDescendants ).Count();
var weps = go.Components.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants ).Count();
Log.Info( $"[nz-prefab] 8 viewmodels / weapons ..... {vms} / {weps}"
+ (vms == 0 ? " ✅ nothing to orphan" : " ⛔ the prefab was saved with a viewmodel on it") );
var ok = !hasViewer && rend.RenderType == ModelRenderer.ShadowRenderType.On
&& go.NetworkMode == NetworkMode.Object && np.IsValid() && vms == 0;
Log.Info( ok
? "[nz-prefab] ✅ THE PREFAB IS A USABLE BODY. `nz_prefab_players 1`, then host."
: "[nz-prefab] ⛔ SOMETHING ABOVE IS WRONG — fix the prefab before switching to it." );
}
finally
{
// ⚠️ WHATEVER HAPPENED ABOVE, INCLUDING A THROW. A probe that can leave a stray
// player body in the scene is worse than no probe.
if ( go.IsValid() ) go.Destroy();
Log.Info( "══════════════════════════════ (clone destroyed)" );
}
}
}