Human¶
Vanilla game class representing the player-controlled character entity. Lives under Assets.Scripts.Objects.Entities in the Entity hierarchy.
Player identification¶
Source: F0024.
The original mods used the LaunchPadBooster connection id to track which player pressed which modifier keys. But AttackWithMessage on the server does not carry the LaunchPadBooster connection id; it carries AttackParentId, which is the Human ReferenceId. Keying PlayerModifiers by Human ReferenceId matches the identifier available at paint time.
Enumeration: Human.AllHumans¶
Source: $(StationeersPath)\rocketstation_Data\Managed\Assembly-CSharp.dll :: Assets.Scripts.Objects.Entities.Human.
Human exposes a static collection of every registered human (alive or not, local or remote):
Fully qualified: Assets.Scripts.Objects.Entities.Human.AllHumans. Field, not property.
Inhabitants are added / removed by Human load and destroy paths; the list contains every Human instance currently loaded in the scene, including the local player, remote players, dead bodies (state Dead), and unconscious players. NPCs of type Npc are not in this list (different subclass; Npc : Entity diverges from Human : Entity).
To enumerate connected, alive human players only, filter by state:
foreach (var h in Human.AllHumans)
{
if (h == null || h.State != EntityState.Alive) continue;
// h is a live player (local or remote)
}
EntityState lives at Assets.Scripts.Objects.Entities.EntityState and has values Alive, Dead, Unconscious, Decay (byte). EntityState.Alive == 0.
Entity.IsDead (inherited by Human) is State == Dead || State == Decay. Entity.Unconscious is State == Unconscious. Neither maps directly to "is the connection active"; use Human.OrganBrain.IsOnline to distinguish an online vs offline player (online = a real network connection is present; offline = the body is still simulated but no client is driving it).
To enumerate every player that has a live network connection (including unconscious):
foreach (var h in Human.AllHumans)
{
if (h == null) continue;
if (h.State == EntityState.Dead || h.State == EntityState.Decay) continue;
if (h.OrganBrain == null || !h.OrganBrain.IsOnline) continue;
// h is an online, non-dead player
}
Human.LocalHuman (static property on Human) returns the local player's Human instance or null when running headless / before spawn.
Ownership: OwnerClientId, CreateCharacter and respawn¶
Thing.OwnerClientId (a ulong) is how the game ties a Human to a player: it holds the player's client id, the Client.ClientId of the connection that owns the body. Its setter and change guard are on Client, which also traces where a server gets the id from. Line numbers in this section are the 0.2.6428.27798 client decompile (.work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs); everything cited is textually identical in the dedicated-server build (diffed: Human.Respawn through CreateCharacter at 354991-355218, OnEntityDecay and CreateBodyBag at 355626-355680, Entity's decay at 295397-295459, RespawnMessage at 270707).
It is set in the call that creates the body. Human.CreateCharacter (368800-368850) creates the body and sets its owner three statements later, in the same synchronous call:
// Assets.Scripts.Objects.Entities.Human :: CreateCharacter (excerpt)
// .work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs:368817-368820
Human human = OnServer.Create<Human>(Prefab.Character, safePositionInRadius, rotation);
human.WorldCenterOfMass = human.ActiveRigidbody.worldCenterOfMass;
human.StartLocation = startLocation;
human.SetBasicsData(clientId, steamName);
// Assets.Scripts.Objects.Entities.Human :: SetBasicsData
// .work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs:368680-368685
public void SetBasicsData(ulong clientId, string clientUserName)
{
OnServer.PublishCustomName(this, clientUserName);
base.OwnerClientId = clientId;
base.name = "Character_" + clientUserName + "_" + clientId + "_" + clientUserName;
}
A server processes incoming messages on the Unity main thread (NetworkManager), and CreateCharacter runs there to completion first, so no message a client sends about the new body can be processed while its OwnerClientId is still unset.
Nothing on a server clears it. The assembly has eight OwnerClientId = assignments. For a Human the ones that matter are SetBasicsData (reached from CreateCharacter; from Human.DeserializeOnJoin, which is a joining client's copy of a body; and from MoveBodyBagToSlot, 376294-376319, which builds a new body for a body bag's brain with SetBasicsData(brain.ClientId, text)) and the load path, OwnerClientId = saveData.OwnerSteamId; in Thing.DeserializeSave (327253). The rest are DynamicThing.CreateOld at creation (39947) and the structure-placement path (ConstructionCreationMessage, CreateStructureInstance, Structure.SetStructureData). None of them resets an existing body's owner. Giving up control clears the brain's id, not the body's: OnServer.RelinquishBrain sets playerBrain.ClientId = 0uL and Brain.RelinquishControl sets ClientId = 0uL, both on Brain.
A respawned player can own more than one Human. The give-up button of the death dialog (368524) calls Human.Respawn():
// Assets.Scripts.Objects.Entities.Human :: Respawn
// .work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs:368623-368649
private void Respawn()
{
CosmeticsBehaviour.SetGameObjectsAsDefault();
CosmeticsBehaviour.UnregisterEvents();
InventoryManager.ParentBrain.RelinquishControl();
SetDefaultLayerForSlotted();
LodManager.EnqueueRequesterToRemove(this);
if (Assets.Scripts.Networking.NetworkManager.IsClient)
{
if (ClientRequestCharacter.Initialized)
{
return;
}
ClientRequestCharacter.Initialize();
NetworkClient.RequestCharacterAsync(isRespawn: true, ClientRequestCharacter.Token).Forget();
}
else
{
SerializedClientInfo clientInfo = GameManager.GetClientInfo(base.OwnerClientId);
bool flag = clientInfo != null;
StartLocationData startLocation = (flag ? DataCollection.Get<StartLocationData>(clientInfo.StartLocationHash) : null);
ISpawnPoint spawnPoint = (flag ? Referencable.Find<ISpawnPoint>(clientInfo.SpawnPointReference) : null);
Human newHuman = CreateCharacter(base.OwnerClientId, DisplayName, CosmeticData, flag, startLocation, spawnPoint);
AwaitTerrainTakeControl(newHuman).Forget();
}
OnEntityDecay();
}
On the simulating side (single player, a listen host's own player) the new body gets the old one's OwnerClientId, and the old body is decayed in the same call. A remote player's respawn goes the other way: the client sends a RespawnMessage, and the server's RespawnMessage.Process (284330-284338) does nothing but Human.CreateCharacter(HumanId, ...) with the player's client id. No server code touches the old body on that path. It stays Dead, stays in Human.AllHumans and keeps the player's OwnerClientId until its own decay, which runs only where the simulation runs:
// Assets.Scripts.Objects.Entities.Entity :: WaitForDecay
// .work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs:309035-309064
private async UniTaskVoid WaitForDecay()
{
CancellationToken cancelToken = this.GetCancellationTokenOnDestroy();
if (Mathf.Approximately(_currentDecayTime, 0f))
{
_currentDecayTime = GetDecayTime();
}
while (_currentDecayTime > 0f && State == EntityState.Dead)
{
if (GameManager.GameState == GameState.Paused)
{
await UniTask.NextFrame(cancelToken);
continue;
}
Thing thing = base.ParentSlot?.Parent;
if (thing is Entity || thing is CryoTube)
{
_currentDecayTime = Mathf.Max(60f, _currentDecayTime);
}
else
{
_currentDecayTime -= GameManager.DeltaTime;
}
await UniTask.NextFrame(cancelToken);
}
if (GameManager.GameState == GameState.Running && !cancelToken.IsCancellationRequested && State == EntityState.Dead)
{
State = EntityState.Decay;
}
}
Entity.EntityDeath starts it only when GameManager.RunSimulation (309022-309028), and GetDecayTime() is 60 (309030-309033, not overridden by Human). So a freshly dead body lasts at least 60 seconds of unpaused game time, and indefinitely while it sits in an Entity's slot or a CryoTube. When State becomes Decay, the Human state handler calls OnEntityDecay() (369216-369217), which on the simulating side moves the belongings to a box and runs CreateBodyBag() (369290-369312): the lungs and stomach, and the brain if it still carries a client id, move into a new DynamicBodyBag, and the method ends with OnServer.Destroy(this). AllHumans drops a body in Human.OnDestroy (367790-367799, the Remove at 367793; otherwise the list is only cleared wholesale, 210334); a body was added in its Human.Awake (367610, the Add at 367662 behind a Contains check).
Consequence for lookups. Human.Find(ulong clientId) (368769-368779) returns the first entry in AllHumans whose OwnerClientId matches, and the old body joined the list first, so while it exists Human.Find returns the dead body rather than the one the player now controls. To ask whether a particular Human belongs to a player, compare that Human's OwnerClientId with the player's client id; do not compare it with Human.Find(clientId). The full respawn flow, body bag included, is on RespawnFlow.
Verification history¶
- 2026-04-20: page created from the Research migration; verbatim content lifted from F0024. No conflicts.
- 2026-04-21: added "Enumeration: Human.AllHumans" section. Additive only; no existing content changed. Verified against
Assets.Scripts.Objects.Entities.Human(line ~45:public static readonly List<Human> AllHumans = new List<Human>();) in game version 0.2.6228.27061. - 2026-09-25: added "Ownership: OwnerClientId, CreateCharacter and respawn" against game 0.2.6428.27798, client and dedicated-server builds (diffed identical). Records that
Human.CreateCharacter(368800-368850) setsOwnerClientIdthroughSetBasicsData(368680-368685) three statements afterOnServer.Create<Human>, inside the creating call; that none of the eightOwnerClientId =assignments in the assembly resets an existing body's owner, while relinquishing control clears only the brain'sClientId; and that a remote player's respawn (RespawnMessage.Process, 284330-284338) creates the new body with the same client id and leaves the old oneDead, inAllHumansand still owned untilEntity.WaitForDecay(60 s of unpaused game time, held while the body is in anEntity's slot or aCryoTube) setsDecayandCreateBodyBagdestroys it, whereas the simulating side's ownHuman.Respawn(368623-368649) decays the old body in the same call. SoHuman.Find(ulong)(368769-368779), which returns the first match inAllHumans, can return the dead body in that window. Additive: consistent with "Enumeration: Human.AllHumans" (dead bodies are in the list) and with RespawnFlow (the old body becomes aDynamicBodyBag, which the new section places at decay). Those sections and "Player identification" were not re-read and keep their 0.2.6228.27061 stamps; the page frontmatter moved to 0.2.6428.27798 for the new section.
Open questions¶
None.