Skip to content

BONELAB

Platform ID com.stresslevelzero.bonelab

DigitalHeaven.Mods.Bonelab Unity IL2CPP is a MelonLoader mod that makes DigitalHeaven avatars first-class BONELAB avatars. Each avatar is built at runtime from its DH pallet. No Unity Editor, AssetBundle or Addressables catalog is involved.

Mod loaderMelonLoader 0.7 (net6), IL2CPP
Game version1.744 (Unity 2021.3.16f1, IL2CPP x64)
Rendering pipelineSLZ’s URP fork (SLZ/LitMAS/LitMAS Standard)

This mod depends on the DigitalHeaven.Unity library, and on its IL2CPP platform for model loading, the runtime humanoid Avatar and material creation. On Unity 2021.3 that platform uses the managed mesh, blend shape and image APIs, which the game’s metadata still holds. On 2023.2 (MegaBonk) it calls the icalls instead. The per-version layout table in Il2CppHumanoidLayout picks the path.

FeatureStatus
Avatars in the avatar menu and the body logYes
Body log hologram (preview mesh)Yes
Game physics body (colliders, masses, grips)Yes
Game-shader materialsYes
Material decals (DH/LitMAS Decals, shipped in the mod)Untested
Spring bonesYes
Spring collidersUntested
Eye look and blinkUntested
Mouth (visemes to LabFusion voices, the microphone and dh.speakSound clips)Untested
Reaction sounds (jump, footsteps, hard landing, pain, dying, death, recovery; wav, ogg and mp3 clips)Yes
Soft-body fit to the meshNo
OverlayUntested
Paper doll (headset and desktop)Yes
World mirror (game mirror or screen, prototypes)Untested
Settings page in the game’s preferences menuUntested
Safety: avatar scale danger colors, launch guard, “Keep this size?”, radial menu entry, death-loop rescueUntested
DH maps as levels in the level select (singleplayer, lit in real time)Untested
Voxel volumes in maps (Minecraft worlds, .vox models) streamed around the playerUntested
Avatars as NPCs in the spawn gun, one entry each, hostile or friendly and riding Ford’s rig (or the base its author chose)Experimental
Wounds and blood on DH NPCs (BONELAB’s own pose-space wounds)Untested

Everything that makes a BONELAB body feel like one, from the physics colliders and the soft body to grips, masses and IK proportions, is derived at runtime from one SLZ.VRMK.Avatar component. Menus, the body log and save-file favorites find avatars through AvatarCrates in the game’s AssetWarehouse. The mod builds both from code.

  1. Boot registration. When the asset warehouse is ready (AssetWarehouse.OnReady, with a per-frame poll as a backstop), the mod lists every .dh-avatar in the DH pallets, which come from the same workspace every other DigitalHeaven mod uses. It registers one Marrow pallet, DigitalHeaven.Avatars, and one AvatarCrate per avatar, with a barcode and a title only (Pallet.CreatePallet, Crate.CreateCrate, AssetWarehouse.AddPallet). No avatar is built at boot. A hologram mesh is attached only if the preview cache or a stand-in the pallet ships already holds one.
  2. Build on demand. An avatar is built the first time it is wanted, and the build is kept for the session. The warm-up builds the save’s current avatar and its body log favorites in turn after registration, outside any game call. It reads each one off the main thread first: its models are parsed, and its textures are decoded (PNG and JPEG by the managed decoder, KTX 2 transcoded) with their mips generated. The main thread uploads BuildUploadBudgetMs of those textures per frame, at least one, and builds the avatar once they are all in. The avatar menu’s mirror preview (AvatarsPanelView.SwapReflectionAvatar) never builds: for an avatar not built yet it shows the player’s current avatar, moves the selected one to the front of the warm-up, and shows it once it is built, if the menu still has it selected. A swap (RigManager.SwapAvatarCrate), a body log favorite whose hologram is not cached (PullCordDevice.UpdateAllPreviewMeshes), a LabFusion swap and an NPC still build an avatar the warm-up has not reached synchronously, inside that call, so the game’s own load that follows finds the asset ready; whatever the read-ahead already cached is not read again.
  3. Rig. The shared loader builds the avatar, and its limbs are straightened into a T-pose. The template stays in that T-pose, because the game measures and animates from it. The rig is then completed. The game’s code shows which humanoid bones PrecomputeAvatar reads without a null check: every body bone, both toes, the neck and shoulders, the thumb, index and ring chains always, and the middle and little chains once their knuckle exists. Fingers are mapped from the skeleton rather than from the rig, through the shared HumanoidFingerTopology. Each chain starts at a skinned child of the hand, skipping a palm bone, and follows single skinned children up to a tip marker such as _end. A renamed duplicate (__dh and a number) is an accessory armature’s copy of a body bone and never joins a chain; the Taidum’s collar armature hangs a copy off each knuckle, one segment off, which stopped every finger at its first bone. Before the shape decides, a chain shorter than three bones goes on through the one real child named for the same finger, skinned or not. Each hand logs its whole subtree under [Fingers], with every bone’s parent, whether it is skinned and its length. A bone sitting exactly on its parent never joins a chain: that is an accessory armature’s copy, which dh.armatureLink reparents onto the body bone of the same name. The rig’s finger entries only hint which chain is which, then bone names decide, then position: the chain furthest toward the thumb side is the thumb, and the rest run index, middle, ring and little. A chain’s bones fill the finger’s three slots in order; a four-bone finger drops its palm bone, and a thumb keeps its first three, since Unity’s first thumb slot is the thumb’s metacarpal. Rigs disagree on this, and one that names its thumb by Unity’s slots would otherwise be pushed a slot down. Only a slot with no real bone gets an empty, unskinned leaf; a middle or little finger the skeleton lacks gets none, because the game does not read it. Each hand logs [Fingers] with every slot’s bone, how the chain was chosen, and the thumb’s axes. Missing toes are added under the feet, and a missing neck or shoulder is inserted into the chain above the head or upper arm. The added bone replaces a rig entry that names a bone with no transform, rather than being appended after it. A bone the body hangs on, such as the hips, a spine bone or a limb, cannot be added, and the log names it. A missing bone makes PrecomputeAvatar throw, and RigManager.SwitchAvatar catches that exception, logs Failed to load avatar to Player.log, and refuses the body. Duplicate bone names are renamed. A humanoid UnityEngine.Avatar is then built on the root Animator through Il2CppHumanoidAvatarBuilder.
  4. Game avatar. The mod adds SLZ.VRMK.Avatar and wires what the Marrow SDK inspector would. The Unity Editor creates every serialized class field of a saved component, so an SDK prefab always carries its six soft bulges (all zero when nobody edited them), its artTransforms, artOffsets and poseOffsets holders, and a surface data reference. A component added at runtime leaves them null. PrecomputeAvatar writes into the holders without creating them, and ComputeMass reads every bulge, so the mod fills them the same way the Editor would. The wrists are the hand bones, as in the SDK’s own Reset. Skinned renderers go into body, head and hair meshes by name. An eyeCenterOverride is always set, because RigManager.SwitchAvatar refuses a body when its Animator reports no LeftEye bone and there is no override. It sits at the eye bones’ midpoint when the rig has both eyes. Otherwise it goes at the avatar’s compiled eyeHeight and eyeForward, or at the renderer height times the shared eye fraction. All of this happens with the template briefly out of its holder, because an inactive Animator never binds its humanoid Avatar. Outside that call, the template sits active under an inactive, persistent holder, like an Addressables prefab, so every clone the game makes starts active. That matters for the body mall’s mirror preview: it instantiates the avatar under its preview transform and calls PrecomputeAvatar on the clone without activating it first. The mod then runs the game’s own PrecomputeAvatar and RefreshBodyMeasurements once, so the log shows the height, masses and stats, and resets the template to not precomputed, like an SDK prefab. The game measures each clone again on swap.
  5. Crate assets. The crate’s main asset and preview mesh get a completed Addressables handle (ResourceManager.CreateCompletedOperation) whose result is the in-memory object. Anything that loads through MarrowAsset then receives the DH object directly.

Marrow barcodes allow only letters, digits and dots, up to 120 characters. A DH barcode maps to DigitalHeaven.Avatars.Avatar.<pallet id>.<avatar path>, with every other character dropped and path separators turned into dots. For example, dev.example:avatars/mayu-custom.dh-avatar maps to DigitalHeaven.Avatars.Avatar.dev.example.avatars.mayucustom. A readable form gets an FNV-1a hash of the full DH barcode appended, cut to fit, for example ….h1a2b3c4d, when it is too long, when it is already an existing crate, or when more than one DH avatar reads the same. In the last case every one of them is hashed, so no avatar’s barcode depends on which pallet loaded first. The mapping otherwise depends only on the DH barcode, so saved favorites keep resolving across launches. Every mapping is logged under [Barcode].

AvatarsPanelView.CalculateSceneList builds the menu. It takes every AvatarCrate in the warehouse, removes each one a CrateFilters filter rejects, groups the rest by tag, and pages through them. The game’s own code shows that those filters key on the crate’s Unlockable flag and on PlayerUnlocks.UnlockCountForBarcode in the active save. UnlockedAndNotRedactedCrateFilter, for example, keeps only a crate that is unlockable and unlocked at least once. DH crates are therefore registered as unlockable, and the mod unlocks each one once in the active save. It does this at registration if a save is loaded, and again right before every menu rebuild. PlayerUnlocks.ClearUnlockForBarcode undoes it.

After registration, the mod rebuilds any avatar menu already in the scene. Every rebuild logs [Menu] '…' rebuilt: N avatar(s), M of 13 DH. The first one also logs what each crate filter decides about a DH crate.

The game poses every finger bone of a hand, the thumb included, through one rotation offset measured from the index proximal. PrecomputeAvatar writes only indexLf1Offset and indexRt1Offset; the thumb, middle, ring and little offsets are never written or read. ArtRig.ApplyRotationOffsetsToRig gives all 15 art finger bones of a hand that offset, and ArtOutputLateUpdate copies each art bone’s world rotation onto the avatar’s bone. A finger bone whose roll about its length differs from the index proximal’s is therefore posed rolled by that difference. On a Mayu that is 155 to 180 degrees on each thumb bone, which puts the thumb pad on the outside. On a Taidum it is also about 48 degrees on the ring and little fingers.

After the rig is completed, and before the humanoid Avatar is built, the mod rolls every finger and thumb bone about its own length until its roll matches the index proximal’s. It measures the index proximal’s bind rotation against an anatomical frame (its length, and the back of the hand), works out the turn that would give every other finger bone that same relation to its own anatomical frame, and applies only the twist part of that turn, never the swing. A bone’s length runs toward the next real bone of its chain, else toward its farthest real child, and never toward a placeholder the mod added. Run 8 applied the whole turn, with the Taidum’s length axes read toward placeholders laid along the forearm, and its fingers came out pointing sideways. A finger’s nail side is the back of the hand. A thumb’s is away from a point under the middle of the palm, which its pad faces. Only the rotation changes: children keep their world pose, and the skin’s bind poses absorb the turn on the avatar’s own copy of each mesh, so the mesh does not move. Each hand logs [FingerFrames] with every bone’s roll, and the full turn in brackets. The reference capture logs the same measurement for the stock avatar without changing it, and names the ThumbRollDegrees that would make a DH thumb sit like the stock one. ThumbRollDegrees adds that roll on top.

The crate’s collider bounds are the bounds of the baked hologram mesh in root space. They are wide because the template is in a T-pose, where arm span is about equal to height. They reach back behind the avatar because a tail counts too: a Mayu’s tail puts the center about 0.46 m behind the feet and the depth at about 1.2 m. The root is not offset. Skinned renderer bounds are not used, because they come from the import-time local bounds, and the rig’s 180-degree Hips correction can mirror them front to back. On the Taidum, they put the center 0.38 m in front of the feet, while the baked geometry sits 0.37 m behind.

When MeshCroncher’s bake is less than a quarter of the renderers’ height, the mod bakes the largest skinned mesh again with its scale. BakeMesh without scale drops a scaled armature, such as a centimeter FBX, and first-run logs showed the NovaBeast’s hologram at 6 cm.

The game releases crate assets on level changes. A completed handle dies when its reference count reaches zero, and a dead handle cannot be loaded again, because the next load would ask Addressables for a key that no catalog holds. The mod therefore gives each DH handle 4096 extra references when it arms it. It re-checks every DH handle when each level starts and re-arms any that still ended up invalid. A level counts as started once a new player rig exists, because BONELAB never fires MelonLoader’s OnSceneWasInitialized; the world mirror is placed again on the same signal. Templates, meshes, materials and textures carry IL2CPP GC handles. The mod’s own long-lived assets (the humanoid avatars, preview meshes, fade materials, sounds, pallets and crates) are flagged DontUnloadUnusedAsset so the unused-asset sweep on a level load leaves them alone. The shared import caches are not flagged: a cached model loader’s original meshes are exactly the ones nothing renders once an avatar draws its own copies, so the sweep takes them. Every cache hit checks Unity liveness instead, rebuilds a destroyed mesh, material or texture and logs cached model loader for '<path>' had N destroyed mesh(es), so a build after a level load costs one fresh mesh build rather than holding every original for the life of the process.

The first two builds patched MarrowAsset.ReleaseAsset and UnloadAsset to refuse releases instead, and both runs crashed on boot with a stack overflow. IL2CPP folds methods with identical machine code into one function, and UnloadAsset(bool) and ReleaseAsset(bool) are one body. Two detours on one body call each other forever. The mod patches no MarrowAsset method now, and it refuses any patch whose native code another method of the same class shares, logging [Patch] … refused.

The hologram mesh comes from the game’s own MeshCroncher.CronchMesh. The Marrow SDK uses the same call when it packs a pallet: it bakes the active skinned meshes, merges them and simplifies the result. The mod runs it on the template while the template is briefly active. If it fails, the preview falls back to the largest skinned mesh, baked in the template’s pose.

A hologram can also come from the avatar’s model stand-in (see model stand-ins), before any avatar is built. At boot, an avatar with no cached hologram whose pallet ships a .dh-proxy beside each model it imports gets a hologram from it (ObjectProxies.Read, which reads definitions and companions only), and its crate’s collider bounds are the stand-in’s. When the game asks for a hologram of an avatar that has neither, the mod builds a stand-in from the avatar’s models with the same builder the compiler uses (ModelProxyFallback, at the workspace’s proxyTriangles and proxyBytes), which decodes the models but still builds no avatar. A stand-in with no silhouette, because the build could only measure the model, becomes a box of its bounds. The stand-in is the model’s bind pose in the avatar’s root frame, mirrored across Z like every mesh the mod uploads, so it can differ from the T-pose bake in pose. The order is the preview cache, then a shipped stand-in, then a built stand-in, then the MeshCroncher bake. A bake always replaces a stand-in once the avatar is built: the crate’s hologram handle is re-armed with it, its bounds become the baked ones, and it is written to the preview cache. Nothing is cached for a stand-in, since reading one is cheap.

Baked holograms are cached on disk, in the workspace cache directory (.dh by default) under bonelab-previews, one file per avatar. The file is named by the Marrow barcode and a key that hashes every DH pallet’s content hash together with PreviewMeshQuality. A boot reads matching files, so the body log shows those holograms without building anything. An avatar can inherit from another pallet, so any pallet change invalidates every hologram. A stale file is never read, and it is replaced the next time that avatar is built.

BONELAB renders its avatars with SLZ/LitMAS/LitMAS Standard, and IL2CPP builds ship only the shader variants the game’s materials use. The mod therefore clones a loaded game material on that shader: the plainest one loaded, with stock Lit first, then the fewest keywords and texture slots, opaque before transparent. The clone is then scrubbed to a neutral surface: no keywords, every texture slot empty at a unit scale, details and emission off, opaque and back-face culled. The DH loader clones this material for every DH material, so anything left on it would show wherever a DH material leaves a slot unwritten. An unscrubbed clone of Ford’s harness once put its normal map on a DH avatar’s fur. The log names the chosen material and any texture slot left set. When none is loaded, it builds a material from the shader, which it finds the way every game shader is found (see Game shaders). That reference material is also the DH loader’s host fallback, and the mod sets MaterialLoader.UseFallbackMaterialOnly, so the loader builds straight onto it instead of first looking up shaders the game renamed or never shipped. Each DH material is then moved onto the reference in place: the shader, its keywords and its defaults are kept, and only the surface is written.

  • Renderer settings: every DH skinned renderer copies its light and reflection probe usage, shadow settings, rendering layer mask and motion vectors from a loaded game avatar’s renderer, and logs both. That renderer is one of the avatar’s own bodyMeshes drawn with the reference shader. Any other skinned renderer under the rig can be the comfort vignette (Vignetter, SLZ/Vignette), whose light and reflection probes and shadows are off; the first seven test runs copied it, which left DH bodies lit by the flat ambient probe alone.
  • Albedo and tint: _BaseMap and _BaseColor, with the DH tiling (a material’s uvScale, which the shared loader writes into every texture slot it binds) and offset. The base map is bound as the shared loader built it: a texture with the palette role already has a single level, so its cells are never averaged. Every base map logs its filter, wrap, mip count and _BaseMap_ST. Repacked normal and MAS maps take the filter, wrap and mip state of their source, so a palette’s companions also have one level.
  • Normal map: _BumpMap, turned on with _Normals. LitMAS multiplies smoothness by the normal map’s blue channel, which it treats as geometric smoothness. The DH normal map is therefore repacked with its XY kept and blue set to 1.
  • MAS: _MetallicGlossMap holds metallic in R, ambient occlusion in G and smoothness in B. It is built from DH’s glTF-packed metallicRoughness (roughness in G, metallic in B), its occlusion texture, and the metallic, roughness and occlusionStrength scalars. Without either texture, the scalars become one texel. An unauthored roughness is the DH default, fully rough, as the engine draws it; it used to be 0.5 here, which gave every unauthored surface a sheen. An empty MAS slot reads white on LitMAS, which is metallic and fully smooth, so every converted material gets one.
  • Emission: _EmissionMap and _EmissionColor, from DH’s linearized emissive color times its intensity, turned on with _Emission. LitMAS dims emission by pow(N·V, _EmissionFalloff), and the stock materials keep the falloff at 1, so a glow faded as its face turned away from the camera. The reference sets it to 0: a DH emission looks the same from every side, as in the engine.
  • Unlit: in a map, a material authored with "shader": "unlit" takes the game’s unlit shader (MapUnlitShader, default Universal Render Pipeline/Unlit) when that shader can be had (see Game shaders) and is supported, with its base map, color, culling and alpha (blend, or a real alpha clip). The log says once which route a map’s unlit materials took. Everywhere else, and in a map when no such shader is loaded, the material stays on LitMAS with its surface moved into emission. The albedo goes black but keeps its alpha, the base map and base color become _EmissionMap and _EmissionColor, and the surface is matte, non-metallic and without a normal map or screen-space reflections, so lighting adds as little as the shader allows. URP Unlit is shipped as a catalog dependency and need not already be in memory; LitMAS emission remains the fallback if it cannot be loaded. The log names the shading each material took (shading=) and, once, which shaders named unlit are loaded.
  • Surface: culling follows _Cull, and _SSRTemporalMul is 0, as the shader asks for skinned meshes. Transparent materials use alpha blending without depth write. LitMAS Standard has no alpha clip and none of its shipped variants hold one, so an avatar’s cutout ("alphaMode": "mask") is drawn blended from a copy of its base map whose alpha is already clipped at the material’s alphaCutoff; it used to render opaque with the clipped pixels black. A map’s masked materials take the game’s URP Lit with a real alpha test instead (Maps).

LitMAS switches normals and emission with float properties rather than keywords, so every converted material lands on a variant the game’s own materials already use. Textures are read through a GPU blit, so compressed and non-readable DH textures repack the same way. The per-pixel math uses .NET’s own math rather than UnityEngine.Mathf, because each interop Mathf call is a native invoke; that alone cost a second per 2048x1024 map. DigitalHeaven.Unity’s MaterialSemantics records every DH channel and scalar the loader saw, including those the host shader has no slot for, which is where the MAS inputs come from.

LitMAS has no matcap. A material whose only shine is a matcap (the black metal of mltn’s glasses: black tint, a gray sphere, authored metallic 0 and roughness 1 as fallbacks the source shader never read) would otherwise convert to a flat, matte black. So a material that declares a replace matcap with no mask and no metallicRoughness map is approximated, in MatcapLook. The loader records the sphere on MaterialSemantics.Matcap, and the mod reads it back through a 32 by 32 GPU blit and measures the circle inscribed in it in linear light (its mean color, mean luminance and brightest texel). With the matcap’s strength clamped to 0 to 1 as the weight w:

  • Metallic moves from the authored value toward 1 by w.
  • Smoothness moves from the authored 1 - roughness toward 0.3 + 0.6 * contrast by w, where contrast is 1 - mean / peak: an even sphere is matte, a dark one with a bright highlight is glossy.
  • Albedo (a metal’s reflectance), when there is no base map, moves from the material’s color toward the sphere’s mean color times the matcap’s tint, lifted a quarter of the way toward the brightest texel, by w.

A masked matcap (a bell’s shine, a skin gloss), one that adds or multiplies, one beside a metallic-roughness map and a material with no matcap keep exactly what they authored. The conversion’s log line ends with the matcap’s measurements and the values it chose.

A DH material’s decals need a shader that draws them, and LitMAS has no slot for one. The mod therefore carries its own variant of it, DH/LitMAS Decals: SLZ’s LitMAS.shader with the same passes (Forward, DepthOnly, DepthNormals, ShadowCaster, Meta; BakedRaytrace is dropped) and keywords, plus up to four decal layers over the albedo. It is built in the Unity editor against the Marrow SDK (UnityReference/UnityRef-Marrow, Unity 2021.3.16f1 with SLZ’s URP 8148.0.7-4) through SLZ’s own ShaderInjector: the copied recipes add Injection_DhDecals_CBuffer.hlsl to the Forward, DepthNormals and Meta passes, so UnityPerMaterial keeps one layout across them, and Injection_DhDecals.hlsl to Forward, which lays the decals over the albedo after the base color and before the detail map. Rebuild it with bun build.ts Marrow --build litmas-decals in UnityReference (the open editor takes the request, or batch mode runs it); it writes Output/2021.3.16f1/windows/dh-litmas-decals (LZ4) and an uncompressed copy.

  • Variants. Direct3D 11 only. The build keeps the game’s URP stripping (SLZ’s PC and Quest pipeline assets) and every fog mode and the mixed-lighting globals (SHADOWS_SHADOWMASK, LIGHTMAP_SHADOW_MIXING), and strips the per-renderer lightmap variants (LIGHTMAP_ON, DYNAMICLIGHTMAP_ON, DIRLIGHTMAP_COMBINED), since Unity never lightmaps a DH avatar or a DH map surface; keeping them made a 53 MB bundle. The result is 3,120 programs (Forward 3,072 fragment and 32 vertex, ShadowCaster 4 and 4, DepthOnly and DepthNormals 2 and 2; Meta compiles to nothing without lightmapping) over 13 keywords, with no compile errors: 9.3 MB with LZ4, 13.2 MB uncompressed. The game’s LitMAS ships 12,288 Forward fragment programs, most of them lightmapped. _NO_SSR is a material feature no decal material turns on, so its variants are not built.

  • Shipping. The LZ4 bundle is an embedded resource of the mod (Shaders/dh-litmas-decals.bundle), as the menu icon is, so nothing extra is deployed. DecalShader loads it once with AssetBundle.LoadFromMemory and keeps the bundle and the shader for the session. A missing or unloadable bundle, or a device that cannot run the shader, says so once and every material stays on stock LitMAS.

  • Which materials. Only a lit material with at least one decal takes DH/LitMAS Decals; it is given every LitMAS property exactly as on stock LitMAS, then the decal rows. Everything else stays on stock LitMAS. An unlit material (whose albedo is black under the emission route) and a voxel atlas never take it.

  • Decal data. The DH loader loads each decal’s texture and mask (prefetched with the material’s other textures) and records them on MaterialSemantics.Decals; UnityMaterialDecals in DigitalHeaven.Unity writes them onto any shader with the _DhDecal{i}* slots, four vectors per decal laid out as the engine’s rows. The blend math is the engine’s, in linear light; placement runs in DigitalHeaven’s UV space and the sample flips back, since Unity meshes and textures are V-flipped. Every decal shares the base map’s sampler, so its wrap and filter are the base map’s.

  • UV sets. Decals and masks read UV sets 0 to 2, which DH’s mesh build writes whenever the source has them. LitMAS carries no fourth set, so a decal on uv3 is drawn on uv0 with a warning.

  • Wounds. An NPC’s decal material still gets its pose-space wound copy; the copy is drawn without the decals.

  • License. The shader is a derivative of SLZ’s URP fork, which is under the Unity Companion License (© 2020 Unity Technologies ApS); the notice ships in the mod’s THIRD-PARTY-NOTICES.md and beside the shader source. It is built only with Unity’s editor for use in a Unity game. Compiling it with DigitalHeaven’s own toolchain is not done.

  • Not yet. Quest (a Vulkan build of the same shader) and uv3.

BONELAB has no chain system of its own: its soft body is a fixed set of torso and limb rigidbodies (PhysSoftBody), with no slot for ears, tails or a bell. DH chains therefore run on the shared DigitalHeavenSpringBone solver, on three kinds of clone.

  • The body. Under IL2CPP, Instantiate copies an injected component but none of its managed fields, so each clone the game makes has empty spring chains. After every RigManager.SwitchAvatar to a DH body, Il2CppSpringBones.BindClone (shared with MegaBonk) copies each chain’s settings from the template and maps its bones into the clone by sibling index. Each chain logs its mapped bones, marking any that no skinned renderer deforms. A shared SpringChainDriver then steps the chains in a postfix on ArtRig.ArtOutputLateUpdate, right after the game writes the body’s pose, and their own LateUpdate is off so they step once. If that method could not be patched (the mod refuses one whose native code another method of its class shares), the chains step themselves instead. The chains simulate on a fixed 64 Hz tick, which is slower than the headset renders. On every frame, ticked or not, each chain draws its last two tick states carried onto the body’s current pose and blended by how far the clock stands past the last tick. That is the engine solver’s rule, through the shared SpringBoneDrawnPose. Run 21 still showed the last tick raw, which left the ears standing still while the head moved and then snapping back at the next tick. Every 120 steps the driver logs how many chains it stepped, over how many rendered frames, and how far the furthest bone stood from its animated pose, so a log shows whether the chains move. It warns if a body is stepped twice in one frame.
  • Mirror reflections. A Mirror instantiates its own copy of the avatar and poses only its humanoid bones (Mirror.WriteTransforms), so its chains would otherwise hang still. After that call, the body’s spring bone poses are copied onto the reflection, local position and rotation bone for bone (PoseCopy). The reflection’s own chain components stay off; a second simulation would drift from the body.
  • The avatar menu preview. Nothing poses it, so its chains are bound the same way and step themselves.

Chain geometry is simulated in world space, so it follows any scale the body has. The bind, mirror and step lines log the body’s scale; run 14 measured 1.000. Gravity is scaled by the bones’ local scales up to the avatar root, measured once when a chain starts, so a later rescale of the avatar root itself does not change it. That holds in every Unity host.

The live capture takes a second body front and side 0.3 s after the player’s shots (player-plus0.3s-*), so two frames show whether the chains move.

Hands push the chains a pallet opts in with allowCollision, on top of the colliders each chain lists. Every DH body carries a palm and five finger capsules per hand (SpringHandSet, shared in the Unity runtime): the avatar’s hand colliders as its pallet resolves them, or, for an avatar compiled before they were generated, the same capsules built from the rig’s hand bones at the template’s bind pose. A finger’s capsule starts on its first bone and its end rides its last, so a curled finger’s capsule curls with it. They are read off the posed bones, the fingers you see, once a frame into one touch frame (BonelabTouch), and each body takes the hands its filter admits through the same Core push and sweep as its listed colliders (SpringTouchTicks); only the hands within reach of a chain are tested, at most SpringTouchMaxVolumes (12) per chain, the nearest first.

Whose hands are read:

BodyIts handsSelf or others
Your DH bodyIts hand colliders on the art rig’s bonesSelf for your chains, others for everyone else’s
A LabFusion player’s or a [Player rig] NPC’s DH bodyIts hand colliders, on its rep’s art rigSelf for its own chains, others for yours
A [Ford] or [DH rig] NPCIts avatar’s hand colliders, on the bones that ride the base’s muscles (a [Ford] build maps them before its bones move, and each spawn from the build)Others for everyone’s chains
Any rig on a body that is not DH, yours includedThe physics hand’s palm and finger boxes (PhysHand.handCol, fingersCol), as capsulesAs above

Cost is bounded by bodies, not chains. Only bodies within SpringTouchRange (3 m) of a body some hand may touch are read, at most SpringTouchRigs (8) of them, nearest first, with yours always among them; a body farther than SpringTouchLodDistance (6 m) from the camera skips touch, though its chains still swing. Touch needs no LabFusion message: every machine already simulates every DH body’s chains, and other players’ hands are the reps’ hands in your scene, so every viewer sees the same boop up to rep latency.

A hand grabs the chains a pallet opts in with allowGrabbing, under the same switches as touch. Every slot but a chain’s anchor is a grab point, so a tail of N bones offers N−1; a zero-length segment (an export duplicate) offers none.

  • The handle. One per hand of yours, made once the default player rig asset has loaded: a copy of SLZ’s own configured SphereGrip from that asset (its host, rigidbody, sphere and hand pose), never spawned and parented to a DigitalHeaven root outside every rig, so no rig’s entity and no LabFusion grip index sees it. Run 43’s lesson is why it is a copy and not a grip built by hand. The bind log names the stock grip’s inventory row beside the copy’s, and whether the copy carries a MarrowEntity (it should not; one is removed). SLZ’s sphere is a trigger, and the hand’s hover query (an overlap sphere that ignores triggers) never finds a trigger, so the copy’s sphere is made solid and told to pass through every collider of your own rig; before that, tails could be pushed but never grabbed. Colliders the rig gains later (a head fit’s capsules, a moment after each swap) are passed through too, each time the handle arms on a new slot, so a handle on an ear never pushes your own head.
  • Arming. While a hand is free, the nearest grab point within SpringGrabHoverReach (12 cm) of its palm arms that hand’s handle: it stands kinematic on the slot, at least SpringGrabMinRadius (3.5 cm) across, and moves to another slot only when that one is SpringGrabHysteresis (2 cm) nearer. Nothing in reach, it is off.
  • Holding. When the game attaches the hand, a grab record is made (who, which hand, whose body, which chain and slot, the offset the hand took it at; Core’s BodyGrab, plain data a LabFusion message can carry later). The slot is pinned toward the hand every tick by Core’s SpringBonePin: it moves by the chain’s grabMovement (0 its own pull, 1 at once), the segments from the anchor bend to follow inside the chain’s maxStretch band, and the rest of the chain hangs from it. With snapToHand the slot goes to the palm; without, it keeps the offset it was taken at. The handle is tethered to the chain’s anchor at the chain’s reach by a kinematic anchor, so a hand stops at the reach and the tail stays straight toward it. Nobody is dragged yet.
  • The log. The [Spring] channel’s summary lines, shown at Quiet, tell the attempt once per change: grab handle check once per hand and rig (the handle’s layer against the hand’s hover mask, the sphere’s kind, what the hand’s hover query makes of the handle at the palm), grab: body ... once per body (how many of its chains your hand may grab, or that none allow it), and per hand armed, hovered, blocked (the hover step that refused it, in reach but not hovered), attached, refused and released.
  • Release. Opening the hand lets go, and the chain springs back through its own pull. A body that swaps, leaves or is destroyed, a switch turned off, or a level start lets go by force (Grip.ForceDetach, then the hand’s joint, object and hover lock), so no hand is left holding nothing.

A grabbed LabFusion player’s tail bends on your machine only; they see your hand closed near it. The grab layer (BodyGrabs) is part-agnostic: a part is offered through IBodyGrabProvider, spring slots are one (SpringGrabs), and a limb can be the next with the same handle, arming, record, tether and release.

Log lines, under [Spring]: <Side> grab handle copied from '<grip>' in '<asset>': stock <row>; copy <row>; entity none once, the <Side> hand took '<barcode>' chain '<chain>' slot N; tethered at R mm from its anchor, and the <Side> hand let go of ... after S s: <why>; <Side> hand armed on spring chain ... at Verbose.

Four switches on Avatars > Spring bones override every pallet and apply at once, each a safety switch that turns yellow off its default:

  • Spring bones (SpringBones): off, every body’s chains take their overlay off and stand exactly as animated.
  • Touch and grab springs (SpringInteractions): off, no hand touches, grabs or poses any chain.
  • Others may touch mine (SpringOthersOnMine): off, only your own hands reach your avatar’s chains.
  • I may touch others’ (SpringMineOnOthers): off, your hands reach no other player’s or NPC’s chains.

The level. Avatars > Spring bones > Spring bones touch the level (SpringWorldCollision, on by default, applies at once, not a safety switch) keeps every chain out of the level, so a tail lies on the floor when an avatar lies down and stops at a wall. It is the engine solver’s world contact, shared through Core’s SpringContact: the particle is kept on its parent’s side by turning the segment about the parent, so a chain keeps its length and bends along the surface. After the limits each tick, per particle, SpringWorldContact asks BonelabSpringWorld (the interop physics, which the Unity library cannot call) one cheap Physics.CheckSphere and, only if it hits, OverlapSphereNonAlloc into a reused buffer, then Physics.ComputePenetration per collider for the push-out. A concave mesh or a terrain has no push-out, so the sphere is cast from the parent onto it instead. The sphere is the chain’s radius (at least 3 cm, plus 1 cm of margin, at the avatar’s scale). Nothing allocates. SpringWorldSweep (cfg only, off) also casts each particle from where the tick began, for a thin wall a fast tail would cross between ticks.

NPC spring limits. A DH NPC’s chains cost a world query per particle per tick, however far from you the NPC stands, so two limits cut that for NPCs only (your own and other players’ springs always run in full). Both are on by default, apply at once and sit on the NPCs page. Each NPC is looked at every fourth frame, from the camera’s position and the bounds of its cached skinned and mesh renderers, without allocating:

  • Level contact (NpcSpringLevelLimit, NpcSpringLevelMeters, 6 m): beyond the distance from your head, its chains skip the world queries (DigitalHeavenSpringBone.SkipWorldContact) but still swing and still feel colliders and hands.
  • Stop (NpcSpringFreezeLimit, NpcSpringFreezeMeters, 20 m): beyond the distance, or once none of its renderers has been visible to the camera for half a second, or its bounds lie outside the camera’s view widened by a quarter, the driver is paused (SpringChainDriver.Paused) and the body stands as animated. When it returns, every chain starts afresh from the animated pose, so nothing snaps or swings in. A frozen NPC also skips the level.

A limit lifts only once the NPC is 15% inside its line, so one standing on it does not flicker; a body that has just left the view keeps running through the half second. The decision is NpcSpringLod, plain code with its own tests; a change logs at Verbose under [Npc] with the distance and whether it is in view.

The mask is built by layer name: Default and Fixture (the level’s own geometry). Left out: Player, PlayerAndNpc, Feet, FeetOnly and NoFootBall (the player’s physics rig, which must not push the tail it wears), EnemyColliders (an NPC’s ragdoll), Interactable (grips and hover volumes), Dynamic (props, which a hand holds and a throw sends flying), Socket, Plug, the tracker and trigger layers, and every trigger collider. Colliders under the chain’s own avatar are skipped as well. The first use logs the mask under [Spring], and each 120-step report adds a world contact line: particle-ticks resolved, how many had a surface in reach, how many moved, the largest push, and the microseconds per particle-tick.

Log lines, under [Spring]: at a swap, '<barcode>': N hand volume(s), M from the avatar's colliders: leftHand generated r 21.0mm, ... once per template, then '<barcode>' on rig '<rig>': C chain(s) (a hand may touch them), N hand volume(s); each 120-step report ends touch: V hand volume(s) offered, S chain step(s) in reach, T test(s), max push P mm while a hand can reach the body. '<label>': C chain(s) off and on follow the Spring bones switch.

Avatars > Avatar scale (AvatarScale) multiplies the size of every avatar you wear, stock ones included, in 44 steps from 0.25x to 100x (default 1x, finer near 1x, then 5, 6, 8, 10, 20, 50 and 100 on the last page). The picker turns with a More cell and opens on the turn holding the current value. Sizes are colored safe, orange or red, a launch never starts at an orange or red one, a change into one asks “Keep this size?”, and the radial menu has an Avatar scale entry that works at any size: see Safety.

The mod scales the new avatar’s root just before BONELAB measures it in RigManager.SwitchAvatar, on your own rig only, so height, eye height, mass, strength and the tracking space follow by BONELAB’s own rule (nothing is edited afterward). A swap BONELAB makes itself (the saved avatar going on as a level starts, an avatar picked from the menu or the body log) takes the whole multiplier at once, so a level loads straight at 10x instead of growing again every time. Changing the value during play puts your current avatar on again once it has held still for a moment, so stepping through values swaps once. A live change is one swap at the whole multiplier, followed by a re-pose: the mod puts the physics rig back on its pose at its own feet with its velocities zeroed (the game’s own teleport), and scales BONELAB’s menu to the new avatar’s arms if the game left it at the old size. Every swap that changes the size gets the same re-pose, a live change or not. For two seconds after a level starts the mod leaves the avatar to the game, which puts the saved one on by itself. A live 1x to 10x change is one swap, not a walk in steps. Headless, the physics rig is thrown up to about 4 m and flails for 3 to 4 seconds before it settles upright, the same as a walk in hops of 2x did, so the single swap costs nothing the hops saved. For three seconds after each live change the mod logs, about every 0.1 s at the Summary level (so it shows at Quiet), a line beginning avatar scale diag: the controller rig’s pelvis height, the headset’s height over the feet, the play space’s scale, the physics pelvis and feet heights, the fastest physics body and its speed, and how many colliders sit below the feet. It is the record to read when a size change throws the rig: a controller pelvis that jumps points at the tracking target, a count of colliders below the feet at the rig overlapping its own floor. The menu’s distance follows the scale too: BONELAB sets the UI rig’s scale (the menu’s size) and the popup view’s own arm-scale field (its distance from your head) from your arms when a level starts and never again, so after a scale change it kept the old avatar’s size and distance (load at 2x then drop to 1x and it opened far away; load at 1x then grow and it opened in your face). The mod puts both on the worn avatar’s arms after every swap and again before every placement of the radial menu (hold B) or a popup page, your own menu only; at 4x both are 4x farther and 4x bigger, and look as they do at 1x. The radial menu’s zones follow as well: PopUpMenuView.LateUpdate hands PageView.UpdateCursor the pointer’s offset from the menu’s center in world meters, and that method cancels inside 0.07 m and outside 0.81 m and picks a segment out to 0.52 m, radii fixed in its code, so at 10x the menu drew ten times wider while the center-cancel and outside-cancel rings kept their standard size. The mod divides the offset by the menu’s drawn scale before the game reads it, for the radial page of your own menu only, so every ring sits on the menu as drawn (at 1x it changes nothing for an avatar with standard arms). After each swap the walking speed (twice the avatar’s speed stat) and acceleration (seven times its agility stat) that RemapRig.SetAvatar sets are checked against the worn avatar and set again if they are off, and the log line avatar scale: movement: lists the avatar’s height, mass and stats, the walk speed, acceleration, jump velocity (a rig setting the game never derives from the avatar) and gravity before and after, so a size that still feels wrong can be read from the log. Remote LabFusion players and [Player rig] NPCs are never scaled by the setting; LabFusion’s avatar sync already carries your avatar’s scale, mass and strength, so other players see the same size with no DH message. If full-body trackers were calibrated when the value changed, the kept calibration is re-fit to the re-worn avatar (with Keep calibration off, the normal calibration countdown starts again after the swap). Holsters and the body log stay stock. BONELAB’s mirrors follow the size too: a Mirror shows its own instance of the avatar’s crate, cached per barcode and never scaled, and Mirror.WriteTransforms places each of its humanoid bones at the world position of the body’s, so on a scaled body the bones landed a scaled distance apart on a 1x skin and the reflection stretched while your own view looked right. Before each write the reflection’s root takes the body’s scale (each axis keeps its own sign), for any rig a mirror shows, and the [Mirror] log says when it does.

Three things the game does not derive from the avatar follow the scale. The first two have switches under Avatars > Scale extras and in the preferences. Each is put on the live multiplier and back to stock at 1x. A value the game or a level sets again is taken as the new stock one, so nothing is scaled twice.

  • Jump (ScaleJump, on). BONELAB sets RemapRig.jumpVelocity (5) and doubleJumpPower once and never from the avatar, so at 5x you left the ground like a 1x avatar. A jump rises to v squared over 2g and gravity stays, so the launch speed is the stock one times the square root of the scale (a 4x avatar launches at 10 and rises 4x as high). The mod writes both on the local rig only, logging a jump velocity line when it changes them.
  • Shadow distance (ScaleShadowDistance, on; ScaleShadowMaxFactor, 8). While you are scaled up the URP asset’s shadowDistance is the stock distance times the scale, at most ScaleShadowMaxFactor times it, so a giant’s shadows reach as far as they look like they should. The shadow map stays the same size, so shadows get coarser per meter and the cost does not grow. It goes back to stock at 1x, with the setting off and at each level start. LOD bias and the camera’s far clip are not touched.
  • Menu pointer reach (always on). The menus are drawn and placed at the avatar’s arm scale, so at a large scale (seen at 100x) they stand farther from the pointer than the event cameras’ stock far clip, and Unity’s graphic raycaster skips anything past it: the options, main and DH menus stopped taking presses while the radial menu, which reads its cursor from a plane, kept working. The far clip of each menu event camera is the stock one times the scale (menu reach lines in the log).

The Marrow SDK gives an avatar ten AudioVarianceData sound slots (footstepsWalk, footstepsJog, highFallOntoFeet, bigEffort, smallPain, bigPain, dying, dead, recovery, and smallEffort, which nothing in the game reads), each a clip list the game picks from at random; HeadSFX voices the effort, pain, dying, death and recovery ones through the head’s own low-passed 3D source, and FootstepSFX the steps. DH fills those slots from the avatar’s dh.reaction components when the template is built: jumped lands in bigEffort, footstep with walk or run in footstepsWalk or footstepsJog (both when untiered), landedHard in highFallOntoFeet, damaged with small or big in smallPain or bigPain, dying, died and revived in dying, dead and recovery. The game then plays them exactly as it plays an SDK avatar’s. A dh.playSound on a head event still comes from the mouth here, since the game has one source per event; a dh.speakSound with mouth moves the avatar’s mouth while it plays (see Mouth).

Clips are the pallet’s stored audio files, decoded to PCM in managed code off the main thread and built into a clip with AudioClip.Create and SetData on the next frame. Three formats are read: wav (PCM 8, 16, 24 and 32-bit and 32 or 64-bit float, read by the mod itself), ogg Vorbis (NVorbis) and mp3 (NLayer). Any other extension, aiff included, fails that clip with a warning. Unity’s own loader cannot be used: BONELAB’s IL2CPP build stripped the DownloadHandlerAudioClip constructor that UnityWebRequestMultimedia.GetAudioClip needs, and run 38 failed every clip on it with Method not found. If AudioClip.Create or SetData turn out to be missing at runtime too, the first clip logs one error saying so and every DH sound slot stays empty, with no warning per clip. An mp3 cut from a longer file can open on a frame whose bit reservoir points before the stream began; NLayer drops that frame, and the mod restores it as leading silence, which is what other decoders play, so the clip keeps its full length. Loads are asynchronous, so a slot is filled as its clips arrive, and after every SwitchAvatar the mod writes the same clips onto the rig’s HeadSFX arrays and FootstepSFX overrides, logging how many the game had copied from each slot: a count below the DH total on the first run means the game reads the slots before DH fills them, and the write is what plays. The pallet’s platforms/bonelab/<avatar>.jsonc may carry patches of its own, layered after the avatar’s chain.

The game’s own vocal hooks (HeadSFX.JumpEffort, SmallDamageVocal, BigDamageVocal, DyingVocal, DeathVocal, RecoveryVocal, and FootstepSFX.PlayStep for the first three steps per rig) are reported back as the DH event they stand for, which is how the player’s face closes on DeathVocal and opens on RecoveryVocal. A [Player rig] NPC that DH kills has its rig’s HeadSFX.DeathVocal called, so its cry goes through the same source; a [Ford] or [DH rig] NPC is no rig, so its death and respawn, and each hit that hurts it, play the reaction as a one-shot at its head (speak, which its mouth follows) or hips or the named bone (play). A hit is read from the NPC health’s TakeDamage and plays damaged with the small tier, or big when it took NpcBigHitFraction of the NPC’s hit points at once; one cry per NpcHurtCooldown, none for a killing blow (the death plays instead). NpcReactions turns these cries off for NPCs, and the Sounds preference turns all of it off. A [Player rig] NPC’s hits are not voiced yet, and no NPC plays the avatar’s footsteps or landings yet.

Log lines to read on the next run, all under [Sound]:

  • '<barcode>': N reaction(s) (...), M from the platform file, K SDK slot(s) [...], C clip(s), P still loading at template build, or no reactions; the SDK sound slots stay empty.
  • loading '<pallet:path>' (<bytes> bytes, <.ext>), decoding off the main thread, then loaded '<pallet:path>': <seconds> s, <ch> ch, <Hz> Hz (<decoder>) with the decoded numbers and wav, NVorbis or NLayer, or clip '<pallet:path>' <why>; the reactions naming it play without it. A warning follows loaded only if Unity’s clip disagrees with the decoded length, channels or rate.
  • cannot be built: this BONELAB build has no AudioClip.Create/SetData (...) once, as an error, if the clip API is missing at runtime.
  • '<barcode>': slot <Slot> filled with N clip(s) [...] (<reason>).
  • '<barcode>' on rig '<rig>' after SwitchAvatar: HeadSFX=present FootstepSFX=present; [<Slot>: game copied X of Y; ...]; DH wrote [...].
  • rig '<rig>' voiced <event>[/tier] through HeadSFX (mouth source at <position>, spatialBlend <n>); DH clips for it: <declared/loaded> whenever the game fires a vocal, and FootstepSFX on '<name>' PlayStep velocitySqr=<v>: walkOverride=<n> clip(s), runOverride=<n> clip(s) for the first steps.

The game blinks no player avatar. It does turn the eye bones: ArtRig.ArtOutputUpdate aims the art rig’s eye proxies along Rig.eyeGaze, and ArtOutputLateUpdate copies them onto the avatar’s LeftEye and RightEye. That gaze comes from the headset’s eye tracking fields, which nothing fills on PC, so it holds straight ahead at 2 m. Blinking and eyelid shapes exist only in the Realistic Eye Movements package some NPCs use. DH avatars therefore run the shared DigitalHeavenEyeLook, the same eye brain the engine runs, on three kinds of clone, as the springs do.

  • The body. After every RigManager.SwitchAvatar to a DH body, the eye look is attached to the clone and steps in the ArtRig.ArtOutputLateUpdate postfix, after the springs. It writes over the game’s gaze within the rig’s eyeRotationLimits, and writes the rig’s eyelids blink, lookUp and lookDown shapes. A rig that names no mesh blinks on the skinned mesh carrying the most of those shapes, as the engine does. It looks at the other rigs’ heads (the midpoint of the art rig’s eye proxies, facing where the headset faces), so a Fusion player’s DH avatar looks at you, at every spawned NPC with an AI brain (its brain’s eye transform, noted from the Poolee.OnSpawn hook the NPCs share and dropped once the NPC is back in its pool), and at its own reflection in any Mirror. The reflection’s face is offered facing along your view mirrored between the two heads, so the eyes meet their reflection’s when you look it in the eye.
  • Mirror reflections. After Mirror.WriteTransforms, the reflection takes the body’s eye rotations and eyelid weights (FaceCopy). A second eye brain would look and blink on its own schedule.
  • The avatar menu preview. Nothing poses it, so its eyes step themselves and wander.
  • NPC bodies. A [Ford] or [DH rig] NPC gets the same eye look, stepped in its puppet’s PuppetMaster.OnPostLateUpdate right after its springs, looking at the same heads. A [Ford] body’s bones ride the base’s rig, so the template no longer maps onto a spawn: the eye look is resolved once on the build before its bones move, and every spawn copies it (DigitalHeavenEyeLooks.AttachLike). A [DH rig] body is mapped from the template like a player’s. A [Player rig] NPC is a rig and is bound as one.
  • Death. When an NPC’s puppet goes dead (or a [Player rig] NPC dies), its face gets the Died game event, whose default is a closed face: blink shapes held at full weight, eyes at rest, no blinks, saccades or look. A pooled respawn reports Revived and the eyes run again. The default is written once, in Core’s AvatarGameEventDefaults, the place a later per-platform override (a creator’s own blend shape, blinking left on) will replace; an avatar with no blink shapes keeps its lids as they are. The log says [Face] '…' as NPC '…': Died; face closed.

The paper doll’s posed copy takes the body’s eyelid weights too; its eye bones ride in its pose copy.

An avatar’s own dh.eyes wins over the global switches, exactly as in the engine: look.enabled: false leaves its eyes alone and blink.enabled: false leaves its lids alone, even when the rig names eye bones and blink shapes, and the log says [Face] '…': eye look disabled by the avatar or blinks disabled by the avatar. An avatar with both off, such as pitr’s Mayu, gets no eye look at all. What each avatar resolved is one [Face] line: the eye bones, or why they stay still, and the blink shapes with their mesh, with near names for any the mesh lacks. Every 10 s each body logs its steps, blinks and saccades, what it is looking at, and how many heads were offered. Every setting is a MelonPreferences entry under [DigitalHeavenAnim] (see Eye settings).

Nothing in the stock game moves a mouth or a jaw, and LabFusion only tilts the humanoid Jaw bone with a voice’s loudness, while in a server. DH avatars run Core’s mouth pipeline instead: once per avatar the rig and its face mesh’s blendshapes resolve to a plan (MouthRig), each avatar gets one MouthBrain and one analyzer, and every frame the brain’s weights are written onto the plan’s shapes, owning them while the voice is heard. It runs on the same bodies as the eyes (the player, other players’ DH avatars under LabFusion, [Player rig] NPCs, and [Ford] and [DH rig] NPCs, whose mouth is resolved on the build and copied to each spawn), steps just before them, and tells the eyes when the avatar speaks. Mirror reflections and the paper doll’s posed copy take the body’s jaw and mouth weights (FaceCopy).

Where the voice comes from, per avatar:

SourceStatusWhat it does
dh.speakSound clips with mouth🟡 untestedA rig’s vocal plays through its HeadSFX mouth source; while that source plays a DH clip, the clip’s decoded samples are fed to the analyzer in step with the source’s playback time. An NPC body’s one-shot is followed on its own clock. Works with or without LabFusion, on every DH body.
Your voice in a LabFusion server🟡 untestedLabFusion’s own capture, never a second one: its samples, taken after UnityVoiceReceiver.SendToSources, else its VoiceInfo.VoiceAmplitude as loudness. Only for an avatar with visemes. A lobby you host alone counts as a server.
Other players’ voices in a LabFusion server🟡 untestedEach packet LabFusion receives (UnityVoiceSpeaker.OnVoiceDataReceived) is decoded a second time for that player’s mouth, else its VoiceSource.Amplitude is used as loudness. Only for an avatar with visemes, and only while VisemesForRemotePlayers is on.
Your microphone in single player🟡 untestedOff unless VoiceMouthFromMicrophone is on. Unity’s Microphone, on LabFusion’s input device when one is set, else the system default. Released the moment a LabFusion server starts or is joined, since LabFusion never starts a device that is already recording.
Steam voice, Meta voice, OVRLipSync❌Stripped, store-bound or discontinued; see the visemes design note.

LabFusion’s jaw flap. For an avatar whose plan has visemes, DH shapes the mouth from the same voice LabFusion flaps the jaw with. LabFusion’s copy of its flap onto that avatar (ArtRigPatches.ArtOutputLateUpdate) is skipped through a Harmony prefix, and DH holds the jaw at rest. For an avatar without visemes in a server, DH runs nothing for the voice and LabFusion flaps the jaw. LabFusion turns whatever the avatar’s humanoid Animator returns for Jaw, which on a DH avatar is the rig’s mapped Jaw bone, so an avatar flaps natively once its rig maps one; the log names that bone per avatar. DH’s face step runs last among the ArtOutputLateUpdate postfixes, after LabFusion’s.

The Jaw bone. mouthJawBone stays on in BONELAB. The stock game never writes the Jaw bone, so a jaw DH turns has nothing to fight, and the brain opens it only for an avatar with no visemes and no mouth-open shape. MegaBonk and VRChat gape because their own animation also drives a mapped jaw. Turn it off if a jaw-only avatar’s mouth hangs open.

Analysis. One analyzer per avatar, made in one place. MouthAnalysis Auto reads shapes (uLipSync’s MFCC) for an avatar with visemes and loudness alone for one without. A voice known only by its amplitude always uses loudness. A voice that sends nothing for MouthVoiceGapSeconds counts as silent, so the mouth closes between LabFusion’s packets.

Sources meet in one place. Every frame the live voice is drained, whether or not a dh.speakSound clip plays. While a clip plays it has the mouth and the voice’s samples are dropped, so the microphone stays open under a clip instead of being released and reopened around it. Your own microphone (DH’s capture or LabFusion’s) runs through Core’s auto level first, which keeps measuring under a clip.

Log lines to read on the next run, all under [Mouth] and shown with LogMouth at Normal (the microphone and server lines at every level, the trace only at Verbose):

  • LabFusion: server state read; local amplitude read; ...; local voice samples hooked; remote voice samples hooked; its jaw flap can be kept off DH avatars with visemes; 3 of 3 server events subscribed once at start, or LabFusion is not loaded. Anything NOT hooked names its fallback.
  • N mouth and M MFCC settings under [DigitalHeavenAnim]: mouthEnabled=… mouthJawBone=… (BONELAB default on: …) once.
  • '<barcode>' on rig '<rig>': <plan>; shapes from <mesh>; jaw '<bone>'; LabFusion's jaw flap would turn its humanoid Jaw '<bone>' per swap, or nothing to move: …. NPCs log NPC <label>: … the same way.
  • '<barcode>' on rig '<rig>': source: <voice and LabFusion's part> on the first step, and now source: … whenever the server state or a setting changes it; then analysis Shapes (MFCC) or analysis Volume.
  • speaking '<clip>' from its HeadSFX mouth source, or once the game voiced a vocal, but its HeadSFX mouth source plays no clip DH can follow if the game plays vocals some other way.
  • microphone (system default) opened at 48000 Hz … and microphone … released: <reason>; LabFusion server on: … and LabFusion server off: single player.
  • With LogMouth at Verbose, every MouthTraceSeconds (0.25 s) while a mouth hears your microphone or moves: from <source>; input -43.2 dBFS, floor -61.0 dBFS, gate open, gain +18.0 dB; loudness max 0.081; top viseme aa 0.42, peak consonant SS 0.80; shape weight sum mean 0.31 max 0.55; jaw max 0.00; speaking 80% of 22 steps. The input, floor, gate and gain appear only for your own microphone; the input is before the gain, the loudness after it.
  • Warnings only when something is off: the local mouth follows LabFusion's amplitude from now on: … if the samples hook never fires, and its jaw was N deg off rest despite the switch on LabFusion's flap once per avatar if the flap gets through.

The mod can render PNGs of avatars, so a build can be checked without a headset. Every file goes under the workspace cache directory in bonelab-captures, and each one is logged under [Capture] with its coverage and the mean color of the subject.

The automatic captures are off by default. DebugCaptures (the Debug captures row) is the master switch, and each kind keeps its own entry, all on, which decides what the master turns on. One gate (DebugCaptures.Allows) is asked by every kind. Run NPC test turns the NPC captures on for its own run, whatever the switches say. The capture hotkey and the thumbstick chord are asked for, so they work either way.

KindEntryDefault with DebugCaptures onWhen
BuildCaptureOnBuildonEach DH avatar when it is built, and the reference avatar once a session
SwapCaptureAfterSwapSeconds4 sThe live rig, mirrors and menu preview after a swap
NPCNpcCapturesonA DH NPC crate’s first spawn of a session as a series, and any DH NPC after it dies (see NPCs)
Settings pageMenuCapturesonThe DigitalHeaven page of the preferences menu when it is opened or turned, at most six a session
  • On build: the frame after a DH avatar is built, its template is rendered offscreen in its bind pose (T-pose). The shots are the body from the front and the side, and each hand from the palm, the back and the thumb side. They go in bonelab-captures\<Marrow barcode>\build-*.png, next to build-materials.txt, which lists every renderer’s probe settings and every material’s shader, colors, flags, textures and keywords. The first build of a session also captures one stock avatar the same way, Ford by default, as a brightness and material reference.
  • Studio lighting: offscreen shots move the avatar to an unused layer, turn its light and reflection probes off, and light it with a key light just above and beside the camera and a flat ambient probe. A DH body and a stock one are therefore lit the same way in any scene.
  • Live: a few seconds after a successful swap, on a hotkey (F9 by default, read from the keyboard whether or not the game window has focus), and when both thumbsticks are clicked in and held for a second, the mod captures the player’s rig, every mirror reflection and the avatar menu’s preview body, in the scene’s own lighting and probes. The subject’s renderers move to the unused layer for the shots, so neither the scene nor a held item is in the picture, and each hand’s clip planes hug the hand, so a sleeve or the body between it and the camera is cut away. Body shots are framed about the chest, level, from the body’s own forward, and cover every skinning bone, tails and ears included. These shots show the game’s own posing, fingers included. They go in bonelab-captures\live\<time>-<reason>\.

Nothing is captured or loaded while the game is busy. Captures, and the reference avatar’s load, wait until the rig has swapped to its first avatar and no swap, build or scene load has happened for CaptureSettleSeconds. Run 9 died natively at boot while the saved DH avatar loaded: the reference load had been started from inside the game’s SwapAvatarCrate, and a capture was queued behind it. Every callback the game calls from native code catches its own exceptions, because one that escapes into IL2CPP frames takes the process down.

Hands are not posed offscreen. The game poses fingers in its art rig, and a humanoid muscle pose would show Unity’s retargeting instead.

The paper doll is stage 1 of a personal mirror: your avatar seen by a camera in front of you, shown on a picture fixed to your headset view and in a corner of the desktop window. Turn it on with PaperDoll, the overlay’s Paper doll toggle, or the DollHotkey (F8 by default, read without the window’s focus). Its setting names and defaults follow the Minecraft mod’s paper doll.

  • Camera. It stands DollDistance from your chest (times your avatar’s scale, as do DollHeight and the clip planes, so a scaled avatar and the items at its hands stay in the frame; the posed copy takes the body’s size too, where it once stood at 1x while the held items stood at the scaled hands), DollAngle degrees round from straight ahead, level at DollHeight above the chest, with a DollFov vertical field of view. It follows the body’s yaw only: the anchor (BodyAnchor, shared with the captures) takes the chest’s position and the shoulder line flattened onto the avatar’s up, so leaning or looking around never tips it. It renders right after the game writes the body’s art pose, at most DollRate times a second, into a 3:4 picture DollResolution times UiRenderScale pixels tall, with the scene’s own lighting and no shadows or post-processing.
  • Subject. By default it is your live body, moved onto the capture layer for that one render. With DollPosedCopy it is a copy of the avatar’s own prefab (a DH template, or a stock avatar crate’s loaded asset), posed from your body’s skin bones every frame through the shared PoseCopy. The copy keeps the head and hair the game hides in first person. It is on the capture layer with its renderers forced off except for its own render, so no game camera sees it. Run 18 measured posing a Taidum copy at 0.25 ms a frame (280 bones) and making it at 6.5 ms.
  • Headset. A quad under the headset transform, HudDistance ahead, HudOffsetX toward HudSide (Left, Center or Right; the side and offset to the side rows) and HudOffsetY up (the height row), HudSize tall (the Headset paper doll size row) at HudOpacity. With HudFacesEyes on (the default) a picture off the view center is turned to face your eyes rather than straight ahead. It is drawn in front of the scene (UI/Default, which the game ships). It is switched on only while the headset camera renders and off again afterwards, so the desktop spectator camera never draws it. In Passthrough spectator mode, RigScreenOptions.SetSpectatorCamera turns the spectator camera’s object off and hands the desktop the eye picture, which would show the quad. So when Passthrough is on and no separate spectator camera renders, the headset picture is hidden. Run 18 found a spectator camera rendering while the mode read Passthrough, which keeps the picture in the headset.
  • Desktop. DollSize pixels tall in the top left corner (DollRight for the top right), drawn before the overlay. IMGUI writes its output raw, and the picture samples back to linear light, so it is drawn with sRGB writes on and matches the headset picture.
  • Mirror image. DollMirrorImage flips both pictures left to right, as a mirror would; off shows the camera’s own view.
  • Held items. With DollHeldItems, what each hand holds and each holster carries is moved onto the capture layer for the render too (DollItems). The items stand in the world where the body holds them, and the posed copy stands exactly on the body, so they appear with either subject. A grip on your own body adds nothing; a grip on another LabFusion player’s body shows that player’s avatar meshes, fading and lingering like any held item (their rig is not walked: a networked body would read as a fixture), with a warning once per player whose avatar cannot be drawn. The log lists the items in the picture whenever they change.
    • What a grab shows. Not the gripped entity, which for a locker door is the whole bank of lockers, but everything joined to the gripped body. The walk follows MarrowJoints and raw Joints from either end, so a part whose joint names the held part is found as surely as one the held part names; run 40’s scene skeleton showed only the bone in hand while the walk followed each body’s own joints forward only. It knows a joint from each body’s own MarrowJoint lists and from every joint under the places it searches: the entity root of each body it reaches, and that root’s parent, where the parts of a thing built from entities side by side keep the joints that name the part held. It follows joints made at runtime too, since a skeleton’s parts are joined by them: in run 41 every link of a held chest or skull was one, and skipping them showed only the bone in hand. It skips only a blade’s stab or slash joint (one a StabSlash on either body’s entity lists), so a knife in a skeleton is not one thing with it. The hand’s own grip joint leads to the player’s rig, which the walk never enters, and a joint made at runtime to nothing (a stab into a wall) never makes a fixture. If it reaches a body fixed to the world (no rigidbody, a kinematic one, or an authored joint to nothing that holds anything), the thing is a fixture, such as a locker door, a door, a handle, a lever, a ladder or a zip line, and is not shown. Otherwise every renderer of every entity in the group is shown, so a skeleton, a crablet, a ragdoll and an NPC show whole whichever part you grab. A static grip (a world grip, a static host, or a grip with no rigidbody) is a fixture outright. A group larger than DollItemMaxSize across is not shown. Anything that fits a holster (a WeaponSlot) is always shown. Each grab is decided once, so a door ripped off its hinges shows on the next grab. A held group of several entities (a skeleton) is walked again every DollGroupRecheckSeconds (0.25 s) while it is held: in run 42 a skeleton pulled apart kept every piece in the picture, including one dropped on the ground, until the last was let go. A part no longer joined to what the hand holds now becomes an item of its own, which lingers and fades out like a dropped item ('<part>' came apart from '<held>' (n renderer(s)): it lingers 2s). Every decision is logged once per thing (held '<name>': shown … or not shown: a fixture ('<body>' is kinematic)), with the bodies, entities and renderers found and what the walk searched (… joint(s) under … place(s), … made at runtime ('<body>'-'<body>', …), … blade joint(s) skipped, and its time), at most twelve every five seconds. Holstered items are their whole entity, as before.

    • Fading. An item fades in and out over DollFadeSeconds (0 shows and hides it at once), in the style DollFadeStyle names; the next fade uses a changed style. dissolve, the default, draws the item with the game’s own SLZ/Hologram Depth fade shader (OpaqueHologram.shader, its blue-noise dissolve) carrying each material’s base, normal, MAS and emission maps (a _MainTex stands in for a base map), whatever shader the item uses. Which of its properties dissolves it is not documented, so a dissolve fade measures it once per session: the item’s mesh renderers are drawn alone, framed tightly on their mesh bounds from whichever of their own axes shows the most of them, into a small picture shaped to the item, with each number property at both ends of its range, _AlphaCutoff and _DepthFader first, and the one whose ends change its coverage most is used. Renderers that are not meshes, overlays, anything over DollItemMaxSize and anything that far from the rest are left out of that picture (a weapon carries renderers tens of meters across). The measurement is only trusted when the item’s own materials cover at least 10% of the picture; below that nothing is concluded and a later fade tries again. Every try is logged. alpha draws a copy of each LitMAS material made transparent the way DH transparent materials are; an item with an opaque material on any other shader (the barrel’s URP Lit, a shotgun’s, a magazine’s cartridges) cannot alpha fade and appears and goes at once. Runs 41 and 42 faded those through a transparent copy of the LitMAS reference carrying only their main texture and color, which looked nothing like them and went dark through every fade. The doll’s picture is cleared to transparent black and shown with straight alpha, so an ordinary alpha blend darkens a fading item toward black: its color is weighted by its alpha twice and the picture’s alpha gets the square of it (run 41’s barrel had a blacker tint during every fade). So each fading material draws three times, keyed on the picture’s own alpha: a depth pass that keeps the rest to the item’s nearest surface; a blend over whatever is already drawn (the body, or the world mirror screen’s opaque backdrop) with its color weighted by the fade, which touches nothing where the picture is empty; and a fill of the empty picture with the item’s unweighted color and the fade as its alpha, which touches nothing already drawn. The extra passes are materials appended after the renderer’s own, which Unity draws on the mesh’s last submesh only, so an item with a fading material on any other submesh, or with no material that can fade, cannot fade: it appears and goes at once, and its comes in/goes out line says at once, since it cannot fade: …. Renderers that are not meshes (particles, lines) are drawn as they are. Overlays (SLZ/Highlighter, SLZ/Icon Billboard) and materials already see-through are drawn as they are, in both styles. Each fade logs the route every material of the item took. A dissolve that cannot be had (the shader is not found, or no property dissolves the item) falls back to alpha, and the log says why. The stand-in materials are swapped onto the item’s renderers only inside the doll’s own render, together with the layer move, so the world never sees them. They are made once per source material and destroyed after five seconds unused.

    • Which items can fade. Decided from the item’s renderers and materials when a fade starts, and logged on its comes in/goes out line. While the dissolve works (it has measured a property that dissolves), every item can fade. Otherwise, under the alpha fade:

      • A renderer that only casts shadows draws nothing in the picture and is ignored (run 43’s torch failed on its DitheredShadowCaster).
      • Materials drawn as they are do not count either way: overlays (SLZ/Highlighter, SLZ/Icon Billboard), materials already see-through (queue over 2500), and anything on a renderer that is not a mesh (particles, lines).
      • Every other material must be LitMAS (SLZ/LitMAS/*, anything with _BaseColor and LitMAS’s blend properties). One opaque material on another shader (URP Lit on the barrel, a shotgun or cartridges, SLZ/Special/Textured Shadow) and the item cannot fade.
      • Each fading material must sit on its mesh’s last submesh, where the extra passes can reach it (run 43: the spawn gun, the Nimbus gun and the lore clipboard each have a fading material on submesh 0 of 2).
      • It must have at least one material that fades.

      An item that cannot fade pops: it appears at once and goes at once, with the reason (at once, since it cannot fade: '<renderer>' draws '<material>' with '<shader>', not LitMAS).

    • DollShowUnfadable (on by default; Paper doll items that cannot fade in the overlay’s Config window and the in-VR page). Off, an item that cannot fade by the rules above is never drawn: held, holstered, joined to your body, lingering or a fragment, whatever its size and even when it fits a holster. One line says so per thing: '<item>' is not drawn: it cannot fade (…) and DollShowUnfadable is off. Turning it off removes such an item already in the picture at once.

    • Joined to your body. Something tied to the player’s body with no hand holding it, such as the Descent level’s noose (NooseBonelabIntro, which ties the knot to the player at runtime and holds it until the dagger cuts it), is a third source besides the hands and the holsters. Every DollAttachedScanSeconds (1 s) the doll looks at every joint in the scene and at the player’s own Marrow joints: each one with one end on the player’s rig, not on a hand (a hand’s grip is already a held item), and the other end off it starts a walk at that far end. Such a walk is trimmed rather than refused at a fixture: a kinematic or bodiless body, or one with a joint to the world, is left out and not walked through, so the noose shows without the gallows beam it hangs from (DYNAMICGALLOWS, jointed to the world). Run 44’s walk only ignored that joint and kept the beam, so the noose’s whole GALLOWS entity (7.4 m) was over the size limit. An entity the walk reached only in part is drawn body by body, plus every skinned mesh of it whose bones lie mostly on the reached bodies, which is how the rope’s mesh, hanging beside its chain of bodies, is found. A hand’s grip joint is found by the same scan, so a held noose shows this way too (the hand’s own walk calls it a fixture). The size limit, DollShowUnfadable and the fade rules still apply, and a thing already shown as held or holstered is not shown twice. A seat or vehicle joined to the body shows the same way when it fits DollItemMaxSize. The first time each one is found it logs the joint, which body of the player’s it holds and the renderer types drawn: '<body>' is joined to your body: <JointType> on '<body>' holds your '<player body>'; 3 MeshRenderer, 1 SkinnedMeshRenderer; …, trimmed at n fixed body(ies) (…) and m joint(s) to the world. The held-items line names it as (joined to your '<player body>', n renderer(s)).

    • Lingering. With DollLinger on, an item you let go of (it was in a hand or joined to your body and is now in neither, nor in a holster, so holstering is not letting go) stays in the picture for DollLingerSeconds, live, falling or flying away, then fades out. A hand’s grip joint is also found by the scan for things joined to your body, so letting go rescans at once: a scan from before the release kept the item “joined” for up to DollAttachedScanSeconds, after which it left as no longer carried, at once with no linger (run 44’s torch). It stops at once if it despawns or is made inactive, or becomes part of something held (a magazine seated in a held gun).

    • Breaking. When a shown item (held, holstered or lingering) breaks, BONELAB’s ObjectDestructible.OnStatDestruction says so, and the item leaves the picture at once rather than lingering or fading as a ghost. Its break is remembered: where its bounds were centered and when. Every pooled spawn (the mod’s one Poolee.OnSpawn hook) with a rigidbody that appears within DollFragmentWindow seconds of the break and within DollFragmentRadius times the item’s size of its center is one of its fragments, and so is any of its CustomFragmentObjects that becomes active in that window. A fragment is shown whole at once, with no fade in, lingers for DollLingerSeconds as a dropped item does, then fades out; it ends at once if it despawns. A spawn in the same frame just before the break is announced still counts, and a break effect with no rigidbody (dust, smoke) does not. Grabbing a fragment makes it an ordinary held item. The log has broke, each fragment taken in (fragment '<name>' spawned <n>ms after '<item>' broke, <d> m from it), each spawn beside it that is not a fragment and why, each fragment’s end, and the break of '<item>' is over with the count.

Held-item optionBONELABMinecraft
Joined-group rule and fixture filter✅❌
DollItemMaxSize✅❌
DollFadeSeconds, DollFadeStyle✅❔
DollLinger, DollLingerSeconds✅❔
Breakable fragments (DollFragmentWindow, DollFragmentRadius)✅❌
Parts that come apart leave (DollGroupRecheckSeconds)✅❌
Hide items that cannot fade (DollShowUnfadable)✅❌
Things joined to your body (DollAttachedScanSeconds)✅❌

Minecraft’s paper doll draws the vanilla item in the avatar’s hand, and a hand there never holds part of the world.

While FbtDebugDraw is on, the paper doll draws the same tracker markers and lines in its picture, so they show on the desktop corner and in the headset quad alike. They are the live markers (FbtDebugDraw keeps one set), moved onto the capture layer for the doll’s render only, so nothing is duplicated and nothing bleeds into the world view. The world mirror screen does not draw them. [PaperDoll] says paper doll shows N FBT markers once, and an error when the markers cannot be shown (FbtTrackers off, no unlit shader, drawing threw).

Every 120 pictures the log gives the average render and pose-copy times under [PaperDoll], with LogPaperDoll at Verbose. The subject is shared with the world mirror screen (DollSubject): it is posed once a frame however many views draw it, and a posed copy is let go two seconds after the last view stops drawing it.

Two prototypes to compare, chosen with WorldMirror (the overlay’s and the in-VR page’s World mirror row). Choosing one places it WorldMirrorDistance in front of you, facing you; WorldMirrorHotkey (F7) places it again where you look. A level load removes it and sets WorldMirror back to Off, so every level starts without one; it is placed only when chosen again, like taking out an item.

  • Game mirror. A copy of BONELAB’s own Mirror, which stands a mirrored copy of your avatar behind its plane (and so shows spring bones through the same Mirror.WriteTransforms copy as the level’s mirrors). The game has no mirror to spawn, so the first level with one (the Hub) lends it: the smallest part of that level holding the mirror and every transform it names is copied under an inactive holder for the session. Its side the players stood on is turned to face you, at the height it stood above its own floor. Until a level has lent one, choosing it logs so and lists any spawnable crate named like a mirror. The community BetterMirrors mod works on the copy as on any game mirror.
  • Screen. The paper doll’s subject on a standing 3:4 screen WorldScreenHeight tall, seen from the screen by a camera aimed at your chest, on a dark background. DollPosedCopy and DollMirrorImage apply to it as to the paper doll.

The game’s preferences menu gets a DigitalHeaven button in its OPTIONS grid, the way LabFusion adds its own: a copy of the Control button, and a page appended to PreferencesPanelView.pages, which PAGESELECT switches between. The page is a copy of the OPTIONS page that keeps its grid’s buttons where the menu placed them, each showing one entry on one line under the stock icon, shrunk to fit its button. The DigitalHeaven button carries the DH cloud in the stock icon frame (Assets/Icons/bonelab). It shows the same settings as the overlay’s Config window, from one list (BonelabSettings), as a tree of pages: each row names the pages it sits under, and the page builds the tree from them (MenuTree), so a setting is defined once. It is still one game page; its cells show whichever sub-page is open.

  • What a cell does is decided by its row’s kind alone (MenuControl), never per row: a sub-page opens (Name >), an on/off setting flips in place (Name: On), a choice or a number opens a picker (Name: value >), a page of its values with the current one marked (now) and the default (default), which goes back by itself after a pick, and an action runs or stops (Calibrate, Run NPC test). Numbers are pickers over fixed steps rather than cycling, since a single cell cannot show the steps it cycles through.
  • The title is the path, for example DIGITALHEAVEN / PAPER DOLL, the parents dimmed (TMP rich text).
  • The return arrow goes up one level, out of a picker first, and from the first page back to OPTIONS.
  • Each page’s cell shows its own stock icon, from one table, MenuRows.PageIcons: Avatars Body, Face Eye, Mouth Audio, Paper doll Photo, World mirror Display, NPCs AI, Maps World, Full-body tracking Sprint, Multiplayer Arena (under Actions), Advanced Experimental and Cheats Sandbox. A setting’s cell shows the Options gear, and the cell that turns a long page shows Start. Icons are named by the code on the labeled sheet of stock icons (External/Decompiled/Bonelab/menu-icons, git-ignored). The game loads few of them as sprites of their own, so the rest are cut at runtime from the two atlases the OPTIONS page always has loaded, found through its Body and Control icons (MenuIconAtlas, measured offline). No game art ships with the mod.
  • The whole tree can be drawn as one picture without the game: bun Platforms/DigitalHeaven.Mods.Bonelab/tools/menu-map/menu-map.ts (add --hints for every row’s hint) builds the mod’s tests, exports the tree from MenuRows and writes tools/menu-map/out/menu-map.png, with the real icons where the sheet folder exists and safety switches marked.
  • A safety switch (a row marked .Watched() in MenuRows, one that when set away from its default silently turns off or degrades something another player or the game relies on: the spring switches, every LabFusion row, Align finger frames, the head fit rows, the NPC reactions, the map rows, the tracking rows that keep the body following the trackers, the game shader, the cheats and the Safety rows; never a cosmetic preference, a log level or a capture toggle) set away from its cfg default is shown in yellow, and every page cell above it, up to the DigitalHeaven button on OPTIONS, is yellow with the count beneath it, for example Avatars (2) >. A forgotten switch can be traced down from the menu’s first page. The labels refresh while the page is open and on every press.
  • The open page is kept for the run of the game, so the DigitalHeaven button reopens it after a level load; a fresh launch opens the first page. Reopening the whole menu still opens on OPTIONS, which is the game’s own default page.
  • A line under the cells shows the one-line hint of the cell pointed at: every row, every picker value and every page has one of its own (MenuRows, at most 80 characters, checked by the mod’s tests), apart from the longer comment the preference carries in the cfg file.
  • A page holds at most what the grid fits (nine cells). A page over that turns with a More cell, and a warning names it, since the tree is meant to fit; the tests hold every authored page to the nine. Developer knobs (captures, test bodies, map internals, log levels) sit under Advanced, the last page; the body preview’s mesh quality, the blend shape import and the page’s own switch are cfg-only.
  • BoneLib rebuilds the page list as a fixed twelve when it adds BoneMenu, which cuts off a page appended earlier. The page is looked for in the list on every check and put back, with the button following its new index, and a warning says so.
DIGITALHEAVEN
├─ Actions: Teleport to spawn, Teleport to player >, Kill myself, Clear spawned, Multiplayer >
│ └─ Multiplayer: Share my LabFusion server on LAN, Colliders on other players, Other players' footsteps and hits, Knocked-out players close their eyes, Other players' voices, Wounds from other players' shots, Leg mode for other players
├─ Avatars: Spring bones >, Align finger frames, Extra thumb roll, Avatar scale, Scale extras >, Snout and skull colliders on my rig
│ └─ Spring bones: Spring bones, Touch and grab springs, Others may touch mine, I may touch others', Spring bones touch the level
├─ Face: Eye look, Blinks, Blink rate, Mouth >
│ └─ Mouth: Moves to voices, my microphone, microphone auto level, how it reads a voice, least opening while voiced, consonant guesses, jaw bone
├─ Paper doll: Paper doll, In the headset, On the desktop
│ ├─ Headset: Size, Opacity, Side, Offset to the side, Height
│ ├─ Desktop and camera: Size, On the right, Camera distance, Camera angle
│ ├─ Look: Mirror image, Posed copy, Held items
│ └─ Held items: Size limit, Fade, Fade style, Items that cannot fade, Linger, Linger time
├─ World mirror: Mirror, Distance, Screen height
├─ NPCs: Hurt and death sounds, Wounds, NPC springs skip the level far away, Level contact distance, NPC springs stop far away or out of view, Spring stop distance
├─ Maps: In the level select, Level select tab, Put me back at the spawn when I fall out, Grabbable walls, Bake a navmesh, Carry my inventory
├─ Full-body tracking: Trackers, Calibrate, Clear calibration, Keep calibration, Hips, Locomotion animation, Leg mode
│ └─ Debug and takes: Debug markers, Mirror follows, Capture tracker snapshot, Record movement
└─ Advanced
├─ Game shader
├─ Menu clicks through walls
├─ Captures: Debug captures, Avatars when built, Live capture after a swap, NPCs, Settings page
├─ Cheats: Unlock every spawnable, Invincible
├─ Safety: Start at 1x after a risky size, Keep this size? countdown, Avatar scale on the radial menu, Get me out of death loops
├─ Tracking: Hips rotation, Yaw only
├─ NPCs: [DH rig] NPCs, [DH anim] NPCs, [Player rig] NPCs, Run NPC test
├─ Maps: Drop that counts as out, Spawn lift, Stock level under the map, That stock level, Physics layer, Solid mesh colliders
└─ Logs
├─ Avatar logs: avatar builds, springs, eyes, mouths, sounds
└─ Other logs: NPCs, paper doll, captures, maps, full-body tracking, overlay and menus

Every new player rig brings a new menu, which is injected once; PreferencesPage turns it off from the next level. Not yet in the page: a live doll preview, a corner grid for the doll’s placement and steppers for numbers.

Actions. The first page holds things to do rather than settings (PlayerActions, PlayerActionsPlan). Its Multiplayer sub-page keeps the LabFusion rows, which moved down a level so the first page stays at nine cells.

  • Teleport to spawn. In a DH map it puts the rig back at the map’s spawn and facing (MapLevel.TryGetSpawn), through RigManager.Teleport with velocities zeroed, as the kill floor does. In a stock level it calls the game’s own Health.TeleportToCheckpoint, which is the level’s start until a checkpoint is reached.
  • Teleport to player. A picker over the other players of your LabFusion lobby, by name (two with one name are numbered), read each time it opens from FusionPlayers.Lobby with LabFusion’s small id. A press puts you 1.2 m to the right of the player, 10 cm up and facing them. With no lobby the row reads Not in a lobby, and with no one else No one else here. The overlay’s Config window leaves this row out, since its dropdown takes its values once.
  • Kill myself. The first press arms it (Press again to die) and a second within 3 seconds calls Player_Health.Death, so the game’s own respawn follows. The Invincible cheat is lifted for the call (the health mode is Mortal and Invincibility.BeforeDeath lets it through) and the mode is put back after.
  • LabFusion carries your teleport and your death through its ordinary rig and health sync; the mod sends nothing extra. The overlay’s menu bar has Teleport to spawn and Kill myself entries too.

The page’s last row, Run NPC test, runs an unattended NPC sequence (NpcTestSequence) so nobody has to hold a spawn gun for it: it spawns the player’s current DH avatar from its friendly variant on Ford on the floor NpcTestDistance ahead of the player’s eyes, facing the player, in every offered body mode in turn ([Ford] first, then the development bodies that are turned on, in the mod’s order), leaves each standing for NpcTestDwellSeconds while the mod’s own NPC diagnostics (NpcHeadTrace, NpcCaptureSchedule, NpcLayerProbe, NpcShots) run, and despawns it. The spot is measured once at the start, so looking away changes nothing. Every line is prefixed [NpcTest] in MelonLoader/Latest.log, ending in a summary block with one line per mode: the crate’s title, whether the NPC reached a live state (its puppet initiated, or its NPC rig active), the capture folders it made and any failure. Its NPCs are photographed even with DebugCaptures off. The row shows the running step, and a second press cancels the run and despawns whatever is up. The player’s avatar must be a DH avatar; anything else logs why and does nothing.

The same sequence runs headless: run-headless.ps1 -Recording <take> -GameEnvironment @{ DIGITALHEAVEN_NPC_TEST = "4" } -ReplayPlan @{ quitWhenDone = $false } -UntilLog "\[NpcTest\] end", with a take that wears the avatar to test (a synthetic take with its header.json avatar barcode set will do). The sequence starts by itself 2 s into the replay, and every NPC with a ragdoll is killed that many seconds after it is live and pushed toward the player so it lands on its belly. Its death pictures then show how its chains lie on a body face down.

In the game’s utility menu (the radial menu on the controller), a button that sits inside a wall lights up under the pointer and then ignores the press. The two are different paths in the game, found in the decompile: PopUpMenuView.LateUpdate intersects the controller’s ray with a plane (no physics) and feeds that point to PageView.UpdateCursor, which lights the button, and to VRInputModule.UpdateRaycastPosition. A press is an ordinary EventSystem press, which asks every raycaster and gives the click to the nearest result. VRGraphicRaycaster has no blocking test (its Raycast never reads the blocking fields), but VRPhysicsRaycaster also hits world colliders, so a wall in front of the button is the nearest result and takes a press it has no handler for.

With Menu clicks through walls on (MenuClicksThroughWalls, under Advanced), MenuWalls patches PopUpMenuView.Activate and Deactivate to know the menu is open and makes VRPhysicsRaycaster.Raycast report nothing in that time, so the graphic hit is the only one and the press reaches the button that was lit. Other world-space panels, and the menu once it is closed, are untouched. Off by default: it is read from the game’s code and has not been tried in a headset, and the physics raycaster may carry other 3D UI that this setting would silence while the menu is open.

Experimental Every DH avatar is one NPC entry in the spawn gun, listed under its own name. What it spawns is chosen in the spawn gun’s info pane while the entry is selected, in rows below its description (the spawn pane): its disposition, its base NPC, and its body mode once a development body is on. Each mode is built in its own file:

ModeBody rowBarcode segmentBodyOffered
FordFordfordThe avatar’s bones ride the base NPC’s own rig, scaled to its height (below), as community NPC mods doAlways (with Npcs)
DH rigDH rigdhRigThe base’s AI rebuilt on the avatar’s own skeleton (below)NpcBodyDhRig, off
DH animDH animdhAnimThe DH rig body with animation only: the puppet disabled, no ragdollNpcBodyDhAnim, off
Player rigPlayer rigplayerRigA player rig wearing the avatar, walked by DH’s own NPC brain (below); it builds on no baseNpcBodyPlayerRig, off

Npcs lists or hides every entry, live, and rebuilds the spawn panels already in the scene; a mode’s switch only decides whether the Body row offers it (the row is hidden while Ford is the only one). An entry whose chosen mode is turned off spawns the next layer’s choice (below). Nothing of the stock NPC’s body is shown or heard in any mode: its renderers are forced off and its voice is silent, except what it holds: a renderer under one of its hands that is not skinned to its body (a sword, a projector) stays shown. The word for friendly or hostile is DigitalHeaven’s shared disposition.

The base and the disposition. Every entry is built from Ford (NPC_Ford_BWOrig) unless the player picked another base in the pane or its author chose one (below). Ford’s parts are what every other stock humanoid shares (the brettEnemy@neutral skeleton, its humanoid Avatar and a 17-muscle ragdoll), and a base must have a PuppetMaster, an AI behavior, a MarrowEntity and a humanoid Animator on its puppet’s target (NpcBase.Unfit, the same check the [Ford] build makes). SLZ itself turns Ford friendly or hostile with a behavior config, not with another NPC, so the disposition is only AI settings, set in one place (NpcTemperament). Both dispositions are written whatever the base, so a Friendly NPC on a hostile base (a NullBody, a Security Guard) is friendly too:

DispositionFromaggressionirritabilityplacabilityvengefulnessOwn type (npcType)Attacks (agroOnNpcType)
HostileNullBody11010NullBody1015: the player and every NPC type but NullBody
FriendlySLZ’s friendly Ford (Ford_FriendlyFord)0.250.5100FordHair510: Ford’s enemies (EarlyExit, NullBody, Omni…), never the player

A player the AI sees always goes on its threat list; aggression decides whether it attacks, and another NPC goes on the list when its type is in agroOnNpcType. Hostile DH NPCs attack everyone, the player and other NPCs alike. The values go onto the NPC’s live health numbers and onto an edited copy of its prefab config, which the behavior resets them from; each spawn checks them and sets back anything it changed.

Each field of an entry comes, on its own, from the highest of three layers that states one the game can take now (NpcChoices):

  1. The player’s choice, made in the spawn pane and kept per entry in UserData\DigitalHeaven\npc-choices.json (format: dh-npc-choices, version 1, entries by the avatar’s Marrow barcode, each with the disposition, mode and base the player set).
  2. The avatar author’s default, npcDisposition and npcBase in the avatar’s BONELAB settings file, the one it opts out in (platforms/bonelab/<avatar>.jsonc, below). npcBase is a stock or modded NPC crate’s barcode.
  3. The default: Friendly, the [Ford] body, on Ford.

A base that is not installed, or that cannot carry an avatar, and a body mode that is turned off, fall back one layer, and the log says so once per entry.

platforms/bonelab/avatars/mayu.jsonc
{ "npcDisposition": "hostile", "npcBase": "SLZ.BONELAB.Content.Spawnable.NPCSecurityGuard" }

A pool holds one prefab, and LabFusion syncs a spawn by barcode alone, so every combination is a variant crate of its own, hidden (redacted) from the list. The spawn gun’s selection of an entry is swapped for its current variant (a postfix on SpawnGun.OnSpawnableSelected), and a choice made while a gun holds the entry points that gun at the new variant, so its next shot, and LabFusion’s request for it, carry the variant’s barcode. The variants on Ford are registered with their entry and keep the barcodes every NPC had before the entries were folded into one (DigitalHeaven.Avatars.Npc.<avatar>.<mode>.<disposition>), so saved spawnables and favorites still spawn. A variant on any other base appends the base’s hash (.b<fnv8 of its barcode>) and is made when first needed. The listed crate’s own barcode is the avatar’s with Npc for the crate type and nothing after it; its main asset carries the first variant it was prepared with, for a spawner other than the spawn gun.

  1. Crates. Once the player rig exists (XR is up and the menu scene has loaded), or 30 s after boot registration if it never appears, each DH avatar gets its listed SpawnableCrate and a hidden variant on Ford per body mode and disposition, in one pallet per source namespace (see the spawn menu) added to the warehouse with AssetWarehouse.AddPallet. None exists during boot: the first boot that registered them there (run 23) died in the VR runtime’s start, and they have no use before a level. Each is tagged NPC, DigitalHeaven and its source pallet’s tags (not Ford’s Humanoid), copies Ford’s unlock state, since the spawn gun’s menu keeps what it keeps of Ford, and its base’s collider bounds for the placement box. Its hologram is the avatar’s body log hologram. Its description is built from named fields (NpcCrateDescription: the avatar and its namespace), rendered as DigitalHeaven NPC: <avatar> and a From <namespace> line, below the crate’s barcode; the body, base and disposition are the pane’s rows.
  2. Build on demand. Nothing is built at boot. Choosing an entry in the spawn gun (SpawnGun.OnSpawnableSelected), anything registering a pool of one of its crates (AssetSpawner.Register), or a load of its crate before either (Crate.LoadAsset) builds the avatar the same way a swap does, then builds the variant’s body mode from its base (below), once per avatar, base and mode per session. Each variant gets its own copy of that build with its disposition applied, armed as a completed handle like the avatar crates’ assets. Each base is loaded from its crate when first needed (Ford at the first scene load) and kept as an inactive copy, its stock handle held for the session.
  3. Spawning. The pool instantiates the built prefab, so the NPC wakes already wearing the avatar. A frame after Poolee.OnSpawn, once the puppet has started, the mod checks the disposition and the silence, and binds the avatar’s spring chains (the shared Il2CppSpringBones.BindClone), which step in PuppetMaster.OnPostLateUpdate, after the ragdoll’s pose is mapped onto the body.

The [Ford] mode (NpcCommunity) builds an NPC the way the community NPC mods measured in research are built (Taidum, Viper, Foxy, Roxy, the Employee, Combat NPCs): Ford’s skeleton, humanoid animator, PuppetMaster ragdoll and AI stay, so walking, attacking, grabbing, taking hits and dying are Ford’s own, and the avatar’s bones ride its ragdoll. It runs on the inactive prefab, once per avatar:

  1. Ford is scaled uniformly to the avatar’s head-to-feet height: everything under the NPC’s root (AiRig, Physics, brettEnemy@neutral) by one factor, as the NPC SDK guide and every Taidum NPC (Ford × 0.70) do, with the root itself left at 1 for the spawner. As the guide says, each rigidbody’s mass and each joint drive’s maximum force scale by the same factor; LiteLoco’s leg length, in world meters, scales too.
  2. The avatar is copied in without its game avatar component or any physics of its own and turned to face Ford’s way. Its limbs are pointed along the matching Ford muscle segments (the thigh along Hip_L to Knee_L, the foot toward Ford’s toes), so they follow Ford’s; its hips, spine, chest, neck and head keep the avatar’s own upright rest (pointing them along Ford’s bent run 29’s NPCs forward and dropped their heads). The head is then turned by how Ford’s head sits on its chest in its idle (Idle2, sampled on a bare copy of Ford’s skeleton) against how it sits in the prefab, which holds it bent forward: an upright head in the prefab tipped back to the ceiling in play (run 30). It is then moved so its hips stand over Ford’s and its feet on Ford’s.
  3. Bones are paired by humanoid role with the shared HumanoidCorrespondence, the torso from the top down: Ford’s 16 role-bearing muscles (Root_M, Spine_M, Chest_M, Head_M, and the hips, knees, ankles, shoulders, elbows and wrists) each take the avatar’s bone of the same role.
  4. Ford’s joints move onto the avatar’s, as Combat NPCs does: each paired muscle and the animated bone it chases move to the avatar’s joint, so the ragdoll and the animation bend where the avatar bends. Humanoid clips write rotations (and the hips’ position) only, so Ford’s clips play on the lengthened bones; the hips stay on the animated root. The joints are re-anchored where their bodies now meet, LiteLoco’s leg length follows the new hip-to-ankle length, and the AI’s eye stays where Ford has it on the head: the AI aims its head and spine by it, and moving it onto a Taidum’s eyes (run 30) came with heads tipping back.
  5. Colliders fit the mesh, as Foxy and Roxy do: each muscle’s main solid collider becomes a capsule around the avatar’s vertices that its partner bone moves most (fingers count toward the hand, toes toward the foot, eyes and jaw toward the head; never tails, ears or hair), along the limb, or along the part’s longest reach for the torso and hands. Its radius is NpcThicknessPercentile of the vertices’ distances from its axis (the median by default), and its ends drop the outermost 2% of vertices. The head counts more and is fitted differently. Every bone under the head bone counts toward it, humanoid or not (a snout, a nose, a tongue, brows), unless a spring chain of the avatar’s sits between (ears, whiskers, tufts), since a snout is rigid and must be felt. The head is fitted as a skull plus whatever protrudes from it: its cloud is cut into NpcHeadSlices slices along the avatar’s forward, never along its widest reach (a cat’s cheek fur reaches farther ear to ear than nape to nose, and sliced that way the head read as two balls side by side). Each slice’s radius is read from its surface: the median over twelve equal angles of each angle’s farthest vertex, so a dense jaw or eyelid near the middle cannot read a slice thin. The longest run of slices at least NpcHeadSkullShare of the widest one is the skull capsule, as wide as its widest slice and centered on its surface (the midpoint of its extremes across the axis), not on the median of its vertices, so one-sided density (a tuft, a tongue) cannot pull it off the head’s mirror plane. When the slices beyond the skull cover at least NpcHeadProtrusionMin of the head’s length, a second, thinner capsule on the same GameObject wraps only the vertices there that the skull capsule leaves out (farther than its radius, plus 5%, from its axis), provided they too cover that share of the length. Fitted to every vertex in those slices, it took in the brow and eyes on the skull’s front and sat at eye level with the jaw hanging below it; fitted to what the skull leaves out, it sits on the muzzle and jaw. Only those vertices within their own reach along the axis count (dropping the outermost 2%), so a few left out beside the skull cannot widen it; it is centered on their surface, each slice’s extremes read with its outermost 2% left out so a stray tuft cannot set one, and it is as wide as its widest slice’s surface reach about that center. A round head stays one capsule, and a long one is no longer a capsule too wide for its snout and too short for its skull. The slice profile (slice, position along the axis, radius, in meters at world scale) is logged with the fit, and so is the skull’s offset from the eyes’ midplane next to where the vertex median would have put it, naming the dominant bones with the most vertices on the leaning side. The fit is Core’s own (ColliderFit, the same code the headless NpcHeadProbe and Studio run), so Studio’s inspector shows any avatar’s skull and protrusion capsules, and the vertices they leave out, before it is ever spawned here. A part with fewer than 12 vertices keeps Ford’s collider. A part that stands near the floor (its stock collider lower than NpcFloorRuleHeight, 0.2 m, at this size; the feet and ankles) may then not reach closer to the floor than Ford’s own does at rest, times the height fit (the shared NpcGround rule); a head, torso, hand or tracker is never moved by it, and every part it passes over is logged. Muscles that no joint connects but whose fitted colliders overlap at rest (a head over the chest) are added to each other’s ignoredMuscleIndexs: Ford collides with itself only once it is killed (internalCollisions off, on for the kill), and overlapping parts would then push each other apart and fight.
  6. Each paired bone is re-parented under its muscle, keeping its world pose. The clavicles and the neck ride the bones Ford’s own skin uses there (skn_clav_L/skn_clav_R and Neck_01 under Chest_M, which Ford’s IK keeps joined to the shoulders and head), moved onto the avatar’s clavicles and neck first; riding the chest left a gap at the shoulders whenever an animation moved an arm (run 29). Every other bone (fingers, tails, ears, accessories) stays under its avatar parent. The avatar’s own Animator is removed; Ford’s animates the NPC.
  7. Springs. The avatar’s spring chains are bound on the build before its bones move, and each spawn binds its own from the build, whose layout it shares.
  8. Ford is hidden and silent. SLZ’s body and joint validation re-reads the ragdoll (it drops each muscle’s Entity tracker, which the build logs), and each body then restores the scaled mass with Ford’s own center of mass and inertia (the inertia scaled with the body), which validation cannot read on the inactive prefab and which keep Ford’s head balanced on its pivot; the fitted colliders change the shape, not the weight. The build fails, spawning a plain Ford with the reason logged, unless PuppetMaster.IsValid passes with every muscle on its target.

Animated muscle frames. Both fitted ragdolls keep their build-time connected anchors (updateJointAnchors=false); native Muscle.UpdateAnchor would overwrite these physical sockets using animation-bone axes without body scale. For [DH rig], a postfix on Muscle.Read also converts the requested rotation to the child and connected bodies’ physical frames using their offsets saved at initiation. The previous PuppetMaster.Read postfix did not run: Marrow’s live OnLateUpdate inlines that wrapper and calls Muscle.Read directly, before the PD controller. The first corrected read logs each muscle and the size of its correction. This repair excludes the working [Ford] mode and runs only while a rebuilt puppet is Active; [DH anim] stays Disabled. A failed native-patch installation refuses the active DH build, using the existing logged stock fallback.

Mapping back to the avatar. Native Muscle.Map converts rotations with the saved body-to-bone offset, but its connected-body position path uses the body’s local point directly in the connected animation bone’s TransformPoint. For the DH head and shoulders, those axes and scales differ. A prefix saves the animation position; a postfix maps the physical segment in world units through the connected bone’s physical frame and blends from that saved position using the native mapping weights. This preserves the fitted segment length across an avatar’s import scale. The disconnected hips retain native world-position mapping. A separate postfix converts SampleTargetRotations(true) recovery snapshots from body rotations to bone rotations, preserving the native layout (root in world space, the others relative to that root). Animation-only samples are already bone rotations. All three native frame patches must install for an active DH build to proceed.

Early DH-rig diagnostics. Tracking begins at PuppetMaster.OnEnable, before the normal pooled-spawn report. Enter/exit lines for enable and Poolee.OnSpawn, disable/destruction lines, and snapshots at frames 1, 5 and 30 report the crate, root position, active/initiation state, live rigidbody count, nonfinite poses, anchor gaps, native read counts, and the AI target’s head rigidbody. Pose error is also measured at the read point, before mapping can overwrite the target bones. The first corrected position map per muscle reports the displacement, weight and parent scales; recovery samples report their rotation correction, and lifecycle lines count both repairs. Run 41 confirmed the read hook ran for all 17 muscles and the body stayed spawned, but it still twisted and fell. The subsequent position-mapping and recovery-sampling corrections need VR validation.

The experimental [DH rig] mode, offered with NpcBodyDhRig on, runs on the inactive prefab before any instance wakes, so every Awake reads the rebuilt rig: the Marrow bodies cache their pose, the joints their configuration, and the puppet initiates on the avatar’s bones. Run 41 remained spawned but unstable; its ability to stand and move still needs an in-game retest after the mapping and recovery corrections above.

  1. Ford’s humanoid bones are read through Ford’s own humanoid Avatar, on a bare copy of Ford’s skeleton transforms (an inactive Animator binds nothing, and activating Ford would wake its AI).
  2. The avatar is copied into the NPC where Ford’s animated skeleton was, turned to face Ford’s way, and, with NpcFitHeight on (the default), scaled uniformly to Ford’s head-to-feet height, so Ford’s AI (reach, stepping, navigation) fits it. Its root then goes back exactly onto the puppet: PuppetMaster.IsValid refuses to initiate unless the target root sits on the puppet, and in run 27 the height fit had lifted the root to put the feet on Ford’s, so no puppet started (a limp ragdoll on the floor, an AI that never woke, an NPC that could not be killed). The humanoid animation places the hips, and so the feet, itself. Its own game avatar component and any physics of its own are removed.
  3. Bone partners are paired by humanoid role, never by name. The torso pairs from the top down, since the arms and neck hang from the topmost torso bone on both rigs: an avatar with a spine and a chest but no upper chest pairs Ford’s upper chest with its chest and Ford’s chest with its spine. A Ford bone the avatar has no partner for (Ford’s jaw on an avatar without one) gets a stand-in transform under the nearest partnered bone.
  4. Ford’s prefab pose is carried onto the avatar through both humanoid Avatars (the shared IHumanoidPoseTransfer), the way the Animator will carry every animated frame, so the puppet initiates on exactly the pose its first frame writes. Pointing each avatar bone along the matching Ford muscle segment instead (run 36) left the retarget’s own zero 38 degrees off at the feet, and the ragdoll stood on tipped toes and fell. The build logs each muscle’s turn against its target beside Ford’s own, and how far its frame turned from Ford’s.
  5. The muscles (rigidbodies, colliders, joints, and every hitbox, grip, tracker and damage receiver on them) move onto their partner bones. Each keeps Ford’s mass. Its colliders are resized in the segment’s own frame: along the segment by the avatar’s bone length over Ford’s, across it by the avatar’s thickness over Ford’s collider radius. The thickness is measured from the avatar’s skinned meshes: every vertex a humanoid bone moves most counts toward that bone’s segment, and NpcThicknessPercentile of their distances from the segment (the median by default) is its radius. Accessory bones (tails, ears, skirts) are left out, and an unreadable mesh falls back to the bone-length scale. A muscle with no partner bone is locked to its parent, merged into it. The joints are re-anchored where the bones meet, and SLZ’s own validation re-reads each body’s colliders, bounds, trackers and mass properties. The feet are what the body stands on, so they keep Ford’s contact shape at the body’s scale, only as long as the avatar’s foot within NpcFootLengthMin to NpcFootLengthMax of Ford’s. No solid collider of a part that stands near the floor (NpcFloorRuleHeight) may then reach closer to the floor than Ford’s own does at rest, and trackers are never moved (NpcGround, which every body mode can use, on the shared ColliderFit shape math): a near-vertical capsule is shortened from below, anything else moves up. In run 29 the paw-thick feet and the thickened shins sank into the floor, and the physics threw the ragdoll over as it spawned.
  6. The puppet’s target becomes the avatar: PuppetMaster.targetRoot points at it, every muscle chases its partner bone, and mappingWeight goes from Ford’s 0 to 1, so the ragdoll’s pose (hits, stumbles, death) is mapped back onto the avatar, the hips’ position included. Ford’s own body is skinned to the muscles; the avatar is skinned to its own bones, which the puppet writes.
  7. The retarget. The avatar’s own Animator gets Ford’s controller (PowerLegs_AnimController_BW_Ford, humanoid clips) with Ford’s culling, update and root-motion settings, and plays it on the avatar’s humanoid Avatar.
  8. Rewiring. Every reference the AI holds into Ford’s skeleton is found through the IL2CPP field metadata (Il2CppObjectFields in the shared library) and rewired to the avatar’s partner bone, a stand-in, or the avatar’s Animator: the puppet’s target root, the muscles’ targets, the IK’s spine, neck, head and hand bones, and the limb IK. The four limb IK components move from Ford’s bones onto the avatar’s, since the puppet finds its solvers under its target, and stay disabled as Ford’s are: the AI’s IK sub-behaviour solves them when it wants them (run 27’s moved copies started enabled and solved every frame at full weight, reaching the arms out and twisting the feet). A limb IK turns its end bone to its target’s rotation, so each IK target turns by Ford’s ankle or hand frame to the avatar’s, handing the avatar the rotation it handed Ford. The AI rig’s pelvis, hip, foot and hand proxies move with the body parts they stand for, and LiteLoco’s leg length scales by the avatar’s hip-to-ankle length over Ford’s. The AI’s eye goes onto the avatar’s eyes, so its vision, and the look targets DH eyes use, sit in the avatar’s head. With NpcRigAiIk off (the default since run 31, when mltn saw the legs and head twisted), the limb IK and the IK sub-behaviour’s spine, neck, head and hand bones stay on Ford’s hidden skeleton: SLZ turns them about Ford’s bone axes (x down the bone), which bends an avatar’s y-along bones where it means to twist them. The avatar then takes the animation and the ragdoll only.
  9. Ford’s body is kept inert: its Animator and the scripts on its skeleton are off and its renderers forced off, while the damage renderers and the eye package that still point at it keep a live object.
  10. Silence. The AI’s voice plays from the first AudioSource on its behavior script’s object, or one it adds; a muted one there takes the voice, and the breathing loop it adds is muted at spawn. Hits keep their own impact source, and footsteps theirs.

Before a rebuild is used it is checked the way PuppetMaster.IsValid checks it (read from its code): the target root exactly on the puppet, every muscle within about 3 cm of its target, no joint shared, and IsValid itself. If a rebuild fails, the entry spawns a plain Ford and the log says why. A spawned NPC whose puppet did not initiate logs an error with the failing condition.

Opting out. An avatar opts out of being an NPC in its pallet’s BONELAB settings file, at the avatar’s path under platforms/bonelab/ (or the ID com.stresslevelzero.bonelab) with .jsonc in place of .dh-avatar (see NPCs):

platforms/bonelab/avatars/mayu.jsonc (for avatars/mayu.dh-avatar)
{ "npcs": false }

The Npcs preference turns every DH NPC off.

Captures. With DebugCaptures and NpcCaptures on, or during Run NPC test, the pictures show the NPC the way the player sees it, for every body mode (NpcShots, scheduled by NpcCaptureSchedule). Each moment has up to three views: from the player’s eyes (the art rig’s eye midpoint, as the eye look reads it) looking at the NPC, a three-quarter view 45° round from that line and 30° up, and a view straight down with the player’s side at the bottom. Each view is framed on the NPC’s own renderer bounds, so a body lying where it fell stays in frame. While a body is photographed, its skinned renderers update their bounds from the pose (updateWhenOffscreen, put back afterwards) and each render re-skins from the bones as they are then (forceMatrixRecalculationPerRender). Only the NPC is drawn, in the scene’s own lighting, over a flat gray card at the height of the floor under it, found by a ray that skips the NPC’s own colliders.

  • Series. Each crate’s first spawn of a session is photographed from all three views at 0.5, 1, 2, 4, 8 and 15 s after it spawns, plus two more eye frames at 15.1 and 15.2 s, so three frames 0.1 s apart show whether a settled body is still moving.
  • Deaths. Any DH NPC is photographed from all three views 0.5, 2 and 4 s after it dies: the fall, the body at rest and its chains settled on it.

At most one picture is rendered per frame, so the views of one moment are a frame or two apart. For [Ford] and [DH rig] bodies the pictures are taken in PuppetMaster.OnPostLateUpdate, after the puppet, the springs and the eyes have finished the frame’s pose. A spawn’s pictures go in bonelab-captures\npc\<time>-<mode>.<disposition>\ as t00.5-eye.png, t01.0-3q.png, t15.0-top.png, t15.1-burst-eye.png and dead-0.5s-eye.png to dead-4.0s-top.png, with npc-materials.txt and shots.txt. A spawn with no series gets a directory ending in -dead. Each moment logs a line and adds it to shots.txt: the stance and AI status from the health line, and the body’s bounds with its top and lowest point above the floor. A body lying flat reads a short top.

[DH rig] spawn diagnostics. Every [DH rig] spawn logs a self-collision audit (NpcSelfCollision, shared by the body modes). It lists the puppet’s internal-collision setting and state, every collider that rides no muscle, and how many of the NPC’s own solid pairs on different bodies physics lets collide. On a crate’s first spawn of the session it also lists every pair that overlaps, with the depth in millimeters, whether a joint connects the two, and whether the pair collides. With NpcRigIgnoreSelf on (the default), every pair of the NPC’s own solid colliders then ignores the other while it lives. This tests mltn’s hypothesis that parts fighting each other mangle the body. A stock NPC does not collide with itself until it dies. The setting is reapplied on every respawn, because a deactivated collider forgets which pairs it ignored.

On a crate’s first spawn of the session, 1.25 s in, a layer probe (NpcLayerProbe) separates the layers that pose the body. It photographs the body from the eyes and the three-quarter view in three states, holding each for the frames its pictures take, and then puts everything back:

  • layers-1-full: the body as it plays.
  • layers-2-noMapping: the puppet’s mappingWeight set to 0, so the avatar shows only the animation and the AI’s IK.
  • layers-3-animatorOnly: the IK sub-behaviour’s foot and arm IK also switched off, so the avatar shows Unity’s humanoid retarget of Ford’s clips alone. SubBehaviourIk.Solve rewrites the foot weights every frame, so they are zeroed again on each frame the state is held, and what SLZ wrote back is logged. With NpcRigAiIk off, the IK acts on Ford’s hidden skeleton anyway, and the log says so.

Each state logs a reading of the avatar’s bones and of the ragdoll’s muscles. The readings cover the pelvis’s tilt and facing, the spine’s bend and the chest’s yaw. For each leg they give the knee’s bend and which way it points, the foot’s yaw, pitch and whether its sole is under it, and the thigh and shin twists. For each arm they give the elbow’s bend and direction and the upper arm and forearm twists, and for the head its yaw, pitch and roll against the chest. Every direction is anatomical: it comes from what each part’s own axes held as forward and up in the build’s standing pose, so an avatar’s bone-axis convention never enters it. The pure math is PoseReadout in the shared library. Each state also logs how far each ragdoll part has turned away from the avatar bone it chases, and anything out of range, such as a knee bending backward, a foot turned more than 60°, a sole not under the foot or a twist past 90°. The first state also logs each muscle’s rotation relative to its target, which the puppet kept when it initiated. The final line compares these sequential samples; the controller keeps advancing, so their differences do not establish a cause. Run 34’s animator-only sample was playing GetUpFromFace, not standing idle. A separate build-time probe samples the same fixed idle clip on transform-only copies of Ford and the DH skeleton, without a controller, physics or AI, to check retargeting against each unmodified source pose.

Logging. At Quiet, LogNpc keeps one line per crate registration and per spawn, and two weight lines. Each [Ford] build says how many GameObjects every spawn of it is (every spawn is ... GameObject(s)): the base’s (its ragdoll, its animated skeleton, the rest, and its skinned renderers), the avatar’s (its own bones, the bones of clothing an armature link put on them, the objects that draw, its armatures and skinned renderers) and what the build added. A pooled NPC that comes back bigger than it went out says so (came back from the pool with ...). At Normal, [Npc] lines cover crate registration and opt-outs with each entry’s disposition values, every load of a DH crate through Crate.LoadAsset or LoadAssetAsync (the first three per crate, with whether its handle is armed), each preparation and base load, for a [Ford] body, the height fit and every scaled part, the turn, the bone pairs and the bones left to ride along, the pose, the placement, every moved pivot, every fitted collider (vertices, before and after), each body’s restored mass properties and every re-parented bone with its offset from its pivot, the kept trackers and the preflight; and every step of the DH rig rebuild: Ford’s humanoid bones, the avatar’s placement and height fit, the bone partners and stand-ins, the pose, each muscle (its target, segment, length and thickness scales, measured radius, mass, every collider’s size before and after, its joint anchors), the AI rig proxies and leg length, each rewired reference and what was left on Ford’s hidden body, the retarget (Animator, controller, humanoid Avatar) and the silenced voice. At spawn: whether the puppet initiated on the avatar, a two-second head trace (the head against the chest in the animation and in the ragdoll, the head muscle against its target, its tilt and spin, and the head’s live mass properties), the disposition values and anything set back, the springs and the voice. At Verbose, every crate and every per-muscle, per-collider and per-bone line is added, and every NpcReportFrames frames each NPC logs a health line: the animator state and clip, the AI’s mental and locomotion state, the puppet’s state, mode and death, how far the avatar’s hips sit from the hips muscle, the stance (the hips muscle’s height over the lower foot, how far the head leans off vertical, and each foot’s height), the grounding the AI reads (its sensors’ foot, hand and body support and isGrounded), the distance to the player, and how many of its own solid collider pairs collide now. Despawns, respawns and cleanup are logged too.

Experimental The Player rig body, offered in the Body row with NpcBodyPlayerRig on, is a second BONELAB player rig wearing the DH avatar, the way LabFusion shows a remote player, walked by DigitalHeaven’s own NPC brain instead of Ford’s AI. The body is physically the avatar’s own size, since the game fits a player rig to its avatar, and it has no PuppetMaster: it moves, climbs and falls like a player.

  • The crate holds only an inactive marker. Each spawn of it loads the game’s default player rig (MarrowSettings.DefaultPlayerRig), copies it at the marker under an inactive parent and strips what belongs to the person playing before it wakes: the Player bone tag, the controller rig’s bodies, trackers and entity (a tracker streams the level around itself), the headset’s camera, audio listener, streaming and volumetric components, the first-person head hiding (PlayerAvatarArt), the quick menu, time input, double jump, controller UI and haptics (a haptor rumbles the person’s own controller), the ammo pickup and inventory plugs. The vignette gets an inert copy, and the voice and wind rush are muted. Both OpenControllers give way to plain BaseControllers. Everything the rig renders stays hidden until the avatar is on: the DH avatar is swapped in with RigManager.SwapAvatar as soon as the rig has put on its own first avatar, usually a frame or two after it wakes, which binds its springs and eyes like a player’s body. Its bodies then get impact surfaces with the avatar’s surface data (a player rig has none, so bullets drew no blood), and BONELAB’s own wounds (see Blood and wounds). Its swaps never name the player rig or schedule captures.
  • The brain is NpcBrain in Core (DigitalHeaven.Core.Npc), host-agnostic and deterministic like the eye brain, so the engine can run it later. It wanders near where it spawned on points snapped to the navmesh, pausing between them; a friendly one follows the player once they have been more than 2.5 m away for a second (walks, runs when more than 6 m behind, stops 1.2 m away) and otherwise stands beside them, glancing at them now and then with its head; it engages what its disposition allows after a short reaction, strikes with alternating hands (a windup, then a swing through the target’s head), flees once when badly hurt, jumps when stuck, and dies at zero health. Hostile goes for the player, every NPC that is not hostile, and anything that hit it (NullBody’s list); friendly goes only for hostile NPCs and never for the player, even one that hit it (Ford’s friendly values). A DH NPC of another mode is as hostile as its crate; a stock NPC counts as hostile above aggression 0.5 (run 33’s friendly took a friendly [Ford] NPC, at 0.5, for hostile and beat it).
  • Turning like Ford, not a turret. Run 40’s NPCs turned the instant the player moved, fought a hand that tried to turn them, and spun when held overhead: the turn stick chased wherever the head looked. SLZ’s Ford turns through its locomotion instead: its nav agent turns it at a roaming or agroed angular speed (BehaviourBaseNav.roamAngSpeed, agroedAngSpeed), LiteLoco places its feet from the root’s velocity and turn (UpdateLiteLoco(worldVelocity, rotDelta)), it faces a target with a spine and head twist (SubBehaviourIk.SpineTwist) rather than its body, and it has in-air, fallen and get-up locomotion states, plus a count of hands holding it (BehaviourGrabbableBaseNav.handCount). The brain now gives a body facing beside the look: the body turns toward its way while it walks, eased and at most NpcRigWalkTurnRate (NpcRigRunTurnRate running, fighting or reacting); a way more than 70° off is turned to in place first (NpcRigStepTurnRate when nothing is urgent); a target it reacts to or fights is stepped round to only past 45°; and beside its leader the body keeps its facing and only the head glances, within NpcRigGlanceYaw of the body, unless the leader has stayed more than 110° round for 2.5 s. The facing never leads the body by more than 20°, and a body turned farther by anything else takes the facing with it, so nothing pulls it back against a hand. Nothing steers while a hand that is not its own holds one of its grips, while it has been off the ground for a quarter second (PhysicsRig.physG.isGrounded), or while its hips tilt more than NpcRigUprightDegrees, and it waits 0.6 s after before it steers again; the eyes then look ahead rather than at a point over or under the head. The walk push is NpcRigWalkSpeed: run 40’s 0.45 made 0.01 to 0.16 m/s, which read as a slow creep toward the player.
  • Driving. Each frame the brain’s move becomes the locomotion thumbstick, turned into the frame the rig steers by (its direction master, or the headset), and the other stick turns the play space toward the brain’s facing (between NpcRigTurnDeadZone and NpcRigTurnFullDegrees off), never while the body is held, airborne or toppled. The brain reads the body’s facing and hip tilt from its pelvis, not from where the head looks, in the pelvis’s frame as the rig’s prefab stands it (run 41 took that frame on the swap’s frame while the body was still settling, 10 to 27 deg off, and an NPC then read 11 deg of tilt standing, turned in place for minutes toward a facing it could never reach, and later lay ‘toppled’ without steering). Its look and hands become the three tracked points (the resting hands hang from the facing, not the glance), written in a prefix on OpenControllerRig.OnRealHeptaEarlyUpdate with RemapRig.inWeight 1. The head is held where it was first put, straight over the body at the avatar’s eye height, as a person standing still walks on the stick: run 30 held it at the play space’s center instead, 1.9 m from the body, and the rig slid the body after it with barely moving legs. At rest the hands hang beside the hips, pitched down along the forearm, and swing in opposite phase while walking (run 40’s level hands bent both wrists up, pointing ahead of the body). While it fights, a hand that is not striking holds a level guard before the chest (NpcRigGuardDrop, NpcRigGuardForward, NpcRigGuardSide), and each swing is sent for NpcRigStrikeSwingSeconds: run 41’s strikes began from the hanging pose and lasted the brain’s 0.18 s, and its fists peaked at 0.18 to 0.45 m/s and never came nearer than 63 cm to the target’s head. A strike moves a hand target through the target, limited to 1.1 times the arm’s own length (shoulder, elbow and wrist of the rig) from the body’s own shoulder, and the brain strikes within the arm plus 0.25 m of the target’s head and stands 0.6 of that away, since run 40’s defaults (1.1 m and 0.8 m) left a small avatar’s fists short of anything they swung at. Arms hold their pose with NpcRigArmStrength of the rig’s own arm force (PhysHand._forceMultiplier) at rest, so they can be pushed around, and full force to fight. Until the brain runs, inWeight is 0, so the rig never follows the person’s own headset. Paths come from the level’s navmesh (NavMesh.CalculatePath); a map without one gets a straight walk.
  • Punches. A player’s punch is HandSFX.OnSignificantCollisionEnter handing off to PunchAttack: with nothing held, the hand closing on the contact at more than 2 m/s and moving at least 0.95 m/s against the chest, and the controller’s grip axis over 0.82 (a fist), it sends a Blunt attack with the hand’s TriggerRefProxy to the hit collider when that collider’s layer is in the hand’s meleeAttackMask, for min((impulse - 2) x 0.09, the avatar's upper-body strength). A stock NPC takes it through MuscleCollisionBroadcaster, OnMuscleHit and SubBehaviourHealth.TakeDamage, which checks no team or owner. The NPC’s plain controllers held the grip axis at 0, so every contact was a slap; while engaged they now report a closed fist. A plain BaseController never raises a grab, so the fist grabs nothing.
  • Its head gets the skull and snout a [Ford] NPC of the same avatar gets (NpcHeadFit, the shared ColliderFit.FitHead over the head bone and every bone under it that no spring chain swings), NpcRigHeadFitDelay after dressing so the avatar has settled on the rig. The fit is the measure SLZ’s own head is grown to (NpcRigHeadShape): a DH avatar keeps the soft body’s head and jaw ellipses at the component defaults, so the player body’s head collider (PhysTorso.cHead), whose AvatarGrip is what a hand grabs, covered about 14 by 16 by 11 cm of a Taidum head whose fitted skull is 32 cm across and whose snout reaches past it. Each ellipse value is scaled by how much farther the fit reaches than SLZ’s head mesh along that side, in the head’s own space, and the torso rebuilds its head collider and grips from them (CalibrateTorsoColliders, CalibrateTorsoGrips). The fitted capsules then go (NpcRigHeadCapsules keeps them); they stay as plain colliders, ignored by the torso and listed in the head’s Marrow body, whenever the head could not be grown. Either way the head’s center of mass and inertia stay SLZ’s, so a snout’s weight never tips it forward. The avatar’s facing is read in the head’s own frame (the eyes’ line against the head’s up, else the head’s z; HeadShapeMath), never against the world’s up: a head pitched past 45 degrees, as a remote player’s is looking down a second after a swap, used to read as not facing along z, was left as SLZ built it, and kept its fitted capsules as solid colliders the head’s grip (which grabs through its mesh collider alone, AvatarGrip._targetColliders) could not reach, a head a hand felt and could not take (run 87). Each fit logs head grab: at Summary (the grip, the mesh colliders it grabs through, the head collider’s layer and mesh size, the capsules kept solid), and a head that could not be grown logs a warning. Run 43 tried a GenericGrip over the fitted capsules instead, and the head lost its grab sound and its collision, held a hand after the player walked off, and slid the body toward it, so it was taken back out. NpcRigHeadFit turns the fit off.
  • Held, it goes loose. While a hand that is not its own holds any of its grips, it eases over NpcRigHeldEaseSeconds to NpcRigHeldStiffness of its spine’s hold (PhysTorso.spineInternalMult) and of its arms’ force, and its head’s tracked target follows where the hand moves the head, NpcRigHeldHeadFollow of it sideways and forward or back and NpcRigHeldHeadLift up and down (its position only: run 42’s target also turned with the head, and the body spun after it at up to 4400 deg/s). A player rig’s whole body is driven to its head’s target, so whatever share the target does not follow is the whole body pulling against the hand: run 44’s 0.8 left a rig held by the head an anchor whose head never moved. On release it eases back. Run 41’s rig held its head “super stiff” where a stock NPC goes loose.
  • Getting up. A player rig stands back up only as the person’s head rises, and the NPC’s head is held as high as it goes, so run 42’s rig sat in a squat for good after a rough grab. Once its hips have been more than NpcRigGetUpDegrees off upright for NpcRigGetUpSeconds with no hand on it, the physics rig is put back on the pose it is driven to (PhysicsRig.TeleportToPose, as LabFusion does for a remote body that strays).
  • What its fists take. The controller’s grip axis is what both a punch and a grab read, and a hand that held it closed through a fight took hold of every body it touched (runs 42 and 43). The grip closes only while that hand’s strike swings, so a strike still lands as a punch and the hand opens, letting go of anything it caught, when the swing ends. The brain never picks anything up on purpose, so a hand that still holds something after NpcRigLetGoSeconds lets go completely (its joint, the object and its hover lock: run 43’s watch cleared only the object while the joint kept the fist on the other body) and stays open a moment; 0 turns that off. A dead rig’s hands let go at death and every frame after.
  • Stock AI fights it. Stock NPCs never took a [Player rig] as a target in run 42. Every stock AI on the other side (hostile for a friendly rig, friendly for a hostile one) within NpcRigSightRange that does not know of the rig is told of it (BehaviourBaseNav.AddThreat, at the threat its own sensors give the rig’s body proxy), at most every 5 s each while the rig lives (NpcRigStockAiTargets).
  • Perception is the player rig, every other [Player rig] NPC, and every stock NPC with an AIBrain (its TriggerRefProxy feet and head), refreshed four times a second. Stock hostile NPCs see the rig as a player and attack it.
  • Hits and death. The rig stays Invincible with no level reload; a prefix on PlayerDamageReceiver.ReceiveAttack counts each attack’s damage against the NPC’s health, which starts at the rig’s own maximum times NpcRigHealthScale, and remembers who dealt it, and the hit body part is pushed along the shot by NpcRigHitImpulse. At zero the rig drops what it holds and is shut down and ragdolled (PhysicsRig.ShutdownRig, then RagdollRig, as a knocked-out player is); the tracked points are released, every stock AI sensor that holds one of its TriggerRefProxys drops it (SubBehaviourSensors.RemoveTarget), again whenever one picks the corpse up, before removal and every frame until the rig is destroyed (a Ford kept a dead [Player rig] as its target past its removal in runs 40 and 41 and stood frozen, throwing a BehaviourPowerLegs.AiUpdate NullReferenceException every frame), every joint drive’s spring goes to zero and the servos stop, and a joint welded between two body parts becomes one that swings within limits, so the body goes limp instead of falling as one stiff piece, as it did in run 30. NpcRigDespawnSeconds later it is removed and its marker returns to the pool.
  • The person’s view is kept to the person’s rig. The NPC rig’s OnBeginCameraRendering is skipped, and its OnEarlyUpdate runs without the static pause callback.
  • The NPC never stands in for the player. Run 33 lost all grabbing after a [Player rig] NPC was grabbed and removed, and LabFusion later reloaded the scene because the player’s feet had left its bounds. The copy loses any UIRig or PlayerRefs before it wakes. Every place that names the player rig (the game’s PlayerRefs and UIRig, the static pause flag, BoneLib’s Player and LabFusion’s RigData when loaded) is logged with what each of the player’s hands holds, before the rig is made, when it wakes, dressed, dead, in every health line and after its removal, and put back if it names an NPC rig. A removed rig first makes every hand on its grips let go (the player’s own hands checked after), closes its grips, and is destroyed half a second later, so no hand keeps a joint to a body that is gone; it counts as an NPC rig until then. Its head and torso grips are the player body’s own AvatarGrips, which LabFusion peers grab on each other. They were closed after run 33, whose causes were later found elsewhere (a hand holding a removed rig, and holster receivers that hover-lock hands, both fixed). Run 40 found the closed host the only refusal left on the head, so NpcRigHeadGrip opens it; the chest, spine and pelvis stay closed unless NpcRigTorsoGrips is on.
  • Body grips. Limb grips with a local damage receiver and collider use Ford’s NPC grip layer, preserving the rig’s previous own-body collision exclusions. Hand hover queries use hoverLayerMask, independently of the physics collision matrix; the log reports inclusion in that query mask and whether each receiver is enabled. Removal independently detaches each attached hand’s joint and object and clears its hover lock, even if an earlier detach step throws. These changes require an in-game grab/removal retest.
  • Cost. A player rig is about 45 rigidbodies against Ford’s 17: fine for a companion or two, not for hordes.

Its [Npc] lines cover the rig asset load, every stripped part, the swap, the eye height and play space scale it stands at, where the headset hangs, the first tracked points, the first navmesh path (and the first failure), Ford’s own walking and turning numbers once a session, every change of state or motion (standing, walking, turning in place, held, airborne, toppled) with its target, no closer together than NpcRigMotionLogSeconds and with the changes between counted, how far and how fast the body turned since the last such line against the rate the brain wanted, and whether it is grounded, held and by what, and how far its hips tilt, every hit with its damage, type, dealer and push, the death with how many joints were loosened, welded and stopped, and the removal. At the start it logs its fists (the arm’s length from the rig and from the avatar’s bones, the strike and stand distances, the upper-body strength that caps a punch, each hand’s HandSFX melee mask, collision collector and collider layers against the layer matrix) and its head (each head grip’s colliders against the avatar’s head bone and head ellipse, and whether the player’s hover query sees them). Each swing logs where the fist was sent against where the hand went (its start, nearest and final distance from that point and the share of the way it came), the hand’s peak speed, its speed against the chest, how near it came to the target’s head, the arm force and HIT or no hit; each landing logs the collider, damage and what took it (a stock NPC’s TakeDamage, or a rig’s health). It also logs how long dressing took, where the head is held, the impact surfaces, each arm’s force multiplier (and whether the game rewrites it), and every grip on the rig with its layer against the player’s hand and a stock Ford grip. Whenever one of the player’s hands touches one of its grips, it logs what that hand hovers and holds. A health line every NpcReportFrames frames gives the state and why (its leader’s distance against the follow distances, what it waits for), target, health, position, the speed it actually walked, the player’s distance, move and sticks, the held head against the body, the arm force, path, bodies seen, inWeight, the motion, the body’s turn rate against the wanted one, and whether it is grounded or held. The head fit logs its vertex count, both capsules, the settled head bone against the rig’s head and the stock collider, and each capsule’s stab reads. A grab and a release each log a line, and a held rig logs every second how loose it is, its spine and arm hold read back and how far the head moved. Its death logs which stock AI let go of it, and each such AI is checked again 1.5 s later (and sent back to its state before agro if it still rages at no one). The shared NpcCaptures photograph it alive and dead.

BONELAB marks an NPC where it is hit in two separate ways, read from its code (the Cpp2IL dumps) and Ford’s prefab:

  • The spray. Every Ford ragdoll body carries an ImpactProperties with the Blood Surface card (SLZ.Backlot.SurfaceDataCard.Blood) and decalType None. ImpactProperties.ReceiveAttack spawns that surface’s impact effect (a tinted particle burst, ParticleTint.TintParticles) and its sound. It would spawn a DecalProjector, a mesh built by raycasting the struck collider (Collider.Raycast, not the skin), only for decalType Collider, which Ford’s bodies do not use.
  • The wound. Every body also carries a VisualDamageReceiver. Its ReceiveAttack maps the hit point from the struck body into the NPC’s rest pose (orgpos, orgrot and orgScale, recorded from the body’s pose at Awake) and hands VisualDamageController.AddToHitArray a matrix. The controller keeps the last 32 (PosespaceImpactManager) and writes them, with _NumberOfHits, onto every material of its Renderers (on Ford, brett_body, the hands, the face and their LODs). Ford’s skin shader, Lux URP/Human/Skin (TEMPORARYILY REPLACED WITH POSESPACE IMPACTS), compares them with each vertex’s rest position. The mesh carries that position in its second UV channel as xyz (the shader’s vertex input reads TEXCOORD1.xyz, where LitMAS Standard reads at most two components), and paints the wound from _HitRamp and _HitColor. SLZ/LitMAS/LitMAS Posespace is the same wound on the LitMAS surface, with LitMAS Standard’s property names.

The player’s own body is never marked. The player rig’s Player_Health ships with _testVisualDamage off, which is the only gate on Health.VisualDamage (called from Health.OnReceivedDamage). The rig has a VisualDamageController on PhysicsRig with no renderers, no VisualDamageReceiver feeds it (the only callers of AddToHitArray are the receiver and Health), and its bodies carry PlayerDamageReceivers but no ImpactProperties (PhysicsRig._impactProperties ships empty). Stock BONELAB draws no wound on the player’s body and gives it no blood surface, so a worn DH avatar gets neither.

What DH does (NpcGore, one path for every mode). When a body is built, the avatar’s LitMAS materials get LitMAS Posespace copies with the stock skin’s _HitRamp and _HitColor (a ramp is taken only when its name holds a fragment of NpcWoundRamps: Ford’s BloodyHitRamp from a [Ford] NPC’s stock renderers, else a loaded material’s or texture’s; run 44’s [Player rig] NPC, built with no stock renderers, took a crablet’s metalHitRamp from the first loaded material and every DH wound looked like concrete. A flesh ramp found later replaces a stand-in on every copy made so far, and the choice logs at Normal under [Gore]), each mesh gets its rest positions (in the avatar root’s space) in UV1, and the controller’s Renderers become the avatar’s skinned renderers. A prefix on VisualDamageReceiver.ReceiveAttack then rewrites the struck receiver’s orgpos, orgrot and orgScale from the avatar bone the body carries, just before the game’s own mapping runs. The game’s own math then lands the hit on the avatar’s rest pose. It also sets the controller’s hitScaleFactor so a wound is as large on the avatar as on Ford, times NpcWoundSize. The carried bone is the one assigned to a [Player rig] body part, the body itself, its muscle’s target ([DH rig]), or the nearest skinned bone under it ([Ford], where the avatar’s bones ride the muscles). A [Player rig] NPC gets a VisualDamageReceiver on each body with a damage receiver, for the avatar bone of that body part, and its rig’s own Health visual damage stays off, as the stock rig ships it (it would write the same arrays in its own space). Material slots on any other shader keep it and show no wound.

Cost. 12 bytes a vertex for the rest positions on the mesh (the UV1 channel it did not have, shared by every NPC wearing the mesh), 12 more for the managed copy the [Gore] lines measure against, and one material per converted slot. The controller makes its own material instance per renderer on the first hit, as it does for Ford. No draw call is added.

ModeBlood sprayWound beforeWound now
[Ford]✅ Ford’s own bodies and surfaces❌ painted on Ford’s hidden brett_* renderers; the avatar’s LitMAS materials read no hits and its meshes had no rest positions🟡 built, untested in game
[DH rig], [DH anim]✅ the stock bodies’ surfaces❌ the rewire left the controller on Ford’s hidden body🟡 built, untested in game (the mode is not in this build)
[Player rig]✅ the impact surfaces DH adds (run 30)❌ DH turned on the rig’s Health visual damage, which wrote hits onto LitMAS materials that ignore them🟡 built, untested in game
Worn avatar (the player)❌ as stock❌ as stock❌ as stock: BONELAB marks no player body

Logging. [Gore] lines say, per build, which renderers the controller now paints and which it painted before, each mesh’s rest positions (how many, what UV1 they replaced), how many material slots moved to Posespace and which kept their shader, and where the hit ramp came from. For the first NpcWoundLogHits hits on each NPC, with LogNpc at Verbose, a line names the struck body, the attack (with the game’s own skips: back-facing, or not first in its pool), the avatar bone it carries and how it was found, the rest-pose point and wound size, the hits the controller now holds, the nearest rest vertex’s distance and renderer, and each painted renderer’s shader and _NumberOfHits. Another line per impact names the surface that sprayed. A body that carries no avatar bone logs a warning and is not marked.

Dead NPCs and late joiners. LabFusion 1.14.2 sends one PuppetMasterKillMessage for a dead puppet, when the joiner asks for that entity’s data after it spawns the NPC. The joiner drops that message while its level loads, and it can arrive before the joiner’s puppet has started, so a player who joins after an NPC died could see it standing. Stock and DH NPCs share this, since both are a PuppetMaster under a LabFusion entity. DH adds its own catch-up on its own message channel (2): each machine that owns a dead puppet sends a joining player the entity ids 4, 12 and 30 seconds after the join, and the receiver kills each named puppet that is still alive here (LabFusion’s kill guard is stepped around for that one call), retrying every second for 90 seconds until the puppet exists. [Npc] summary lines say what was sent and what was killed. A player without DH still gets LabFusion’s own message only, and [Player rig] NPCs, which LabFusion does not sync, are not covered.

Advanced > Cheats holds the switches that bend the game. Every row there is a safety switch, so one that is on turns yellow and flags the pages above it up to OPTIONS, like the other watched settings. All are off by default.

RowConfig keyWhat it does
Unlock every spawnableUnlockEverythingLists every locked spawnable in the spawn menu (see The spawn menu).
InvincibleInvincibleYou cannot be hurt or die.

Invincible. The game has its own health mode for this: Health.healthMode is Invincible, Mortal or InsantDeath (the game’s spelling), and Player_Health.TAKEDAMAGE does nothing in Invincible, so hits, falls and explosions all stop in one place. While the row is on, the mod writes Invincible to the local player’s own Player_Health every frame it is not already there, so a respawn or a level that builds a new body is covered at once. It remembers the mode it found and writes it back when the row turns off, unless something else changed the mode in the meantime or the game already held the body invincible, in which case it leaves it alone. A direct Player_Health.Death call (a kill floor or a death trigger) skips TAKEDAMAGE, so a prefix refuses it for the held body only. Only your own body is touched: NPC rigs, including DH NPCs, and LabFusion’s remote bodies are never written or refused. In a LabFusion lobby you are invincible on your own machine; Fusion’s damage to you lands through the same TAKEDAMAGE and stops there, and what other players see of you is theirs to decide. Reading the game’s code shows no other writer of the mode on the player’s body besides ToggleInstantDeathMode, which the mod overrides back to Invincible on the next frame. This has not been tried in the game yet, and a failed write to the mode is logged once as an error.

Advanced > Safety holds the ways out of an avatar size or a level you cannot play in. All are on by default, and every row is a safety switch, so one turned off is yellow and flags the pages above it up to OPTIONS. Their lines log under [Safety] (LogInterface).

RowConfig keyWhat it does
Start at 1x after a risky sizeScaleLaunchGuardA launch whose saved avatar scale is orange or red resets it to 1x.
Keep this size? countdownScaleConfirmA change into an orange or red size is asked about; no answer goes back.
Avatar scale on the radial menuScaleRadialAn Avatar scale entry on the radial menu: Reset to 1x, Scale up, Scale down, and Keep and Revert while asked.
Get me out of death loopsMapTrapRescueDying or respawning over and over after a level loads offers a way out to Void G114.

Danger bands. Every avatar scale is safe, risky (orange) or dangerous (red): safe from ScaleSafeMin (0.5) to ScaleSafeMax (3), dangerous above ScaleDangerAbove (20) or below ScaleDangerBelow (0.25, the smallest scale, so small sizes stop at orange), and risky in between. Each edge belongs to the safer band, so 3x is safe and 20x orange. The Avatar scale row shows its value in that color, on the in-VR page (TMP rich text) and on the overlay’s dropdown, and the picker colors each step, 50x and 100x in blood red.

Never launch into a dangerous size. At 100x the lasers stopped reaching the menus and the setting was saved, so the next launch started at 100x too. Now, at startup, before the first avatar goes on, a saved orange or red AvatarScale is reset to 1x in the cfg rather than overridden for the session: a size that broke one launch cannot break the next, and the cfg, the menu and the worn avatar never disagree. The size it was is logged as a warning ([Safety] avatar scale: the saved 100x is dangerous, so this launch starts at 1x ...) and, once the first level starts, shown as a BoneLib notice when BoneLib is loaded.

Keep this size? A change into an orange or red size, from any surface, goes on at once; once the new size is worn (or after ScaleConfirmWaitSeconds, 10 s, if it never settles), “Keep this size?” counts down ScaleConfirmSeconds (10 s) with Keep and Revert. No answer goes back to the last size you kept. A further change while it is open restarts it and still goes back to the last size kept, so stepping up through several orange sizes comes back in one revert; going back to the kept size, or to a safe one, answers it. BONELAB has no confirm dialog of its own (its popups are the radial menu and BoneLib’s notices), so the question is shown on a panel and answered on the radial menu. The panel is built the way the settings page is, a copy of the OPTIONS page with its title as the question and a line under it for the countdown and the two answers, in a world-space canvas of its own. It stands SafetyPanelDistance (0.7 m) in front of your face times your avatar’s arm scale, at the menu’s size for the worn avatar, moves back in front of you when you turn or walk away, and steps aside while the menu is up. It has no buttons: BONELAB’s menu pointer exists only while the menu is open and only on the menu’s own plane (PopUpMenuView.LateUpdate intersects the controller’s ray with it and hands the point to VRInputModule), so a canvas anywhere else is never pressed, whatever raycaster it carries. The first build put Keep and Revert on the panel and nobody could press them. Open the menu to answer: while a question is open, the radial turns to a page of its two answers, Keep on the right and Revert on the left, three slices each, with the question at the top and Menu at the bottom for the home page. A question that opens while another menu page is up (the settings page you just picked a size on) closes the menu once so the panel shows. The radial’s Avatar scale entry offers Keep and Revert as well.

The radial menu’s Avatar scale entry. Hold the menu button: an Avatar scale entry sits in a free direction of the radial menu’s home page, added the way LabFusion adds its quick mute (a PageItem on PageView.m_HomePage). PageView.Render draws one slice per direction and the last item of a direction takes it, and BONELAB fills its home page in two steps: Preferences (east), Inventory (south), Levels (northeast) and Avatars (northwest) when the menu starts, then the spawn gun’s menus (southwest, west) and the magazine eject (north, only while a gun is held) during play. The first build took the first direction free at the moment it looked, west, and the spawn menu added there later covered it without an error. The entry now tries southeast first (never BONELAB’s, though LabFusion’s quick mute sits there), then north, and every second it moves off any direction another item has taken since. With LabFusion running and a gun held, every direction is taken: the entry shares north with the magazine eject, which wins it until the gun is put away, and the log says so once. Where the entry went is logged at every level (radial menu: Avatar scale added at, moved from). Pressing it draws a page of its own in place with PageView.Render, the way BONELAB draws the home page. A radial item has no kind (sub-page or action): a press runs its callback and nothing else. The first build switched pages with PageView.ChangePage, which nothing in the game calls and which plays the close animation first; that animation ends by deactivating the page view’s object, which stops the switch before the new page is drawn, so the press closed the whole radial. The press, the page drawn, and every close (by DH with the reason, or by the game with the page view’s state) are logged at every level. BONELAB’s radial gives every item one of eight fixed directions and decides the slice sizes itself (whole, half, third, quarter or eighth), so one item cannot be given half the wheel: Reset to 1x takes five neighboring directions instead (west through north to east), the biggest target the wheel allows, with Scale down (southwest), Scale up (southeast) and Back (south) below it. Scale up and down step through ScaleRadialPresets (0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10, 20, 50, 100), each label naming the size it goes to. While “Keep this size?” counts down, west and east become Revert and Keep and the entry reads Keep size?. Reset, Keep and Revert close the menu; Scale up and down leave it open. A second after each opening, the log says whether the page is still up and lists the slices the page view drew and their segment sizes.

Map safety. For MapTrapWindowSeconds (180 s) after any level loads, DH maps or stock ones, the mod counts every death of your own body (a Player_Health.Death that goes through, so not one the Invincible cheat refuses), every Health.Respawn of it, and every time a DH map puts you back at its spawn after a fall. A death and the respawn after it are one cycle: anything within MapTrapMergeSeconds (8 s) of the last one counted is the same cycle. A slow loop counts as much as a fast one; only the window bounds it. At MapTrapDeaths (3) cycles, “Having trouble?” asks on the same kind of panel, answered on the same radial page, and in a BoneLib notice, with No, I’m good and Get me out and a MapTrapCountdownSeconds (15 s) countdown. No, I’m good ends the watch for that level. Get me out, or no answer, loads MapTrapDestination, Void G114 (fa534c5a868247138f50c62e424c4144.Level.VoidG114), BONELAB’s big flat sandbox, and Void G114 again if that barcode is not installed. In a LabFusion lobby only the host may change the level: the host’s rescue takes the whole lobby along, and a client leaves the lobby (NetworkHelper.Disconnect) and loads Void G114 alone MapTrapLeaveSeconds (1 s) later. The question says which of the three it will do.

None of this has been tried in the game yet. The panel’s pointer hits rest on the copied raycaster and camera, which the build line logs; the radial menu’s sub-page and its slices are logged the first time it opens.

The spawn gun’s panel has three tabs, and each reads something different from a crate (read from the game’s code, disassembled, and its logs; the panel’s own source is not available):

TabReadsWhat DH sets
CategoriesThe crate’s Tags, against a TagTree the game builds (NPC has the children Humanoid and, for crates with no second tag, Other)Tags NPC and DigitalHeaven, then the source pallet’s own tags (furry, mayu; the generic avatar and assets are skipped); a tree node DigitalHeaven added under NPC wherever a tree is built (TagTree.SetupTagTree)
PalletsThe title of the crate’s pallet, one list entry per titleA pallet per source namespace, its title a path: DigitalHeaven/io.mltn. The mod nests the path (below), so the tab shows one DigitalHeaven entry that opens
AuthorsThe author of the crate’s pallet, whitespace removedThe source namespace, as in io.mltn, dev.pitr, poof

One naming rule. Grouping, pallet titles, pallet barcodes and the Authors tab all come from the source pallet’s id, never from the pallet header’s free-text author, which disagrees with itself (mltn on nearly every io.mltn.* pallet, io.mltn on io.mltn.test, which is why the Authors tab once listed both). The owner namespace is the first folder of the pallet’s place under Source/ (a pallet id is its folder path with dots, so io.mltn.avatars.mltn-mayu lives in Source/io.mltn/avatars/mltn-mayu): the longest top-level source folder the id is or starts with. dev.pitr belongs to dev.pitr, poof.avatars.poof-taidum to poof, tech.azuki.avatars.mayu to tech.azuki. There is no list of top-level domains. A machine with no matching source folder (a player who only has compiled pallets) falls back to the id’s first two segments when it has three or more, which is right for reverse-DNS ids and wrong for a one-segment owner such as poof; a namespace in the pallet header, which DH does not have yet, is where the mod already looks first. Both are in SpawnMenuNames.NamespaceOf, the only place the rule lives.

The Pallets tab nests, by patch. The stock panel nests only its Categories tab: that tab refills its list from the tag tree on every PopulateMenu, while the Pallets and Authors tabs fill a flat list once, in GenerateCategoriesMapping. What is not tab-bound is the corner tick on a button (UpdateTagPageItems enables it for any name that is a tree node with children), the back button and the path text. After the stock mapping, the mod collapses the DH titles into one DigitalHeaven entry (PalletNesting), selecting it lists the namespaces below it, with All as the first entry of every level listing that level’s crates, a corner tick on every entry that opens, the back button climbing a level and the path text naming where you are. The stock selection runs for every other entry. A failure anywhere leaves the flat list: the titles are paths, so it still reads DigitalHeaven/io.mltn. That is what run 48 showed, for two causes: the sort threw before the nesting could run (a postfix never runs after a throwing original), and the corner-tick patch on UpdateTagPageItems failed to install because Harmony binds arguments by the game’s names (tagPageIdx, not pageIdx). At the top the path text is the sort label (NEW or ABC, which SelectTab(3) writes), so the mod restores it there and swaps the sort button for the back button inside a level. Every open of the tab logs one [Menu] line at Quiet: nested N pallet(s) under 'DigitalHeaven' or nesting not applied: <why>. The Authors tab stays flat, one entry per namespace.

Rich text. The title and description are TextMeshPro rich text, and the mod switches it on for every spawn-gun field that shows crate text (the log says how many were off). One palette, MenuText, colors them: Hostile red and Friendly green (a variant’s title and the pane’s Disposition row), a body mode such as Player rig blue, the namespace violet, and the crate’s barcode, the first line of every DH crate’s description, bold amber.

The avatar crates stay in the DigitalHeaven.Avatars pallet, which holds no spawnable crate and so no longer shows in the spawn gun at all. Maps stay in DigitalHeaven.Maps.

The Pallets tab sorts through palletInstalledDates, which the panel fills once from the warehouse’s pallets. A pallet it never saw is a key the sort cannot find (KeyNotFoundException: The given key ' (SLZ.Marrow.Warehouse.Pallet)', run 45, twice), so before each category mapping the mod gives every warehouse pallet without a date one, and writes a date for every DH pallet its spawnable crates name whether or not a lookup finds it (the sort reads the dictionary natively, by the crate’s own pallet, and still threw in run 48 with every warehouse pallet reported dated). DH pallets also carry their title as the Unity object name; the game prints a pallet by it, and a pallet created without one printed as that empty key. The first repair logs a [Menu] line at Quiet naming how many pallets it dated, and a mapping that still fails logs an error naming each crate pallet with no date or missing from the warehouse’s own list.

Unlock every spawnable. The Advanced > Cheats row UnlockEverything (off by default, a safety switch, so it earns the yellow trail) lists every spawnable the game keeps behind progression in the spawn gun’s menu, for a player who has not unlocked the gadgets and weapons yet. The panel builds its lists in GenerateCategoriesMapping by running CrateFilters that ask PlayerUnlocks.UnlockCountForBarcode whether the save unlocked a crate. While the setting is on, a postfix on that call reports one unlock for a locked crate, and only while the mapping runs: nothing else that reads the count (gacha boards, placement gates) is spoofed, and the save is never read back or written, so turning the setting off restores the real list. A redacted crate keeps its real answer, so DH’s hidden NPC variants and SLZ’s internal crates stay out. Switching the row rebuilds the open spawn panels, like an NPC setting, so the items appear or vanish without reloading the map. LabFusion syncs a spawn by barcode, so a client spawning an item it has not unlocked should work (the unlock count is a menu-list check, not a spawn check); this is unverified in a lobby.

A diagnostic for turning avatars into NPCs, [NpcProbe], logs the first two spawns of each NPC crate: the target animator (humanoid, avatar, mapped bones), every skinned mesh and whether it and its root bone sit under the puppet’s target skeleton, the ragdoll muscles, the AI’s identity, and how many RigManagers the scene has. It adds a delegate to the puppet’s OnPostLateUpdate and logs how often it fires over 120 frames. When the spawn gun’s menu builds its categories, it logs how many spawnable crates it kept, grouped by unlock and redaction state, its category names, the pallets the kept crates come from, and what the DH pallet holds. The menu lists only spawnable crates, which the DH pallet now has through its NPC crates.

While a DH NPC entry is the selected crate, the info pane (SpawnablesPanelView’s title, pallet, author, description and tags, all 3D text with no row container) changes as follows, made once per pane (SpawnPane). The description becomes the entry’s state, Base NPC: Ford (default) or Base NPC: Security Guard (picked), always naming the base; the parenthesized note is smaller, dimmed and about 70% opaque (TextMeshPro <size> and an 8-digit <color>, MenuText.WithNote), and the Disposition row carries a note only once the arrows put it somewhere the base’s own disposition is not, Hostile (yours), so the row stays short enough to read at full size; the author line becomes the source namespace, dimmed, the pallet line is blank, and no barcode is shown. Rows follow under the description’s text:

RowValuesShown
Disposition ◀ Friendly ▶Hostile, FriendlyAlways
Body ◀ Ford ▶The offered body modesOnly while a development body is on
Cells Base NPC and Reset, side by sideAny installed NPC that can carry an avatarOnly for a body that builds on a base; the Player rig body shows Base NPC: n/a and no cells

Each row is a copy of the game’s own Options cycle row, the bloom row of the graphics page (Control_UI_InGameData.txt_bloom, from the nearest object above it that holds both arrow buttons): a label, a value and two buttons. Its presses are rebuilt through MenuButtons, the same helper the settings page wires its cells with (the click event replaced, since a copy’s persistent BUTTON_Bloom call survives RemoveAllListeners). The copy stands at the Options row’s own size times NpcPaneRowScale, centered under the description and shrunk to fit the description column’s width (SpawnPaneLayout, where the layout is decided), and a press is an ordinary EventSystem press, as on every panel of the menu. Each ◀ or ▶ press steps the entry’s choice (skipping the modes that are off), saves it (choices and variants) and points every spawn gun that holds the entry at the new variant. The words are NpcPaneText and the body modes’ own names, so the suite checks the text the pane shows.

Picking a base. The two cells are copies of the pane’s own list button (its icons and corner tick hidden), labeled Base NPC and Reset, side by side under the rows. A press on Base NPC enters pick mode (NpcPickMode): the cell is tinted and reads Cancel, the description says Pick a humanoid NPC on the left to be the base., and the list on the left switches to the stock Humanoid tag, a sibling of DigitalHeaven that holds only humanoid NPCs. The breadcrumb above the list reads Picking base NPC in the DH pink and the category column (All, Humanoid, Other, DigitalHeaven) is hidden; both come back on a pick, a cancel or a reset. A pick, a cancel or a reset switches the list back to the DigitalHeaven tag and selects the entry again; the menu closing or another crate being selected restores nothing. Tabs, pallets and paging stay the game’s. The next press on the list is caught by a prefix on SpawnablesPanelView.SelectItem that returns false, so the game never selects it and the pane and the spawn gun stay on the entry being edited; the crate is read from the selected list and page the way the game reads it. A crate that is not tagged NPC, or is a DH NPC, is refused at once with the reason on the pane, and pick mode stays. Any other is loaded (once per session, held like Ford) and checked (NpcBase.Unfit: a PuppetMaster, an AI behavior, a MarrowEntity and a humanoid target Animator, so a Crablet or a turret is refused); a fit one becomes the entry’s base, saved like any choice, and pick mode ends. The disposition follows the base: it is set to the new base’s own, Hostile when the aggression of its AI config is 0.9 or more (every hostile stock config has 1, Ford’s 0.499) and Friendly otherwise, read from the loaded prefab (NpcBase.DefaultDisposition); a base whose config could not be read leaves it as it was. The arrows override it afterwards. A base found unfit is remembered for the session. A press on Cancel, selecting another crate, or the menu closing leaves pick mode with nothing changed. Reset sets the player’s base back to Ford (and the disposition to Ford’s, Friendly), over any author’s default; the base line changes to Base NPC: Ford (default). DH NPC entries no longer carry the stock NPC Other and Other tags the panel’s mapping gave them, so they list only under DigitalHeaven and All. Bases other than Ford are as they are: their own AI, attacks and ragdoll sizes, and whatever they hold stays shown; they are untested in game. The pane is read from the game’s code; whether the copied row draws on the pane’s canvas at the right size is checked in game, and the first build logs NPC rows made in the spawn pane at Normal with whether a canvas sits above them, and spawn pane geometry with the pane’s rects in meters and its font sizes, then once each row’s and cell’s measured width against the column, and at Verbose both rows’ parts and scales.

Every log tag goes through one helper (Logs): a channel writes its tag and asks its category’s verbosity preference, Quiet (the default), Normal or Verbose. Errors and warnings are logged at every level. No diagnostic line was removed when the defaults went quiet; each was given a level. The categories are rows of the Config window and the in-VR page (Log: …), and lines from the DigitalHeaven libraries (the loader, the eye look, the springs) are routed by their own tag, or count as avatar build detail.

At startup, and whenever one of these settings changes, the mod logs one line at every level that lists each category, such as [Log] levels: Npc=Quiet Map=Normal PaperDoll=Verbose ..., so a log says what it could and could not show.

SettingTagsWhat Quiet keepsVerbose adds
LogNpc[Npc], [NpcProbe], [Gore]One line when the NPC crates register and one per NPC spawnHealth and motion reports, hits, punches, grips, per-muscle, per-collider and per-crate detail, the layer probe, the NPC probe, wounds per hit
LogAvatar[Avatar], [SlzAvatar], [Material], [Texture], [Renderer], [Fingers], [FingerFrames], [Rig], [Preview], [Build], [Swap], [Mirror], [Warehouse], [Barcode], [Handle], [Warmup], [Menu] and the DH loaderOne line when registration finishes, one per avatar build, one per swapPer-crate, per-material, per-texture, per-finger and soft-body detail, preview cache reads and writes
LogSpring[Spring]Nothing but problemsNothing more (every line is Normal)
LogFace[Face]Nothing but problemsGame events on a face, heads offered to eyes, the periodic eye health line
LogMouth[Mouth]Microphone opened or released, LabFusion server on or offEach clip spoken, the MouthTraceSeconds trace
LogSound[Sound]Nothing but problemsEach clip loaded, each slot filled, each sound voiced
LogPaperDoll[PaperDoll]The paper doll turned on or offEvery held-item decision, fade, linger and fragment, the dissolve probes, the render timings
LogCapture[Capture]One line per avatar or reference capture and per live capture done, and a hotkey ignored while loadingEach shot, its framing and each written file
LogMap[Map]The maps registered, each map built (time and spawn), each map releasedThe interception, the player marker, the rig’s placement, each diagnostic of a build, the repacked textures destroyed, each level menu’s flags, and the loaded mesh, material and texture counts after a release
LogFbt[Fbt]Everything: the probe is off by default, and its reports, calibration and restore lines are what a test run is forEach tracker that loses or regains tracking, and the foot diagnostics (from Normal)
LogInterface[Overlay], [Keys], [Preferences], [WorldMirror], [Lan], [Safety]A world mirror placed; LAN sharing started or stopped, a join from the Nearby list, the firewall hintSettings page layout and icons, mirror crate listings, each nearby server heard (Normal), each dropped packet (Verbose)
LogPerf[Perf]The counter log’s file, at startup; anything that stops itEach counter row as a line, and for each avatar: its read-ahead (models, textures decoded off the main thread and their upload time), its build by stage (DH build, rig, humanoid, LitMAS materials, SLZ avatar, hologram bake, with texture uploads, repacks and the change in process, texture and mod memory) and the loader’s own step timings, Import timing summary (Normal); the object census at each level start (Verbose)

[Level], [Settings], [Patch], the boot lines and [NpcTest] (Run NPC test) are not gated: each is one line for a level, a setting change, a patch or something a person asked for.

Three read-only [Rig] watches on your own body write at every log level, so a report from someone else’s machine carries them without a log setting changed (PlayerBodyWatch; the rules are game-free and tested). None of them changes anything.

  • Invisible contacts (BodyContactSeconds, 0.25 s). Each solid collider of your physics rig is overlapped against the world, and the depth is measured (Physics.ComputePenetration). A collider you are inside of that nothing draws (no enabled, active renderer under what it belongs to) and that rides a rigidbody, or that DH made, is named once per BodyContactRepeatSeconds: its path, what it belongs to (a DH NPC, another rig, a LabFusion player’s rig, a pooled object, an entity, or its root and scene, where DontDestroyOnLoad marks something kept across levels), which of your colliders meets it and how deep, and whether a LabFusion lobby is up. A static collider with no renderer is a level’s own collision hull and is never named. This is the line an invisible wall that pushes you leaves.
  • Body spread (BodySpreadSeconds, 1 s). The headset against the physics head and the avatar’s hips against the physics pelvis; a line when either passes BodySpreadMeters (0.3 m), again every BodySpreadRepeatSeconds while it stays apart, and once when it is back together. The avatar’s head bone against the physics head is in the line but never trips it, since the game moves that head for first person. A head that lags behind the person, or a body held off by something, shows here in centimeters.
  • DH collider census (DhColliderCensusSeconds, 60 s). The solid colliders of every live DH NPC and how many of them sit in an NPC nothing draws, the colliders on your rig’s head (SLZ’s own plus a head fit’s capsules), the armed grab handles and the paper doll copy’s colliders, written when a count changed.

A long session that slows down until a restart fixes it has something that only grows. The counter log writes one CSV row every CounterLogSeconds (60 by default, on at every log level) to UserData\DigitalHeaven\counters\counters-<session>.csv, one file per game launch. Each row is appended as soon as it is taken, so a crash keeps every row before it. A [Perf] line names the file once, at startup.

ColumnsWhat they are
time, uptimeMin, level, fusionLobbyWhen, how long the game has run, the level’s title, and whether a LabFusion server is up
frames, fpsMean, frameMsP99, frameMsMaxThe frames since the last row: their count, mean rate, the 99th percentile frame and the worst one
gameHeapUsedMiB, gameHeapSizeMiBThe game’s il2cpp heap (il2cpp_gc_get_used_size and _heap_size). Boehm never compacts, so the size rarely falls
modHeapMiB, modAllocMiBPerMin, modGen0..modGen2The mods’ own .NET heap, which MelonLoader runs beside the game’s: its size, its allocation rate and its collections since the last row
modAllocUnscopedMiBPerMin, modAllocTopScopesWho allocated: the rate outside every per-frame tick (Harmony patches, LabFusion callbacks, worker threads), and the five ticks that allocated most, each with its MiB per minute. Read from the profiler’s allocation tally, which counts each tick’s own bytes for two counter reads a tick and no allocation. The session gets the five as counters of their own, alloc <tick> MiB/min
unityAllocatedMiB, unityReservedMiBUnity’s native memory, as its release-build profiler counters report it
processPrivateMiB, processWorkingSetMiBThe whole process: committed memory and working set
vramUsedMiB, vramBudgetMiB, vramSharedMiBThe game’s own video memory and the budget the OS gives it, from DXGI on the Direct3D 11 adapter. Empty on another graphics API. A system monitor shows every process on the GPU, so it reads higher
textureMemoryMiB, textureDesiredMiB, textureNonStreamingMiB, streamingTextures, nonStreamingTexturesUnity’s texture memory and texture counts
objects, texture2D, renderTextures, otherTextures, meshes, materials, shaders, audioClips, gameObjects, components, censusMsEvery live Unity object, counted by kind, and how long counting took
idBurnPerMin, lastInstanceIdInstance IDs used per minute, and the counter’s position. Every object created at run time takes the next ID down, and the 32-bit counter never comes back up, so a mod that creates and destroys objects in a loop eventually breaks the game however few are alive at once
gcPins, pinnedHandles, warehouseHandlesObjects DH pins against the il2cpp collector (nothing unpins them yet), the GC handles behind them, and the handles that keep DH’s crates’ operations alive
npcs, npcPlayerRig, npcRig, npcCommunity, npcSkeleton, gore, face, springs, touch, sounds, materialsConverted, avatarScale, fusionFx, dollItems, palletNesting, diagnosticsSeenThe entries each part of DH holds across levels

Counting the objects reads Resources.FindObjectsOfTypeAll without making a .NET wrapper per result. A wrapper holds a strong il2cpp GC handle until .NET finalizes it, so a census made of wrappers would itself be the largest handle leak it measured. Instead the census reads each result’s class pointer and its m_InstanceID field from il2cpp memory, after checking that field’s offset against the ID Unity reports for one object. The walk is the counter log’s one hitch, and censusMs measures it.

Object census per level. With CensusAfterLevel on (the default), CounterCensusDelaySeconds (10) after each level starts, and never while it loads, a census of every live Unity object by exact type goes to the session file, and a [Perf] line gives its total, its change since the last census and the five types that grew the most. A level that leaves its objects behind shows as the next level’s census starting higher. With LogPerf at Verbose the census is also written beside the counter log as census-<session>-<n>-<level>.csv, one row per type with its count, its change since the previous census, and its lowest and highest instance ID, and [Perf] logs the fifteen types that grew the most.

Each row also goes to the log as one [Perf] counters: line at Normal.

The live session. With DiagnosticsSession on (the default), every reading is also appended to a session file, UserData\DigitalHeaven\diagnostics\session-<time>.jsonl, which the Halcyon Diagnostics viewer follows as it grows. The file carries more than the CSV:

  • Marks for each level start and each LabFusion server joined or left, so a chart says what happened where it bends.
  • Every census, by exact type, with its id range, so the viewer can diff any two and rank the types that only grow.
  • Profiled frames, only when asked. Every system the mod ticks each frame runs under a profiler scope named after it: DhNpcs, BonelabSounds, FusionFx, FbtNet and the rest. The work that runs from the game’s own callbacks has scopes too: NpcPuppet (each DH NPC after its puppet’s pose, with NpcPuppet.springs and NpcPuppet.face inside it) and ArtPose (each player rig after its art pose, the local one and every remote one, with ArtPose.springs and ArtPose.face). Each span carries the managed bytes its tick allocated, which the viewer’s tree shows as alloc KB per frame. A tick whose time comes in rare spikes far above its median is usually paying for a garbage collection someone else’s allocations started, and the alloc column tells the two apart. MapLevel on a voxel map breaks down further: voxel image pump, voxel submit (with voxel plan inside it), voxel land, voxel animate, voxel collision sync and map navmesh on the main thread, and the worker threads’ own tracks: voxel mesh region, voxel open and voxel light on the meshing threads, and the voxel collision threads that cook colliders. The viewer’s Profile button turns them on for a few seconds, streams the spans a batch a second, and turns them off again. It never stays on by itself, since every frame’s spans would be hundreds of megabytes an hour.

There is no socket. The viewer writes what it wants (a census, a mark, a profile) as a line in session-<time>.requests beside the session, and the mod reads that file twice a second, starting after whatever it held when the game started. A census on request waits while a level loads and for CounterCensusDelaySeconds after it starts, and the session notes that it is waiting. With CensusTimer on, one is also taken every CensusTimerMinutes (10) under the same rule. It is off by default, because a census is a short hitch, measured in censusMs.

Every DH map in the workspace, additionalPalletDirectories and the engine’s content folder is offered as a BONELAB level, in singleplayer. It is listed in the level select like any level, shows its name on the stock loading screen, and is unloaded by the game when you leave. The mod has no menu of its own for it.

  1. Registration. With the avatars, once the asset warehouse is ready, the mod registers a second Marrow pallet, DigitalHeaven.Maps, with one LevelCrate per map. The barcode is DigitalHeaven.Maps.Map.<pallet>.<path>, a pure function of the map’s DH barcode (hashed when it is too long or two maps would meet). The crate’s title is the map’s file name alone, as in the Schedule I Maps window. The crate’s main scene is a made-up key that Addressables never resolves. The crate carries the DigitalHeaven tag, and the Mod tag when MapPanel is Mods.

  2. Interception. Selecting the level runs the game’s own load. The loader asks SceneLoadQueue.AddLoad for the crate’s scene and the mod keeps the request from being queued. When the loader then reads the level’s instance with SceneLoadQueue.GetInstance, right before it calls SetActiveScene on the result, the mod builds the map into a scene of its own with the shared Unity map builder, puts a PlayerMarker at the map’s spawn, and stores the scene in the queue’s instance table as if Addressables had loaded it. It reads the stored instance back and checks the scene is valid, so a scene the game would reject is caught here instead of hanging the load. The rest of the stock load (loading scene, music, player rig, event system, plugins) runs unmodified. No stock level is loaded under the map.

  3. Placement. The loader reads the marker’s position but ignores its rotation, so once the rig exists the mod teleports it to the spawn, facing the way the map’s spawn faces. A kill floor puts you back at the spawn if you fall below the lowest collider by MapKillFloorDrop, since nothing in the loader respawns anyone.

    Cleanup volumes. The game keeps an ObjectCleanupVolume (a 5 km wide, 100 m thick trigger centered at y -275) under every level to delete what falls out of the world, and LabFusion teleports the local player to a checkpoint when they touch it. A map built far below the origin has its floor inside that volume: gm_flatgrass sits at y -312, so the player was thrown up and back down in a loop, standing on the volume and never on the map. When the rig is placed, any cleanup volume that covers the spawn and reaches above the kill floor is moved down to 1 m under the kill floor, and put back when the map is released. The move is logged at Summary level.

  4. Failure. A map that cannot be built, or whose scene the instance table does not keep, is logged as a [Map] error at every log level. The loader is handed an empty scene so its load finishes, and the mod then loads the stock level the player came from (else the Void G114 hub, else the main menu), logging where at the same level and showing a BoneLib notification with the map’s name and the first line of the reason when BoneLib is loaded. A load that stalls inside the game for longer than MapLoadTimeoutSeconds (120 s) is abandoned the same way, rather than sitting on the loading screen.

  5. Unload. The game unloads the scene when the next level loads, the same UnloadSceneAsync it uses for every level. The mod notices, destroys the textures it repacked for the map and closes the map’s pallet sources. The meshes, materials and textures the builder made are owned by a component on the map’s root and are destroyed with it.

Materials go through the same path as avatars: the DH loader builds each material, then BonelabMaterials moves it onto SLZ/LitMAS/LitMAS Standard with its albedo, normal map, a packed MAS map and emission, so a map renders with the game’s own shader rather than pink. The repacked normal and MAS textures belong to the map and are destroyed with it, unlike an avatar’s.

A scene made at runtime has default render settings and no lights, which is why maps first rendered flat and dark. The shared map builder now makes the light the map authors, in the map’s own scene (MapLighting, on by default):

Map contentIn BONELAB
lighting sun (direction, color, intensity)One directional light. A map with no lighting block gets the engine’s default sun. Shadows follow MapSunShadows
light entities (point, spot, directional)One Unity light each, at its position plus offset, aimed down its local -Y, with its range and cone. Lights authored off are skipped. Up to four times MapMaxLights are made, and only the MapMaxLights nearest your head are on at once, chosen again as you move (a map with 128 lights and a limit of 64 keeps the 64 around you lit, not the first 64 it lists). A light authored baked is lit in real time, because lightmaps are not applied
ambientSky, ambientGround, ambientIntensityUnity’s three-color ambient (sky, the mean of the two, ground) with the intensity folded in, written to the scene’s render settings the first frame the map’s scene is the active one
ReflectionsOne realtime reflection probe at the spawn, rendered once when the level starts (MapReflectionResolution, MapReflectionProbeSize). Without it, metallic surfaces reflect nothing
skyImageA cubemap built from the compiled sky, set as the map scene’s skybox (RenderSettings.skybox) on SLZ/Skybox/SLZ Cubemap, the shader every BONELAB level with a sky sets its own skybox with. The game’s pipeline draws the scene’s skybox itself (SLZ’s URP replaces the stock skybox pass with a procedural triangle drawn under DRAW_SKY_PROCEDURAL at the start of the transparent pass), for every camera that clears with the skybox, the rig’s Headset camera among them, and the skybox shader ships that draw’s stereo instanced variant. The map’s yaw and exposure are in the cube; the material’s _SkyColor is white, _Rotation 0 and the fog toggle off, as the levels leave it
No image sky: procedural sky, a map with voxel volumes, or noneA generated sky set as the skybox the same way as skyImage (see below). halfLambert, falloff, lightmaps and the GI volume are not applied
backdrop (Source’s 3D skybox)BONELAB has no pass to draw a backdrop in, so its parts are built where they stand in the map, a miniature room far from the playable area, and its records are left out. Their renderers cast no shadows, as in the engine, whose shadow pass never sees a backdrop. Built in place with shadows (two-sided, like every map renderer), gm_construct’s room, a 740 m slab of ground and buildings 270 to 318 m above the map, shaded the whole map out to the sun’s shadow distance: the map looked dark until you were far enough from it (fbt10)
FogNone unless MapFogDistance asks for some, set on BONELAB’s own volumetric fog (see Fog)
Masked materials ("alphaMode": "mask": foliage, decals)The game’s URP Lit (MapCutoutShader) with _ALPHATEST_ON, depth written, no blend, queue AlphaTest and RenderType TransparentCutout, which is how BONELAB’s own foliage is set up (ClimberPlant, Medieval_Foliage_Ivy_1k: URP Lit, _ALPHATEST_ON _NORMALMAP [_METALLICSPECGLOSSMAP], queue 2450). The base map binds as it is with the authored cutoff, the normal map under _NORMALMAP, and a metallic-smoothness map (metallic in R, smoothness in A) repacked from DH’s metallicRoughness under _METALLICSPECGLOSSMAP. The forward pass ships its alpha test only as _ALPHATEST_ON, _NORMALMAP _ALPHATEST_ON and _NORMALMAP _ALPHATEST_ON _METALLICSPECGLOSSMAP, so a material with a metallicRoughness map and no normal map turns _NORMALMAP on too and samples the shader’s flat default; a test checks every set the route can make against the installed shader’s stereo variants, and the shadow, depth and depth-normals passes for their alpha test. The build ships no URP Lit forward variant with an occlusion map, so occlusion is not bound, and none with emission beside the alpha test, so a masked material that emits stays blended through LitMAS from a clipped copy of its map. One [Map] line per map at every log level counts each route

The colors a map authors are sRGB face values with a separate linear intensity, and Unity’s light and ambient colors are the same, so they pass through unchanged and the intensities are used as they stand. MapSunIntensityScale, MapLightIntensityScale and MapAmbientIntensityScale match them to the game’s exposure. A map’s lighting is one [Map] line at every log level: the sun, how many lights were made, the ambient, the probe and the sky. The skybox shader is found the way every game shader is (see Game shaders), from MapSkyboxShaderKey, whose default is the asset path of the game’s SLZ/Skybox/SLZ Cubemap (its skycubemap shader bundle); the log says the map has no sky if it cannot be had. The skybox goes on the map’s scene the first frame it is the active one, when only the loading screen’s cameras exist, so the report waits for the player rig: once it is placed, a [Map] line at every level says which skybox it is, whether its cube is bound (the write is verified by name and id, since a texture write by name does not always stick under Il2CppInterop), whether the active scene still holds it, and which world cameras clear with the skybox, the only ones the pipeline draws it for. It warns when the cube is not bound, the scene holds another skybox or none, or no camera clears with it.

A map with no skyImage gets a generated sky (MapSkyGenerated) on the same skybox shader, so its sky is not black. A map that draws the procedural sky, or holds voxel volumes and names no sky, gets a day gradient (MapSkyZenith, MapSkyHorizon, MapSkyGround) with a halo and disc where its sun is; any other map gets the flat clear color the DH engine draws behind it (MapSkyFlat). MapSkyIntensity scales both. The [Map] lighting line says which sky was made and why. When the skybox material cannot be made (the shader is not found, or a map is loaded before the update loop has preloaded it), the game’s cameras clear to the sky’s horizon color instead (the generated sky’s horizon, or an image sky’s mean color a little above its horizon), with a warning that names the cause, and the colors are given back when the level ends. The cameras there when the map is built are the loading screen’s; the player rig’s head camera comes later, so every half second the backdrop also reaches any camera made since, and says so once with each camera’s clear flags and eye. Run 52 left the headset black that way. The fallback now sets SolidColor clearing as well as the color on game base cameras, including a later headset camera. URP overlay cameras retain their depth-only behavior. Both original color and clear flags are restored when the map unloads.

BONELAB’s fog is volumetric. The rig’s Headset camera carries SLZ’s VolumetricRendering, which scatters light through a froxel grid every frame, and SLZ’s URP reads the fog’s density from its Volumetrics volume override. Every stock level carries a global volume that sets it (Descent’s mean free path is 120 m, the Hub’s 60 m, Container Yard’s 300 m); the scene render settings’ fog is off in most of them. A DH map’s scene had no volume, and the persistent GameplaySystems volume sets only post effects, so the game fell back to the override’s defaults: a 50 m mean free path at and below 0 m that thins to a 1 km haze by 50 m, its color taken from the sky cube the game’s SkyManager falls back to rather than the map’s sky. That was the fog over every DH map (fbt10), thickest on maps built around 0 m such as gm_construct and mltn-city. All of this was read from the installed level and rig bundles.

A DH map has no fog of its own: the engine draws none, and a Source map’s env_fog_controller is only reported. So the mod gives the map’s scene a global volume with the Volumetrics override at priority 1000, above every stock level’s (0 to 20) and below the game’s load fade (Load Fade Fog, 1e9), so loading still fades to black. MapFogDistance is its mean free path in meters, 0 (the default) for none. MapFogBaseHeight and MapFogTopHeight place it, and it takes the map’s sky colors. One [Map] line at every log level says which fog the map was given. Once the rig is placed, another says whether the headset camera reads the volume’s layer, what fog the game resolved and which other volumes set fog. A volume on a layer the camera does not read is moved to one it does, with a warning. Another line counts the renderers that cast the sun’s shadows and the backdrop ones that cast none, with each set’s box.

A shader none of the game’s loaded materials uses is not in memory. It sits in its own deduped bundle (deduped_assets_shader/skycubemap_*.bundle, lit_*.bundle) that Addressables loads only as another asset’s dependency. It is still an entry of the BONELAB pallet’s catalog (catalog_BONELAB.json), keyed by its asset GUID, with its asset path as the location’s internal id. The path is not a key, and loading by it failed with InvalidKeyException: No Location found for Key=Assets/_SLZPackages/Shaders/Sky_Far.shader (run 52). The mod preloads the configured reference, skybox, cutout and unlit shaders from OnUpdate once the warehouse is ready. A setting holding one of the four known asset paths is mapped to that shader’s catalog key (GameShaderKeys.CatalogKey, checked against the installed catalog by the tests); anything else is located as a key. Nothing walks a catalog’s keys: reading them through Il2CppInterop crashed the game natively, first during a map load (fbt7, the generic enumerator) and then at boot (the non-generic one, IEnumerator.Current under ShaderLocations).

Addressables.LoadAssetAsync<Shader> starts each request; later updates check IsDone before reading the result. Nothing calls WaitForCompletion or loads a bundle synchronously. Successful handles stay owned for the session; failed or wrong-name loads release their handles. Catalog changes allow unresolved requests to try again. A shader with no catalog entry can still be taken from loaded shaders and retained across unused-asset sweeps.

Map and material builds only read this cache, including when called from the native scene streamer. If a build precedes shader readiness, it uses the existing fallback for that build; the log announces when the real shaders become ready for subsequent builds. Wait for those lines before reloading a map to inspect its real sky and cutouts. BONELAB names URP Lit Universal Render Pipeline/Lit (PBR Workflow); the old Universal Render Pipeline/Lit setting remains an alias. Other mismatched shader names are refused.

Which source supplied each shader is one [Material] line at every log level, such as shader 'SLZ/Skybox/SLZ Cubemap' from the catalog, 'Packages/com.unity.render-pipelines.universal/Shaders/Sky/SkyCubemap.shader' by asset path, bundle deduped_assets_shader/skycubemap_….bundle. A shader that cannot be had is a warning, once per name and key, naming why each source failed. One line then counts the loaded shaders and the catalogs’ shader locations, and both are listed at Verbose. A test reads the installed game’s catalog and checks that each default path is the internal id of exactly one shader entry, is not itself a key, and has its bundle on disk. The headless suite also reads the installed bundles with a test-only AssetsTools.NET dependency to verify the actual shader names and stereo vertex/fragment variants and alpha-tested stereo fragments; no game binaries are included or copied. Runtime Addressables loading and headset rendering still require a VR check.

BONELAB streams its own textures within a memory budget, and a texture the mod uploads uncompressed is charged to that budget for as long as the map lives: after a large map, items in your hands drew at low resolution. A map’s textures now go in block compressed (DXT) with their CPU copies dropped, the repacked normal and MAS maps too (MapCompressTextures), and a source map that only fed a repack is destroyed once the materials are converted. The repacks are made no larger than MapRepackMaxEdge (1024), which the GPU does while it reads them. Their per-pixel math runs on all cores. With LogMap at Normal, the log prints the game’s texture memory (resident, not streamed, what streaming wants and targets, the budget) before the build, after it and after the map is released.

The map’s baked companions (lightmap, GI volume, reflection cubemaps) are read only when something asks for them, so a game that lights the map its own way no longer decodes a lightmap it throws away. The build line splits the time: opening the pallet and decoding the geometry, the scene (with its meshes and materials, how much of that was texture uploads and how much of those was block compression, the colliders, the lights and sky, and the voxel setup), and the conversion with its readback, packing, upload and compression. A normal, metallic-roughness or occlusion map is only read back to be repacked, so it is uploaded uncompressed and compressed once, as the repack; compressing it on upload as well cost most of the seconds a large map added.

Colliders are built by the shared builder (MapColliderShape) on the physics layer MapColliderLayer, which is 0 (Default) unless you change it. The game’s MarrowLayers names no dedicated static-world layer; 6 (Fixture) is the nearest and is a setting for an install where Default does not hold you up.

A stock level is more than its geometry, and a map built at runtime gets the three parts of it that decide how it plays from the mod. Status is ✅ works, 🟡 partly, ❔ untested in the game.

PartStatusWhat the mod does
Grabbing walls, rails, pipes and edges❔MapGrabConvexColliders
NPC navigation❔MapNavMesh
Inventory and ammo carried in and out❔MapInventory

Grabbing. A hand grabs the world through WorldGrip.ValidateGripScore, read from the game’s compiled code. It overlaps a sphere around the hand with every layer but Player, skips every collider with a rigidbody, and keeps the one nearest the hand by Collider.ClosestPoint. A box, a sphere or a convex mesh answers with a real distance. A concave MeshCollider cannot answer it, so the game uses the hand’s own position, which is distance zero, and the first concave mesh the physics query returns beats every other collider in reach. It then raycasts that one collider along the hand; a miss means no grab. A map made of one concave collider per node therefore gives the grab to whichever node the query lists first, usually the floor, and the ray misses the wall, the rail or the pipe the hand is on. That is why the floor grabs and little else does.

With MapGrabConvexColliders on, the mod splits each node’s collider mesh into its connected pieces and gives every piece that is a closed, convex solid (watertight, no face with vertices on both sides, at least five times MapGrabTolerance thick, at most MapGrabMaxHullVertices distinct vertices) a convex collider of its own. Such a piece collides exactly as its mesh did. What is not convex, such as a room’s shell, stays one concave collider, and a hand on it still loses a tie to nothing but another concave mesh. One [Map] line per load says how many colliders were made convex and whether the hands’ hoverLayerMask includes the map’s layer.

Navigation. BONELAB’s levels carry a baked navmesh and a map built at runtime has none, so every NPC stands still. With MapNavMesh on, once the rig is placed the mod collects the map’s colliders with NavMeshBuilder.CollectSources and bakes one mesh per agent type the game defines (NavMesh.GetSettingsByIndex, so a stock NPC’s agent finds the data for its own type) with NavMeshBuilder.UpdateNavMeshDataAsync, which runs on Unity’s worker threads. The data is added with NavMesh.AddNavMeshData and removed when the map is released. A map wider than MapNavMeshMaxSize bakes a square of that size around the spawn. The [Map] log reports the sources collected, the time, and whether the spawn lies on the mesh. The stock BehaviourBaseNav needs nothing beyond the mesh to roam. A map has no zones, so ZoneAggro never alerts an NPC and doors with NavMeshLinks do not exist; an NPC notices the player by sight as it does anywhere.

Inventory and ammo. A campaign level’s BonelabGameControl restores the holstered items and ammo from the active save at the end of the load (BonelabProgressionHelper.RestoreInventory and RestoreAmmoCounts) and writes them back when the level ends, both under the level’s key. A map has no such component, so the player arrives with empty slots. With MapInventory on, the mod arms the same restore on the stream session’s level-load callback and saves through SaveInventoryInProgress and SaveInProgressAmmoCount as the next level starts to load, under MapInventoryKey (DigitalHeaven.Maps), one key for every DH map. It sets no completion, no progress and no hub flag; the save gains only the key’s inventory and ammo values, in memory until the game next saves. Set MapInventoryKey to a campaign key such as Hub to start with what that level saved. Leaving by quitting the game does not save.

A map’s voxelVolume entities (a Minecraft world, a .vox model) stream in around you, as in Schedule I. The streaming is the shared UnityVoxelScene in DigitalHeaven.Unity.Content, which the Schedule I mod uses unchanged; this mod supplies what is the game’s: where you are, the log, the materials and the convex colliders.

PartWhat the mod does
DrawingRegions mesh on worker threads and land within a per-frame upload budget, up to MapVoxelRadius meters from your head, and are dropped when they leave it or the mesh budget (MapVoxelMeshBudgetMb) runs out
CollisionChunks within MapVoxelCollisionRadius meters of your feet are solid, on the map’s layer MapColliderLayer; the one under your feet is cooked on the spot. Each chunk’s closed convex pieces get convex colliders of their own, with the rules of MapGrabConvexColliders, so a hand finds a block it reaches for (see Playing a map). The cook runs on the worker thread
MaterialsThe opaque and translucent atlas surfaces of each volume go onto LitMAS as they are made, with screen-space reflections off (a matte block only gains a view-dependent horizon from them), non-metallic, at the volume’s roughness (0.9) and with no emission falloff. One [Map] line per volume, at every log level, names each surface’s shader, blend, metallic, smoothness, emission and queue. The atlas is bound as it is, never repacked or compressed, because the streamer animates it in place and keeps its own point-filtered mip chain. A translucent surface (water, glass, ice) is blended after the opaque ones and writes no depth
Cutout blocksPlants, torches and fire are a real alpha test: the game’s URP Lit (MapCutoutShader, loaded by MapCutoutShaderKey) with _ALPHATEST_ON, depth written and no blend, because LitMAS has no alpha clip; a map’s masked materials take the same route (Maps). The build ships no URP Lit variant with both alpha clip and emission, so a volume with glowing tiles draws the glow again over its cutout faces: the game’s unlit shader, additive and alpha tested by the glow atlas, scaled by MapVoxelGlowScale times the voxel emissive intensity. Opaque glowing blocks (lava, glowstone, lamps) glow through LitMAS emission. The log says once which shader the cutouts took, and warns when they fall back to blending through LitMAS. That fallback is what run 52 drew, and it is why its cutouts shone: a transparent LitMAS surface raises its alpha toward 1 by a Fresnel term, so the clear part of a leaf or torch quad turns opaque and reflective at a grazing angle
LoadingThe terrain around the spawn is made on the loading screen for up to MapVoxelPrewarmSeconds, so you are put on ground and not above it
Kill floorThe map’s floor is lowered to the lowest point of any voxel volume that has opened, so walking into a valley does not put you back at the spawn
NavigationThe navmesh is baked from the chunks standing around you and baked again once you have moved MapNavMeshRefreshMeters, since the terrain that exists changes as you walk. A map that has not streamed any terrain yet waits and tries every second
UnloadThe streamers stop and every region, collider and atlas is destroyed when the map is released

The block textures of a platform pallet (the Minecraft pallet’s own) are decoded on the main thread with the mod’s managed decoder, because the game’s bindings cannot read pixels back from a texture. MapVoxels = false leaves a map’s volumes out.

With LogMap at Normal, a line every MapVoxelLogSeconds reports the regions resident, how many loaded and unloaded since the last line, the mesh memory, the colliders and the main-thread cost per frame. A voxel map whose spawn sits inside the streamed terrain raises you onto it (up to MapSpawnClearMeters) and says so in the build line. Material properties a shader lacks are warned about once each, with one [Map] line counting the repeats left out. At every level the log says when a volume opens, when the terrain around you has settled, how the prewarm went, and, as an error, if streaming stopped.

In run 45 the level select had no tabs: the 11 DH maps (15 were registered) appeared in the one list, mixed in with every other level. That is how it stays until a tab exists. The first rebuild of each level menu logs one [Map] line at every level, level menu '<name>' rebuilt: <n> level(s), <k> of <m> DH map(s), which says how many of the registered maps that menu listed.

The DigitalHeaven tab is not in the stock menu. A tab for the DigitalHeaven tag needs a BonelabLevelsPanelView with filterByTag on and levelTags set to DigitalHeaven, plus a tab button that shows it, and both live in the menu scene’s hierarchy, which the mod would have to clone at runtime. That is not built, so MapPanel defaults to Mods: the crates carry the Mod tag and sit on the mods tab (and on any tab that takes every external level). Choosing DigitalHeaven removes the Mod tag and lists the maps nowhere until such a tab exists. With LogMap at Normal, each level menu logs its flags and tags when it rebuilds, which is what a future tab is cloned from.

By default no stock level is loaded. If that fails on an install, MapUseShell makes every map crate name the stock level MapShellBarcode (Baseline, SLZ’s empty test level) as its scene. The game loads that level as it would any other, and once the rig exists the mod builds the map beside it and turns off the shell’s renderers and colliders, leaving its scripts running. This path is untested and shows the shell for a moment before the map. Both settings need a restart.

Limits: lightmaps and the GI volume are not applied (the map is lit in real time, so a baked scene looks flatter than in the engine), the procedural sky is drawn as a generated gradient, not the engine’s atmosphere, entities other than spawn points, box brushes, lights, object instances (see Unity → Maps) and voxel volumes are not built, no thumbnails in the level menu (a level crate has no preview slot), and static batching is never used because it reads renderers back as arrays, which the game’s bindings do not offer. A map’s first load converts its materials, which takes longer the more textures it has.

BONELAB’s multiplayer is the community Fusion mod, read by reflection with no dependency on it: without it DH loads and runs as before. Fusion sends the barcode of the crate the local rig wears (two frames after a swap) and the avatar’s measured stats, and a receiving client builds that barcode itself, loading the crate’s asset directly and never through the game’s RigManager.SwapAvatarCrate. A DH avatar crate is registered at boot but built on demand, so DH hooks Fusion’s RigManagerExtensions.SwapAvatarCrate and builds the crate (the same build the local swap uses) before Fusion asks for its asset. Peers with the same DH pallets then see each other’s DH avatars, with the sender’s synced scale. A peer who lacks the pallet sees Fusion’s PolyBlank (or Fusion downloads the avatar, when its download setting is on and the crate comes from mod.io), and DH logs one warning naming the barcode. A DH avatar’s mouth follows its player’s Fusion voice (see Mouth).

Fusion plays a remote player’s death, dying, recovery and jump through that rig’s own HeadSFX, which DH already hooks, so a remote DH avatar answers a death with its died reaction and closes its face. A lobby that is not Mortal (LabFusion’s default) knocks a player out instead: it sends the dying event and never a death, so DH closes the face on dying too (KnockoutClosesEyes), on your own rig and a remote one, and a recovery or respawn (DH also hooks Health.Respawn, which Fusion calls on a remote rig) puts it back to revived. Fusion sends no footstep, no landing and no hit on an NPC, so DH works those out on each machine (see the table, and what each machine runs). The person’s own rig is never replaced by a networked one as the rig DH treats as the player’s.

Every row assumes the same DH pallets on every machine and the same DH build. Status is ✅ works, 🟡 partly or by design, ❌ not available, ❔ untested; nothing below has been played in a lobby yet, so ❔ marks what LabFusion’s source and the code say should work.

FeatureStatusNotes
DH avatar shown to other players❔The barcode goes through Fusion’s own avatar sync; the receiving side builds the crate on demand (FusionAvatars)
Your avatar kept in a lobby❔The lobby’s Custom Avatars permission can put your rig on PolyBlank; DH warns once ([Swap])
Avatar scale, height and mass❔Fusion copies the sender’s measured stats onto the remote avatar, and the same build measures the same; a lobby’s maximum avatar height rescales through the same swap
Missing pallet on the other side❔Fetched from the wearer (see DH content between players); PolyBlank with LabFusion’s progress bar until it arrives
Spring bones on a remote avatar❔Bound after every SwitchAvatar, whichever rig it is, and stepped in the same ArtOutputLateUpdate hook
Eye look and blinks on a remote avatar❔Same bind and step; remote heads are look targets for every avatar, and so is yours
Mouth from a remote player’s voice❔Fusion’s received packets, only for an avatar with visemes; VisemesForRemotePlayers off leaves the jaw to Fusion
Your mouth in a lobby❔Fusion’s capture of your microphone replaces DH’s, which is released
Death, dying, recovery, respawn on a remote DH avatar❔Driven from Fusion’s calls to the rig’s HeadSFX and Health.Respawn; the face closes and reopens through the same path as your own. A knockout sends only “dying”, which closes the face (KnockoutClosesEyes). DH also reads the player action itself and, when the face did not follow, closes or opens it and logs a [Face] warning at Quiet
Snout and skull colliders on your rig and a remote one❔The [Player rig] NPC head fit (NpcHeadFit.FitRigHead) runs NpcRigHeadFitDelay after a DH avatar is put on any player rig, yours (PlayerRigHeadFit) or a peer’s (RemoteRigHeadFit), growing SLZ’s head to the skull and snout so a hand or shot here meets it. LabFusion copies the sender’s synced head ellipses over a rep’s avatar when the game refreshes its measurements; the fit follows the swap’s own refresh
Jump sound on a remote DH avatar❔Fusion’s jump action calls remapHeptaRig.Jump() on the rig, and the game’s RemapRig.Jump calls HeadSFX.JumpEffort (read from the game’s compiled code). DH checks, at Normal, that it did, and voices it itself when it did not ([Sound])
Hit sound on a remote DH avatar❔Fusion tells every machine “dealt damage to another player” with the victim’s id (only while friendly fire or the gamemode allows the hit), and DH plays the victim rig’s HeadSFX pain vocal for it. The amount is not sent, so it is always the small pain (RemoteHitDamage)
Footsteps on a remote DH avatar❔Fusion sends no footstep. DH counts the ground the rig’s pelvis covers while it is moving and plays the rig’s own FootstepSFX every RemoteStepDistance, standing down for any rig the game steps itself (RemoteReactions)
Hard landing on a remote DH avatar❔A fall of at least RemoteLandSpeed that stops suddenly in the pelvis’ motion plays the avatar’s landedHard clip at the feet, unless the game voiced one itself (PhysGrounder.HighFallClipSpawn)
Blood and wounds on a DH NPC another player shoots❔Fusion makes the shooter’s gun fire again on every machine and each machine’s own bullet marks the NPC. When another player’s shot has marked no DH NPC on this machine after RemoteShotWindow, DH hits the NPC along the same ray itself, with no damage (RemoteShotWounds)
dh.speakSound and the avatar’s sound slots on a remote avatar❔The slots are written onto the remote rig’s HeadSFX and FootstepSFX at every switch, so the game’s own vocals play them
Materials and shaders on a remote avatar❔One conversion per template, shared by every copy
Mirror reflection of a remote avatar❔Fusion makes a mirror per player; springs and face copy by the mirror’s rig
Hologram, menu preview, paper doll, world mirror🟡They show your own avatar. A networked rig is never taken for yours
[Ford], [DH rig], [DH anim] NPC spawned by anyone❔Fusion syncs the variant’s barcode; each machine builds the NPC crate on demand in AssetSpawner.Register, and a variant on a base this machine never chose is made from its barcode first, in a prefix on LabFusion’s CrateFilterer.HasCrate (the base must be installed here). The owner’s physics drive it; other machines get its pose
[Ford] NPC fights remote players❔Stock AI, which senses every player rig’s TriggerRefProxy
[Player rig] NPC❌the pooled object is an empty marker with no MarrowEntity, so Fusion syncs nothing of it: each machine builds and runs its own copy, and its brain knows one player, you
DH map as the lobby’s level❔The crate is in every client’s warehouse, so Fusion loads it by barcode through SceneStreamer.Load and DH builds it
Lobby on a DH map you lack❔Fetched from the host while you wait in LabFusion’s downloading level (see DH content between players)
Map props that react across players❌A built map has no MarrowEntity to sync
Level changes in a lobby❔Per-level work follows your own rig only, never a networked one
Joining a nearby server from the Enter Code page❔Both players need DH; see joining a server
Lobby players in DH’s Social window❔Every LabFusion player, by SteamID64 and LabFusion name, is listed in the overlay’s Social window once a second (FusionPlayers), and one who leaves moves to its disconnected players; works whether the others have DH or not, so a player can be added to the DH friends list from there. [Lan] logs each join and leave at Normal
Full-body tracking: other players’ legs and hips❔Both players need DH; the host may lack it. Visual legs from the solved rotations, feet pulled onto the sent ankles, the pelvis through SLZ’s tracked spine on the remote rig, and every late-joiner case (see full-body tracking over LabFusion). Checked headless through the echo harness; not yet played in a lobby

LabFusion downloads a missing level, avatar or spawnable from mod.io only: it asks the player who has it for the mod.io listing, and a DH crate has none, so for a level the joiner used to be disconnected (“The server’s level failed to install!”). DH takes that request over for DH barcodes (FusionDownloads, a prefix on NetworkModRequester.RequestAndInstallMod) and fetches the content from that player through DH’s shared transfer stack (sharing pallets with other players). Both players need DH.

  • What comes. The pallet that holds the map or avatar, with its dependencies; anything you already have with the same bytes is skipped. A level comes from the host, an avatar from its wearer, a DH NPC from its spawner (it brings its avatar, and the avatar’s NPC entries register with it).
  • The pipe. Requests, answers and consent ride DH’s LabFusion messages. The bytes ride DH’s own Steam P2P connections to that player (virtual port 17480, through the Steamworks LabFusion already runs), TransferStreams of them in parallel, so a transfer never fills the connection LabFusion’s own messages share. Each connection’s send buffer and rate ceiling are raised (TransferSendRateMbps). Until they connect, or if TransferStreams is 0, the ranges go through LabFusion’s connection, a few at a time. Only a player in the lobby may connect. Every 5 s while bytes move, a [Transfer] line gives the rate in and out per player and Steam’s own status of one connection (its route, ping and rate estimate), which is how the pipe is measured.
  • While it comes. A level holds you in LabFusion’s own downloading level, its bar showing what, from whom, bytes, rate and time left. An avatar shows PolyBlank with LabFusion’s progress bar over the player, then swaps. The desktop overlay’s Transfers window lists every pallet either way.
  • Asking first. Content from someone on your DH friends list (dh-friends.jsonc) comes automatically. From anyone else, a LabFusion notification asks Accept or Decline; after an Accept a second one offers to add them as a DH friend. LabFusion’s own Download Levels / Avatars / Spawnables switches and its Max Level Size and Max File Size still apply.
  • Game content. A pallet imported from a game (Half-Life 2, Garry’s Mod, Counter-Strike, ULTRAKILL, Minecraft…) only comes to a player whose Steam account owns that game; otherwise the request is declined with the reason, and the game can be imported from your own copy. Most of a Source map’s size is usually its game’s pallet (gm_flatgrass is 8.6 MB beside 1.8 GB of Half-Life 2).
  • After it lands. The new maps and avatars are registered at once under the barcode the other player used, every level menu is rebuilt (closed ones too), and LabFusion carries on: it loads the level, swaps the avatar or spawns.
  • Your own copy wins. A pallet you already have under the same id but with other bytes is kept, and named in the log.

[Lan] (LogInterface) logs each request, its answer, each pallet installed and any refusal at Normal, and at Quiet what LabFusion asked for and how it ended. The Steam P2P lanes (listening, up, closed with Steam’s end reason) are logged at Quiet too.

LabFusion 1.14.2 logs nothing when a connection ends: a client sees only that the server is gone, and a host that drops a client (a kick, or ValidateReceivedID refusing a message whose claimed sender does not match its connection) says nothing. FusionDiagnostics hooks those paths and logs at Quiet:

  • [Lan] [Disconnects]: every way out of a lobby (InternalServerHelpers.OnDisconnect, with the reason and the managed callers), a lost Steam connection on either side with Steam’s end reason and debug text, a host closing a player (DisconnectUser), telling one they are disconnected or refusing a join, and a refused message with the first bytes of it.
  • [Lan] [Catchup]: on the host, each joiner’s catch-up of existing entities (how many creations were sent); on the joiner, a pre-existing spawn that finished while LabFusion knew neither its owner nor the host, which is what makes CatchupManager.RequestEntityDataCatchup throw.

LabFusion 1.14.2 offers no Steam join or invite. It never sets Steam rich presence or a connect string, its Steam lobby is created invisible (it only carries the metadata the lobby search reads), and it runs Steamworks under SteamVR’s app id (250820) after shutting the game’s own down. Right-clicking a friend in Steam therefore shows no Join Game, and no setting changes that. Inside LabFusion there are two ways in:

  • Browse. Fusion > Matchmaking > Browse lists Public servers, and Friends-only servers to the host’s Steam friends; the Friends Only filter on the left narrows it to friends. A new server is Public unless its host changed Location > Privacy. The search asks Steam for at most its default 50 lobbies before the friends filter runs, so on a busy day a friend’s server can fall off the list, though Steam sorts by distance and a friend on the same network is near the top.
  • Enter Code. Fusion > Matchmaking > Enter Code: the host reads the code under Location > Code. The Join button calls NetworkHelper.JoinServerByCode, which finds the lobby by its LobbyCode key and connects to the host through Steam’s relay.

Nearby. With DH on both machines, the Enter Code page also lists the LabFusion servers hosted on your local network, under the Join button, one copy of that button each: Nearby: Poof, Hub, 2/8, with the host’s LabFusion version added when it differs from yours. A press puts the code in the field and calls the same JoinServerByCode, so it is exactly typing the code and pressing Join, and the game still connects through Steam’s relay. While nothing is heard the list shows Nearby: no servers heard yet.

  • Host. While you host a server that FusionLanSharing shares (the Share my LabFusion server on LAN row on the Actions / Multiplayer page), DH sends a UDP broadcast every FusionLanIntervalSeconds to port FusionLanPort, both to the limited broadcast address and to each network adapter’s own broadcast address. Automatic (the default) shares a Public or Friends-only server and not a Private one, Always shares a Private one too, Never shares nothing; a Locked server is never shared.
  • What is sent. The server’s code, your LabFusion name, the level, the player count and maximum, and LabFusion’s major and minor version, in at most 171 bytes (FusionLanPacket). Nothing else: the code is what people read aloud anyway. Nothing is sent past the local network and no outside service is involved.

Clear spawned. The Clear spawned row on the Actions / Multiplayer page (also in the overlay’s Config window) removes every spawned object at once: NPCs (stock, [Ford], [DH rig], [DH anim] and [Player rig]), guns, magazines, items and props, which is every pooled object in the world. It leaves the map and its fixtures (circuit sockets), every player’s rig, anything a hand holds and anything in a holster slot. The first press arms it (the row reads Press again to clear); a second press within 3 seconds clears, so a stray press does nothing. The log’s [Clear] line counts what went by kind (NPCs, guns, magazines, props and items) and what stayed by reason. Offline it just works. In a LabFusion lobby only the host may press it, and a client’s row reads Host only: the host calls Poolee.Despawn on each object, the same call LabFusion’s own admin Despawn All makes (PooleeUtilities.DespawnAll, host only, which walks the registered props), so LabFusion’s prop entities carry each despawn to every client. DH walks every live Poolee instead of LabFusion’s registry so it also reaches the DH NPCs that LabFusion does not sync, which each machine runs its own copy of: those are cleared on the machine that presses, and a client keeps its own copies until it clears them offline or leaves the lobby.

  • Joiner. DH listens only while the Enter Code page is open. It accepts a packet only from a private, link-local or loopback address and only when it is exactly the format, and drops every other one. A server stays listed while it keeps advertising and drops FusionLanExpirySeconds after its last packet (FusionLanList). At most FusionLanShown servers are listed.
  • Windows Firewall. The first time the listener opens, Windows may ask on the desktop whether BONELAB may receive on the network, which is invisible in the headset. Until it is allowed, the list stays empty. After three expiry spans of hearing nothing, DH logs a [Lan] line at Quiet saying so; allow BONELAB once under Windows Security > Firewall & network protection > Allow an app through firewall. Sending needs no rule.

[Lan] (LogInterface) logs at Quiet when sharing starts and stops (with why), a join from the list, the firewall hint, and any failure to listen; at Normal each server heard; at Verbose each dropped packet and each time the list is drawn, with the size of LabFusion’s button grid.

A mirror mod and LabFusion. BetterMirrors copies every item and NPC that enters a mirror’s zone and destroys the copy’s MarrowEntity and Rigidbodies after LabFusion has registered the copy as a prop; LabFusion’s NetworkProp then throws in CopyBodiesToPose on every frame for as long as it lives, and the game slows with each such copy. DH sweeps for these props (FusionSweepSeconds) and unregisters them, naming each in a [Npc] line at Quiet. A [Npc] live DH NPC records line (NpcCensusSeconds) shows the records DH holds for NPCs, so a leak shows at any log level.

LabFusion 1.14.2 does not send a hit on an NPC. A bullet an NPC takes is the machine’s own: the shooter’s gun is made to fire again on every other machine (GunShotMessage), each machine’s bullet marks what it hits, and the shooter’s machine alone moves the NPC’s ownership to the shooter (ImpactUtilities.OnHitRigidbody). Only the owner’s machine applies damage (SubBehaviourHealth.TakeDamage is refused elsewhere), so blood and wounds are decided by where each machine’s own copy of the shot lands. DH wounds are painted through VisualDamageReceiver.ReceiveAttack on whichever machine receives the hit; with LogNpc at Verbose each hit line says whether this player or another shot it. The remote-shot fallback above covers a machine where the replayed bullet marks nothing. A remote player’s pose is synced but their footsteps and landings are not, so those two are derived from the pelvis’ motion by one shared tracker (RemoteMotion) that also follows your own rig, to log the speed the game’s own landing happened at ([Sound], Normal).

The [Swap] category logs, at Normal, each barcode Fusion sends for the local rig, each avatar a remote player announces and each remote rig that now wears a DH avatar; at Quiet it warns when the rig’s DH body sits under a different crate than the one Fusion sends, when a remote player wears a DH avatar this install lacks, when Fusion fails to put one on a remote rig, and when Fusion puts your own rig on PolyBlank in place of a DH avatar. [Npc] logs, at Normal, each DH NPC Fusion spawns and, at Quiet, a warning when one is spawned before its base has loaded and once per [Player rig] session that it is not synced. [Map] logs, at Normal, the DH map a lobby loads and, at Quiet, a warning when the lobby’s map is one this install lacks.

BONELAB’s native tracker input is repaired by the mod; SteamVR Manage Trackers must assign each tracker its role. Enable Trackers, restart, then press Calibrate. No body targets, foot hold, physical legs, hips or floor correction run before a successful calibration for the current rig and avatar.

The Full-body tracking page has eight rows and one sub-page:

RowDefaultDescription
TrackersOff, restartEnable SteamVR tracker roles; restart, then calibrate before they drive your body
CalibrateAction, 3-second countdownStand straight with feet under your hips and arms out; press again to cancel
Clear calibrationActionForget the saved calibration, restore the original rig and stop tracker-driven body motion
Keep calibrationOnA calibration survives level loads, avatar swaps and restarts until you clear it or calibrate again (see the kept calibration)
Debug and takes → Debug markersOffShow tracker, target, physics and art markers
Debug and takes → Mirror followsOffKeep the calibration mirror visible and following you
HipsOnThe waist tracker drives the pelvis, so sitting and lying down follow it
Locomotion animationOnBONELAB’s walk animation plays on your legs while the stick moves you. Off: the legs keep following the trackers while the body slides (Visual only and Visual + kicks: the visible legs never hand over to the game’s stepping legs; Physics with the foot hold: the legs are held on the trackers; the physics legs of Visual + kicks are left alone)
Leg modeVisual + kicksHow tracked legs are shown: Visual only (the legs follow the trackers and nothing else changes, for telling IK problems from physics ones), Visual + kicks (the same, and a fast kick takes its physics leg along) or Physics (the game’s physics legs chase the trackers, as before)
Leg mode for other playersSame as mineHow other DH players’ tracked legs show in a LabFusion lobby (see full-body tracking over LabFusion): Same as mine, Visual only, Visual + kicks or Physics
Debug and takes → Capture tracker snapshotActionSave three views of the body, the markers and a floor tile (no menu, prop or held item), and the tracker, joint and rig measurements
Debug and takes → Record movementActionRecord your head, hands, trackers and buttons every frame for replay; press again to stop. The row shows the seconds recorded

Under Advanced → Tracking, two rows apply at once, so a lobby can tell the pelvis’s rotation from its position without a restart:

RowDefaultDescription
Hips rotationOnWith Hips: your pelvis takes the waist tracker’s rotation; off, only its position follows (FbtHipsRotation)
Yaw onlyOffWith Hips rotation: only the tracker’s heading, on the game’s tilt (FbtHipsYawOnly)

Calibration saves original target references, puck and target poses, calibration flag and pelvis gimbal state before writing anything. It lifts pucks to each part’s height to shorten the rotation lever arm, captures the offsets, and publishes calibration only on success. Clear, tracker input disabled, avatar/rig change or failed calibration restores the saved state and releases foot/physics ownership. Recalibration first clears the previous capture; canceling its countdown leaves the stock rig.

Torso capture: native OpenControllerRig.CaptureTrackedOffsets uses a fixed quaternion (-.5, .5, .5, .5) for pelvis/chest. That frame is unsuitable for arbitrary Vive tracker mounting. The mod replaces it with inverse(puck.rotation) * procedural.rotation and measures the position in the puck frame, including scale. It evaluates SLZ’s procedural chest/pelvis at capture and rejects a reconstruction error over 2 cm or 2 degrees. Regression tests replay the saved waist pose, including turned/scaled world frames. The fbt7 VR test also confirmed aligned hips, rocking, sitting and lying down, so hips are on by default; their native spine gate and dropout fade are unchanged.

Visual legs (Visual only and Visual + kicks): what the player sees is the avatar’s own skeleton, Avatar.artTransforms, not SLZ’s ArtRig. In the disassembly, RigManager.LateUpdate runs PhysicsRig.OnLateUpdate, which calls ArtOutputUpdate and tail-calls ArtOutputLateUpdate; that copies every ArtRig.art* bone’s world position and rotation onto the avatar’s (64 SetPositionAndRotation calls), and ArtOutputUpdate never rewrites the art* leg bones, which ride the physics limbs. The first build wrote the art* bones: nothing showed the same frame, and every later frame copied a leg pose that never recovered, even after Clear calibration (fbt9). The mod’s postfix on ArtOutputLateUpdate now places the avatar’s thigh, shin and foot, which the copy rewrites every frame, so the legs show that frame and nothing stays behind. Each bone is placed outright, position and rotation: the hip from the avatar’s hips and its offset measured at calibration, the knee and ankle from the shared TwoBoneIk with the bone lengths measured at calibration, so a physics leg left far from the pelvis cannot pull the visible leg apart. The bone frames come from LimbBasis in DigitalHeaven.Core, as the built-in locomotion’s do; each leg bone’s child axis and knee-forward are measured in its own frame at calibration, so no bone axis is assumed. The knee bends toward the knee tracker when one is tracked; otherwise toward the pelvis’s front plus the foot’s forward and up. While the stick walks the body faster than FbtLegWalkSpeed, the visible legs ease back to the game’s stepping legs and return when the walk stops; every hand-over eases over FbtLegFadeSeconds. In Visual only nothing else is touched: no foot hold, no physical legs, no floor clearance. In Visual + kicks a kick starts only from a foot tracker moving at least FbtKickSpeed (1 m/s) as it rises FbtKickHeight (0.15 world meters) above its calibrated standing height, with the other foot down, the body on the ground and the physics pelvis at least FbtKickMinPelvis (0.8) of its standing height; it lasts at most FbtKickSeconds (1 s) and ends below half the height. A raised foot at rest never kicks: in fbt9, sitting with a foot up held a physical leg for 47 s and pushed the body backward. The floor clearance only lifts a kicking foot. Other DH players in a LabFusion lobby see these legs and the hips (see full-body tracking over LabFusion). Physics leg mode is the previous behavior below.

In a game mirror. Mirror.WriteTransforms poses its reflection from SLZ’s physicsRig.artOutput bones (artUpperLegLf and the rest), never from the avatar’s, so legs placed on the avatar after ArtOutputLateUpdate would show on the body and not in the mirror, yours and other players’ alike. After that call, the reflection takes the thigh, shin and foot local pose of each leg placed this frame or the last (FbtVisualLegs.CopyToReflection); the reflection is a clone of the same avatar, so local poses carry over.

Feet and floor: foot hold still bypasses Footstep.PreUpdate with _freeOffsetT = 1. Lift now means raw tracker rise from its calibrated standing height, scaled into world space, rather than the planner’s offset target height. Entry is FbtLiftThreshold leg lengths (0.03); release is half that. This avoids falsely holding a planted foot. While the mod owns physical legs, both legs are physical, as the game’s own PhysicalLegs makes them: setting the planted limb kinematic on its own (PhysLimb.SetKinematic frees its joints and zeroes its drives) left it frozen in the world while the body moved on (fbt9). Clear calibration puts the legs back to kinematic and any limb whose mode disagrees with the rig’s back in step, and its Quiet line names what it restored. Legs already made physical by the game are left alone. All collision contacts stay enabled.

For a lifted tracker, eight corners of the physical foot collider are projected onto its requested pose. Downward rays find nearby static/kinematic floor, excluding the player and dynamic props; the foot target gets enough vertical clearance before SLZ’s leg solve. A finalizer restores its calibrated offset afterward. FbtFloorClearance defaults to 1.5 cm and FbtFloorProbeMeters to 20 cm. This replaces all five floor modes, collision-ignore pairs and drive-strength overrides. It is intended to prevent low-kick scraping without removing prop contacts; standing stability, floor transitions and kicks still require VR validation.

Reach and scale: fbt7 captured a raised remap foot 0.994 m above the floor with the physics foot at 0.724 m, and several folded physical knees. All nine snapshots had zero added floor-clearance lift. Those observations do not establish a joint limit, friction, self-collision, or double scaling as the cause. Physics reads virtual skeleton rotations, after remapping and interpolation, rather than directly following the remap foot endpoint. Raw animation/remap heights belong to different proportional frames.

Snapshots now include each rig’s hip/knee/foot poses and scale, avatar measurements, the real player’s measurements and saved height, the actual virtual servo target, current joint twist/swing and target rotation, and geometric self-overlaps with pair/layer/joint collision filters. An overlap is not proof of a solver contact. These distinguish an already folded requested pose from physics failing to reach it; no force or joint-limit change is inferred from the old snapshots.

BONELAB already has a player-height setting: Control_Player.SendHeight converts saved centimeters to meters and BodyVitals.CalibratePlayerBodyScale uses avatar eye height divided by the real player’s estimated eye height for tracking-space scale. Use the game’s height setting before calibrating. Its RealHeptaAvatar.SetPlayerWingspan and SetPlayerInseam methods are empty in this build despite saved fields for both, so a new arm-span policy would require an actual remapping implementation. There is no second DH height field or unverified scale multiplier.

The calibration mirror appears only for a calibration you start with the Calibrate button: during the countdown and for FbtCalibrationMirrorSeconds (1.25 s) afterward, independent of Debug markers. A kept calibration put back on its own (after a level load, an avatar swap or a scale change) never shows it, and neither does the re-calibration an avatar scale change starts when no calibration is kept. Mirror follows keeps it visible. Debug markers retain puck axes, calibrated targets, physics pivots, art feet and floor contact markers; paper-doll rendering remains available. Capture tracker snapshot, on the same page, saves front/side/top pictures and snapshot.json under UserData/DigitalHeaven/fbt-snapshots.

Tuning stays in the cfg. Periodic tracker/foot diagnostics default to disabled; calibration and failures still log at Quiet. To investigate a pose, enable FbtDiagnosticsInterval and LogFbt = "Normal", or capture a snapshot. Existing cfg values are retained; removed FbtNativeBody, floor-mode and drive-strength entries are ignored.

What SLZ’s solve does with each role, read from the game’s compiled code:

RoleRead by UpdateHeptaBodyNotes
Both feet✅Feet are wired together or not at all, as the solve needs both
Knees✅Only with both feet wired
Elbows, shoulders✅Bend the arm solve
Waist, chest🟡The tracked spine runs only while XRApi.Body (Meta’s body tracking) is tracking with permission, so on its own a waist puck leaves the pelvis procedural. The hips pass that gate for the call; the chest rides only with a waist

SLZ’s AssignTrackingPucks is not called: it copies Meta body-tracking bones into the tracker transforms and returns at once without them. LabFusion itself sends no legs and pushes a remote pelvis toward the sender’s position, but turns it only while ball locomotion is off, so without DH on both sides other players see the game’s procedural legs and an upright pelvis. Nothing writes RemapRig.

A calibration lasts until you press Clear calibration or calibrate again. A level load, an avatar swap, a setting that re-wears your avatar (Avatar scale), a lost tracker map and a game restart all take the live calibration off the rig, and each is followed by the kept one being applied again with no countdown and no T-pose, as soon as the rig and avatar have held still for FbtRestoreSettleSeconds. A fresh calibration is saved to UserData\DigitalHeaven\fbt-calibration.json (FbtKeepCalibration off keeps it in memory only), and Clear calibration deletes the file.

What is kept. For each wired role: the target’s offset under its puck (the tracker’s position and rotation relative to the body point it stands for, in tracker space), the puck’s lift and its raw standing height; plus the hips’ standing height, the visual legs’ ankles, and the height of the avatar they were measured on. The offsets are the tracker’s mounting on your body, so they are not scaled. The lift and the hips’ height are the avatar’s part heights, so a different avatar re-fits them: the body measure is Avatar.height, the part’s height scales by the new avatar’s height over the saved one’s (FbtCalibrationRefit), and the raw tracker height stays. Avatar scale changes the avatar’s height, so it re-fits the same way. The saved calibration is never overwritten by a re-fit one, so swapping back and forth does not drift it. Whether the ratio places the legs right on avatars of very different proportions needs an in-game check; calibrating again on the new avatar always replaces it.

Which trackers. A tracker reaches the game only as an OpenXR role (waist, left foot, …); the serial stays in SteamVR, which assigns it to the role. The kept calibration therefore matches by role. It applies once a role it holds is tracking; when only some are, it waits FbtRestoreGraceSeconds for the rest, then applies without them (the game’s own rules still need both feet or neither). A tracker the calibration holds nothing for is left unwired rather than measured out of a stance. With none of its trackers on it waits, and a calibration that fails to apply is not tried again until the rig, the avatar or the set of tracking roles changes.

Strapped differently? A tracker re-mounted after the calibration was made no longer matches it; press Calibrate (or Clear calibration first) to measure again.

Untested in a lobby Two DH players in a LabFusion lobby see each other’s visual legs and hips. DH sends its own messages through LabFusion’s module messages (FusionMessages): LabFusion tags a ModuleMessageHandler subclass by a hash of its assembly and type names, so DH builds that subclass at runtime with Reflection.Emit, with the same names on every machine and no compile-time dependency on LabFusion. A host without DH still relays them (LabFusion 1.14.2’s NativeMessageHandler.Handle relays before handling, and an unknown module tag passes PreRelayMessage), and a player without DH drops them silently, so any two DH players share their bodies whoever hosts.

What is sent. Nothing at all while the trackers are off or no calibration is wired (FbtNetGate). The moment the gate opens, the calibration goes to everyone, reliable, and again after every new calibration. The moment it closes (Clear calibration, Trackers off, an avatar swap, a recalibration’s countdown) one Stopped notice goes out, so receivers let go at once instead of at the timeout. While it is open, on LabFusion’s own 20 Hz tick and unreliable, a pose of at most 57 bytes, stamped with the sender’s clock, goes to the other players (FbtNetPacket):

PartContents
PelvisThe target the hips wrote (already blended by the sender’s weight and rotation settings), in play space, and the weight
Each visual legThe solved thigh, shin and foot rotations relative to the avatar’s hips, the ankle in play space, the leg’s weight, and whether the foot is down (within FbtLiftThreshold leg lengths of its standing height), kicking or held
Calibration (once)The avatar’s barcode, each leg’s bone frames, hip offset, thigh and shin lengths, and an epoch the poses repeat

Play space is the headset’s parent, the frame LabFusion sends the head and hands in, so the feet stay in step with them. Positions are 1 mm in 16 bits, rotations the smallest three components at 10 bits each (about 0.1 degree). This follows what VRChat and ChilloutVR send, the solved pose rather than tracker targets.

What a receiver does. It shows each body between the two pose ticks around a moment FbtNetDelaySeconds (0.075 s) behind the newest, by the sender’s stamps (FbtNetTimeline), as VRChat and ChilloutVR do; a tick that arrives after a newer one is dropped. The first echo run eased toward the latest tick instead, the way LabFusion moves the head and hands (NetworkTickManager.InterpolationTime), which steps at the 20 Hz tick; FbtNetBuffered off brings that back. The pelvis runs through the same tracked-spine path as the sender’s own hips (FbtHips): for that rig’s UpdateHeptaBody only, its trackedPelvis is a DH target at the sent pose, eased in from its own procedural pelvis, its waist puck stands there too, and _hasAssignedOffsets and XRApi.Body are lent and put back. So the remote physics body bends, sits and lies down, not only the picture. The legs are rebuilt after ArtOutputLateUpdate from the sent rotations on this machine’s copy of the avatar (FbtLegMath.Rebuild), from its own hips, then a two-bone fix pulls the ankle onto the sent ankle: all the way for a foot that is down (FbtNetPlantedCorrection), lightly for one that is up (FbtNetFreeCorrection), easing between the two over FbtNetCorrectionEaseSeconds (0.15 s) as a foot is put down or lifted rather than switching with each tick, so feet never float or sink when that rig’s pelvis sits differently here, and a raised leg keeps its shape. A rig wearing another avatar than the calibration names keeps the game’s legs, with one warning. No pose for FbtNetTimeoutSeconds (0.5 s) eases the body back to the game’s.

Late joiners. Every player missing the current calibration gets it:

CaseHow
You are calibrated and join a serverThe gate opens when the server does, and the calibration goes to everyone
You are calibrated, host, and someone joinsEvery calibrated player sends it to each player who joins (MultiplayerHooking.OnPlayerJoined)
You recalibrateThe countdown sends Stopped; the new calibration goes to everyone with a new epoch
You swap avatarsThe swap clears the live calibration (Stopped); the kept calibration, re-fit to the new avatar, goes to everyone with a new epoch and the new barcode once it applies (see the kept calibration); without one, the next calibration does
A kept calibration applies (level load, restart)It announces itself like a fresh calibration: the gate opens and the calibration goes to everyone with a new epoch
A receiver missed it anywayPoses naming an epoch it lacks make it ask the sender (Request), at most once per FbtNetRequestSeconds

Leg mode for other players (FbtRemoteLegMode) picks how their legs show here. Same as mine follows your own Leg mode. Visual only and Visual + kicks draw their legs; a kick lands through the kicker’s own body, and LabFusion shows what it hits (it already syncs the physical-legs switch a kick makes). Physics leaves their legs to the game, as LabFusion shows them; their hips still follow. The kick and lift flags and the per-rig state are there for a later step, where a remote’s physics legs follow the synced pose, so a kick also hits what this machine simulates and a grabbed leg pushes back.

What to look for in the log ([Fbt], Normal): LabFusion: DH messages registered, shared body: calibration N for '<barcode>' to everyone (…), shared body: sending poses on the LabFusion tick, shared body: player N's calibration …, shared body: player N's legs on rig '…' follow what they send and shared body: stopped. A failure to register or send is at Quiet. With FbtDiagnosticsInterval set, a line per other player gives their calibration, fades and why their legs are or are not drawn. A second line per other player, shared body: player N's pelvis on rig '…', gives headings about the play space’s up, measured from that rig’s head: the received pelvis target, this rig’s procedural pelvis, and after SLZ’s solve the pelvis, the chest and the physics pelvis. So a pelvis that arrives turned, a solve that turns it and a physics body that does not follow each show as their own number.

The headless harness cannot run two players: LabFusion 1.14.2 has only Steam-backed network layers. The echo (-Echo in replaying a take) runs every message through the real pack, unpack and receive path onto a second rig beside the player, and tests each late-joiner case. LabFusion’s transport itself is checked by hand: two Steam accounts on two machines in one lobby. The hips put back the controller rig’s feet center, and the foot hold switches the physics legs.

Record movement writes one take per press-and-press-again under UserData/DigitalHeaven/fbt-recordings/<timestamp>/, so a movement can be replayed into the game later without anyone in the headset. Each take holds:

  • header.json: the format name and version, the mod’s build commit, the game and Unity versions, the level (barcode, title, scene), the avatar (barcode, title), where the player stood and faced at the start, the game’s player height, wingspan and inseam with the real player’s and the avatar’s measurements, every DigitalHeaven preference as the cfg writes it, both controllers’ types, the calibration as the take started (each role’s tracking, wiring, puck lift, standing height and target offset, the hips’ standing height and the visual legs’ calibrated ankles), and every calibration or clear during the take with the frame it happened at.
  • frames.bin: a 24-byte preamble (DHFR, version, frame size and the device, controller and bone counts), then one fixed-size little-endian record per frame, about 1.7 KB, so 9 MB a minute at 90 frames a second.
  • remotes.bin: only when a LabFusion lobby was up, and only with FbtRecordRemotes on (the default): every other player, see below. header.json lists them under remotes.
  • start/ and stop/: a tracker snapshot (the three pictures and snapshot.json) at each end.

A frame is read every frame the game runs, at two points. After OpenControllerRig.OnEarlyUpdate (so a calibration that frame is already in it): the frame time, the tracking space’s world pose and scale, the RigManager’s world pose, then the headset, both controllers and all ten tracker roles from XRApi as the rig reads them (connected and tracking flags, the play-space pose, the same in world space, linear and angular velocity), and both controllers’ stick, touchpad, trigger, grip, finger curls and every button with its down and up edges. Then after ArtRig.ArtOutputLateUpdate (after the visual legs wrote): the physics pelvis, feet and locomotion ball, the controller rig’s pelvis, the avatar’s own hips, thighs, shins and feet (Avatar.artTransforms, what the player sees), the calibrated pelvis and foot targets, and from version 2 the avatar’s chest, head and upper arms, all in world space. A frame’s flags mark the calibration countdown (the T-pose), a calibrated rig, the frame a calibration finished and whether the body was read.

Nothing runs while no take is open; a frame is encoded into one preallocated buffer and streamed through a 1 MB file buffer. The header is written at the start too, so a take cut short by a crash still reads to its last whole frame. A take stops when the row is pressed again, when the level or the player rig changes, or after FbtRecordMaxSeconds (600). The start and stop are logged at Quiet as [Fbt] recording movement to '...' and [Fbt] recording stopped (...): N frames over S s.

The format is FbtRecording.cs, free of game types and round-tripped by the test suite. A reader refuses a version or layout it does not know; a new layout is a new version number. Version 2 appended the upper-body bones at the end of each frame, so a version 1 take still reads, with those bones absent. Version 3 left the frames alone and added remotes.bin, so a version 1 or 2 take reads with no other players.

In a LabFusion lobby, Record movement also records every remote player, so a bug seen in a real lobby (the remote chest twist, a leg that sticks) can be replayed from what this machine actually received and showed. remotes.bin (FbtRemoteTrack.cs, FbtRemoteRecorder) is a preamble (DHRM, version) then variable records, each a kind byte, a byte count and a payload, so a reader skips a kind it does not know and stops at a record a crash cut short:

RecordWhatWhen
Rig sampleThe player’s headset, both controllers and the controller rig’s pelvis, each as the local pose under their play space (what LabFusion writes) and in world space; the play space, the controller rig’s root and the RigManager in world space; the remap’s crouch and feet offset; and the physics pelvis plus the avatar’s hips, chest, head, upper arms, thighs, shins and feet in world space, read after LabFusion posed the rig and FbtNet drew their legs from their messages, so the sample is what this screen showedFbtRecordRemoteHz (30) times a second per player, 681 bytes each, about 20 KB a second per player; 0 is every frame
MessageEvery DH full-body message from them (poses, calibrations, requests, stops) whole, as it arrived, with the take’s clock and game frameOn arrival, from the top of FbtNet.Receive, always
AvatarTheir avatar barcode, its height and the scale of the copy on this machineWhen first seen and whenever any of the three changes

The calibrations this machine already held when the take began are written first, as messages at time 0, so a take started mid-lobby still has what the receiver had. The clock is the take’s own: a remote record and the local frame at the same moment carry the same time. Rigs are matched to LabFusion small ids once a second while a take is open; nothing runs with no take open or with FbtRecordRemotes off, and a failure writing a remote stops only the remotes. The take’s Remotes header entry lists each player, their first avatar, how many samples and messages they have and when they were first and last seen. fbt-replay remote <take> prints the same from the file.

Platforms/DigitalHeaven.Mods.Bonelab/tools/run-headless.ps1 -Recording <take> -ModBuild <mod build folder> plays a take back into the game on mltn’s machine with nobody in VR, so a full-body tracking change can be tested and compared on the same movement every time. -LaunchCheck only checks that a level is reached.

A run stops early when its setup is broken, and the exit code is the whole answer: 0 is a good run, 2 means the guard or the restore tripped, 3 means the setup was broken (the avatar did not build or is not the one worn, a material fell back to an error or loader shader, or the decal shader did not load). A pass prints one preflight ok line; a failure prints the reason and the log lines that caused it. preflight.json in the report folder is for tools. -Avatar <barcode> overrides the take’s avatar, and -AllowFallback, -AllowDecalFailure, -AllowWrongAvatar or -SkipPreflight turn conditions off for a run that tests a fallback on purpose.

What the run does, and what it never does:

StepDetail
Refuses to startwhile BONELAB, SteamVR or any program from a Steam library runs
RuntimeUnity’s OpenXR mock runtime (mock_runtime.dll, Unity Companion License, installed locally under %LOCALAPPDATA%\DigitalHeaven\tools\mockxr, never committed), selected with XR_RUNTIME_JSON for the game process only. The system’s runtime is never read or changed
Parked for the runsteam_api64.dll, every mod but DigitalHeaven and every MelonLoader plugin; with -ModBuild the installed DigitalHeaven mod and libraries, replaced by that build
Saved and put backMelonPreferences.cfg, the save games, Player.log, Player-prev.log, MelonLoader’s Latest.log and Logs
LaunchBONELAB_Steam_Windows64.exe directly (never steam://), windowed, put behind every other window once and never touched again
Live guarda SteamVR process or a second BONELAB stops the run at once; a run is never retried
Afterwardevery parked and saved file restored and checked by SHA-256, even after a failure; any SteamVR process the run caused is stopped and reported

The harness plugin (DigitalHeaven.Mods.Bonelab.Harness) is copied into Plugins for one run and removed after. It is never part of the mod’s build, deploy or release, and does nothing without DIGITALHEAVEN_HEADLESS=1. On mltn’s own copy it skips SteamClient.Init, so the game’s ownership check (MarrowEntitlement.CheckSteamEntitlementAsync, which passes when Steam initializes without throwing) passes with Steam parked. It turns off Valve’s refresh rate feature, since XRApi.InitializeXRLoader otherwise waits forever for an extension the mock runtime lacks. It makes the headset count as worn, since the rig pauses the game otherwise, and it mutes the game.

The replay itself is FbtReplay in the mod, off unless DIGITALHEAVEN_FBT_REPLAY names a plan (replay.json, FbtReplayPlan):

  1. It sets the take’s preferences in memory before anything reads them, then any the run overrides (-Settings @{ FbtLegMode = "physics" }).
  2. Once the boot level has streamed in, it loads the take’s level, puts on its avatar and stands the player where the take began.
  3. Every frame it writes the take’s headset, controller, tracker and input values into XRApi’s devices after XRApi.OnPostNewInputUpdate, and again just before OpenControllerRig.OnEarlyUpdate, since the mock runtime’s own devices update in between. The whole rig then runs as it would live. Button edges come from what was held the previous frame. Game time steps one physics step a frame (Time.captureFramerate at 1/Time.fixedDeltaTime, 120 in BONELAB), so the pictures cannot change the pace and no frame takes an uneven number of physics steps, and each game frame plays the take blended between the two recorded frames around that moment, since a live take is recorded at an uneven rate (9 to 37 ms a frame on mltn’s PC). Stepping at the take’s own average rate instead, and holding the recorded frame at or before each moment, made the replay jerkier than the take itself: on leg-raise-stomp, seen as a 30 fps camera would, the head’s frame-to-frame velocity change was 0.091 m/s against the take’s 0.069 and the pelvis’s 0.058 against 0.042; stepped and blended it is 0.057 and 0.041.
  4. It calibrates at the take’s calibration frame. A take that began calibrated has no T-pose in it, so its calibration is restored instead, a second into the settle with the body standing in the take’s first frame: the calibration wires the targets as always, then the take’s recorded target offsets, puck lifts and standing heights replace the measured ones before the hips and the visual legs measure from them, the hips take the recorded standing height and each visual leg’s ankle goes back to its recorded offset. The bone frames of the visual legs, which a take does not record, are measured in that standing frame. done.json gives each body part’s distance from the take’s first frame, and on mltn’s leg-raise-stomp take the replay’s visible feet stayed within 0.1 mm of the take’s mean distance from their targets.
  5. It records itself as a take, snapshots the frames given by -SnapshotFrames, and quits.

With -ReplayPlan @{ settingChanges = @(@{ frame = 40; settings = @{ AvatarScale = "10" } }) } (FbtReplaySettingChange) the replay changes those preferences once it reaches each take frame, in order, the way a change in the menu during play would, and logs replay: at take frame ... settings changed.

With -Echo @{ joinAt = 300; playerJoinAt = 600; forgetAt = 900; recalibrateAt = 1200; swapAt = 1500; swapAvatar = "<barcode>"; calibrateAfterSwapAt = 1600; trackersOffAt = 1800; trackersOnAt = 2000 } (take frames; leave out any step) the run also sets DIGITALHEAVEN_FBT_ECHO (FbtNetEcho, FbtNetEchoPlan). Every full-body message the player sends goes through its pack and unpack into the real receive path, as if from another player, whose rig is a second player rig (PlayerRigCopy, the same copy as the [Player rig] NPCs) standing puppetSide meters to the player’s right, wearing the same avatar and given the player’s head, hands and turn the way LabFusion gives a remote rig them. Before joinAt there is no server, so nothing is sent. The second rig is recorded as a take of its own (echoed\), every frame after its legs are drawn, with its receiver’s state per frame in echo-net.csv; fbt-replay echo <replayed> <echoed> <folder> (FbtEchoMetrics) pairs the two takes by game frame and measures, each rig against its own body, each leg bone’s rotation relative to the hips, each ankle relative to the hips and above its own floor, and the hips’ height and turn, and the upper body the same way: the chest against the head (whole, and as a heading about the play space’s up) and each upper arm against the chest, with each measure’s error over time, the jitter of both rigs and the lag at best fit. The chest’s heading error decides a verdict, chest against the head: PASS while its 95th percentile stays within FbtEchoMetrics.ChestToleranceDegrees (5), and a FAIL makes the command exit 1: a pelvis that reaches the second rig turned against its head twists the chest and wraps the arms round the torso, which the leg and hips measures alone never saw. A version 1 take has no upper body, and its verdict reads not measured. Each step is a late-joiner case, and echo.json holds every event and a verdict per case: the events the case needs, in order, within windowSeconds (5) of game time, and no pose sent while the trackers were off.

With -RemotePlayer <id> (the player’s LabFusion small id as header.json lists it under remotes; it implies -Echo, and the take must hold remotes.bin) the second rig plays a player the take recorded instead of looping the replayed player’s own messages, so a real-lobby bug runs from the real recording. plan.remote in echo-plan.json sets it (FbtNetEchoPlan.Remote). The take’s own frames still play the local player. From then on, at the take’s own clock (FbtReplay.Elapsed):

  • the recorded player’s DH messages are handed to the real receive path (FbtNet.Receive) as player 1, each at the moment it arrived, calibrations first, so the receiver asks, interpolates and draws their legs from exactly the bytes it was sent. What the receive path sends back (its requests) goes nowhere;
  • the second rig’s headset, both hands, the turn of its controller rig and its crouch and feet offset are the recorded rig’s, blended between the two samples around the moment, as LabFusion writes a remote rig’s;
  • it wears their recorded avatar (re-dressed if they changed it), at the scale the observer’s copy had.

The late-joiner steps do not run, and the second rig stays where it was made: a player who walked in the recording is not moved in the replay, only their body is. echo.json gains recordedPlayer with how many messages were fed. Then fbt-replay remote <take> <id> <folder> writes the recorded player as an ordinary take (FbtRemoteConvert: their headset, the pelvis, and the bones the observer saw), and fbt-replay echo <folder> <report>\echoed <out> pairs it with the second rig’s take frame by frame, exactly as for the replayed player: the chest against the head, the arms against the chest, the legs against the hips. The rig the replay records for its own take (replayed\) also records the second rig as a remote when FbtRecordRemotes is on, so replayed\remotes.bin can be set beside the original.

Each run writes a report folder (-OutDir, by default %LOCALAPPDATA%\DigitalHeaven\fbt-replays\<timestamp>):

PathWhat
done.jsonfinished, any error, frames played, the level barcode, title and scene, the mod build
metrics-take/, metrics-replay/metrics.csv (one row per frame) and summary.json (mean, 95th percentile and maximum over calibrated frames)
summary.mdthe take against its replay, column by column
replayed/the replay as a take, with start and stop snapshots
snapshots/frame-N/the snapshots asked for
video/front.mp4 and side.mp4 (the body seen along the take’s starting heading), eye.mp4 (the headset’s own eye), with -Echo echo-front.mp4 and echo-side.mp4 (the second rig the same way), and views.mp4 (all of them side by side), H.264 at about 30 frames a second, rendered every whole number of game frames so they are evenly spaced, with the front and side cameras easing after the pelvis over VideoFollowSeconds (0.75 s) instead of shaking with it, encoded by ffmpeg when it is on the PATH. Without ffmpeg the numbered PNG frames stay. Every view comes from the capture rig’s offscreen camera, never the screen, and shows the whole scene with the debug markers
echo.jsonwith -Echo: every event and each case’s verdict
Latest.log, Player.logthe run’s logs

The metrics (FbtMetrics) are read from frames alone, so a live take and its replay are measured alike. For each foot: the calibrated target to the visible (avatar) foot, to the physics foot, the two feet apart, the tracker to the visible foot, the knee’s bend and how fast the thigh, shin and foot turn (a popping bone shows as a spike). The pelvis target to the physics pelvis and to the avatar’s hips, and how far the play space has drifted and turned since the first frame.

The fbt-replay tool (Platforms/DigitalHeaven.Mods.Bonelab.Replay) works without the game: synth <folder> [level barcode] writes a synthetic take (stand, a T-pose with its calibration, a kick with each foot, a squat), metrics <take> <folder> measures any take, compare <a> <b> prints two runs’ summaries side by side (two builds, or two leg modes), dump <take> <frame> prints one frame, jitter <take> [fps] measures how unevenly the head, feet and pelvis move (as a camera at that rate would see them), remote <take> [player folder] lists the other players a take recorded in a lobby or writes one as a take of its own, and inputs <take> says what each controller’s stick, trigger, grip and buttons did.

The replay drives the controllers’ inputs, not the rig’s values: the take’s stick, trigger, grip, finger and button values go into XRApi’s controllers with their edges, so BONELAB’s own locomotion, jump, crouch and turning run from them as they did live.

SLZ’s profile has never run in a shipped build, and turning it on as it is costs the controllers and creates no trackers. The mod repairs three things, all only while FbtTrackers is on, in process with Harmony:

FaultCauseRepair
Hands sink into the body with no laser, buttons still workBONELAB’s hand pose is Unity’s additive palm pose alone. Unity OpenXR 1.8.2’s PalmPoseInteraction.AddAdditiveActions returns at the first action map with no hand path instead of skipping it, and the tracker’s map has none, so every controller profile after it in OpenXRSettings.features lost the palm pose (Valve Index went from 24 actions to 23)The tracker profile is moved to the end of OpenXRSettings.features before it is turned on, and is left off if it cannot be. The merge is also replaced by one that skips a hand-less map, falling back to Unity’s on any error
Could not create a device for 'HTC HTC Vive Tracker OpenXR', no tracker ever trackingSLZ’s XRViveTracker.devicePose is OpenXR’s own PoseControl. The game’s OpenXR package uses the Input System’s and never registers OpenXR’s, so the Input System infers the “Pose” layout from the type name. Worse, a discovered tracker is built from the layout the Input System’s XR support derives from the runtime’s descriptor (XRInputV1::HTC::HTCViveTrackerOpenXR, based on XRViveTracker), which names “Pose” for its pose outright. Either way the wrong class is built and FinishSetup throwsOpenXR’s PoseControl is registered as the OpenXRLegacyPose layout before XR starts. Each layout the XR support builds on XRViveTracker (caught as InputManager.RegisterControlLayoutBuilder registers it) gets a layout override naming devicePose’s layout outright, and the mod logs what devicePose then builds as. Every other device’s layout is left alone
The left foot’s tracking would mirror the right foot’sSLZ’s input asset binds LeftFootTrackingState to <XRViveTracker>{Right Foot}/trackingStateEvery tracker action is checked against its role’s binding, and a wrong one is overridden

The hands guard. After Unity merges the palm pose, the mod checks that every action map with a hand path carries it. If one does not, it logs HANDS AT RISK at startup and again at every level: turn FbtTrackers off and restart before playing.

What to look for in the log ([Fbt], every line at Quiet):

  • OpenXR features (29, mod start), the tracker profile moved from #11 to #29 of 29: ...: the feature order the XR loader will use, the tracker last.
  • XR loader starting; tracker feature enabled: True; feature order: tracker last and registered OpenXR's own PoseControl as the 'OpenXRLegacyPose' layout.
  • XRInputV1::HTC::HTCViveTrackerOpenXR.devicePose built as 'Pose'; the layout override '...' makes it 'OpenXRLegacyPose': the layout a discovered tracker is built from is fixed, and the tracker devices can be created. (XRViveTracker.devicePose builds as 'OpenXRLegacyPose' checks SLZ’s base layout only.)
  • hands OK: the palm pose (palmpose) is in all 7 hand profiles; actions per map: ...: the controllers will have their poses. HANDS AT RISK instead means turn FbtTrackers off.
  • tracker bindings: 20 actions checked, 1 rebound: LeftFootTrackingState ....
  • path: OpenXRSettings feature. '<name>' (XR_HTCX_vive_tracker_interaction) was off, now on: the profile was found and turned on.
  • OpenXR runtime ...: XR_HTCX_vive_tracker_interaction enabled = True: the runtime accepted it. False means the probe stops here; Player.log’s Runtime extensions enabled list says the same.
  • input devices (...): the Input System devices the game has, a tracker with its role and what its devicePose built as. A Vive tracker the runtime bound appears here even when XRApi.Trackers reads nothing, which tells a missing binding from a missing tracker.
  • t=12s 3/10 tracking: head play (...); waist play (...) world (...); ...; devices (3): waist (...), leftFoot (...), rightFoot (...): the poses. Play is the OpenXR play space, world is Unity’s. A waist about 1 m up and the feet near 0 mean the poses are sane. The devices part reads the tracker devices straight from the Input System, so a role listed there but untracked before it points at SLZ’s action map rather than the tracker.
  • calibrated (N wired, ...): each wired target and its offset from its puck. An offset over about 0.5 m means the calibration went wrong.
  • Errors and warnings name the step that failed and are logged once.

The DigitalHeaven overlay draws on BONELAB’s desktop window, never in the headset. Press F1 or - on the desktop keyboard to open and close it (OverlayHotkeys). The keys are read without the game window’s focus. A - typed into the focused window also counts, for virtual keyboards that type characters rather than press keys. Escape closes it while the window has focus. With LogInterface at Normal, every function key and - pressed is logged with the action it fires ([Keys]), so a key that does the wrong thing, or nothing, shows in the log. A window whose drawing throws draws an empty body and logs the error once; the other windows, and the paper doll’s corner, keep drawing. Its Config window shows the same settings as the in-VR page (see In-VR settings page) and saves them right away. A setting that applies later says when. The renderer name lists have no text field in the overlay yet, so they are edited in the file.

The menu bar’s Diagnostics entry opens the Halcyon Diagnostics viewer on UserData\DigitalHeaven\diagnostics, and closes it again when it runs (a graceful close first, a kill if it has not exited after about 2 s). Running means any process named HalcyonDiagnostics, however it was started, checked once a second. The label says the state: Diagnostics when closed, Diagnostics (running) when open, Diagnostics (connected) when the viewer is following this game’s folder (one the mod started is, by construction; one found running is only if it left a live viewer marker), and Diagnostics (not installed) when no exe is found, which is logged once with where it looked (DiagnosticsAppPath, else UserData\DigitalHeaven\Diagnostics\HalcyonDiagnostics.exe). A -p:Deploy=true build also publishes the viewer there.

Preferences file: UserData/MelonPreferences.cfg, category [DigitalHeaven].

SettingDefaultDescription
EnabledtrueRegister DH avatars. Restart after changing
UseGameShadertrueMove DH materials onto ReferenceShader (see Materials). Off keeps the DH loader’s materials, for avatars and maps alike: cutouts draw solid, glass opaque and voxel floors as dark mirrors. In the menu it is Advanced > Game shader, a safety switch that turns yellow while off, and every map built with it off logs a [Map] warning
UnlockEverythingfalseAdvanced > Cheats > Unlock every spawnable, a safety switch. Lists every locked spawnable in the spawn menu; the save is untouched.
InvinciblefalseAdvanced > Cheats > Invincible, a safety switch. Holds the local player’s own health in the game’s Invincible mode and refuses a direct kill; restores the earlier mode when off. Applies at once.
ScaleLaunchGuardtrueAdvanced > Safety, a safety switch. A launch whose saved AvatarScale is risky or dangerous resets it to 1x (see Safety).
ScaleConfirmtrueAdvanced > Safety, a safety switch. “Keep this size?” after a change into a risky or dangerous size; no answer goes back.
ScaleRadialtrueAdvanced > Safety, a safety switch. The radial menu’s Avatar scale entry.
MapTrapRescuetrueAdvanced > Safety, a safety switch. “Having trouble?” after repeated deaths or respawns; Get me out loads Void G114.
ScaleSafeMin, ScaleSafeMax, ScaleDangerBelow, ScaleDangerAbove0.5, 3, 0.25, 20The avatar scale’s danger bands.
ScaleConfirmSeconds, ScaleConfirmWaitSeconds, SafetyPanelDistance10, 10, 0.7”Keep this size?”: its countdown, the most it waits for the size to be worn, and the panel’s distance at 1x.
ScaleRadialPresets0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10, 20, 50, 100The sizes the radial menu’s Scale up and Scale down step through.
MapTrapWindowSeconds, MapTrapDeaths, MapTrapMergeSeconds, MapTrapCountdownSeconds, MapTrapLeaveSeconds180, 3, 8, 15, 1The death-loop watch: its window after a level loads, the cycles that ask, what counts as one cycle, the countdown, and a LabFusion client’s wait between leaving the lobby and loading.
MapTrapDestinationVoid G114The level Get me out loads.
ReferenceShaderSLZ/LitMAS/LitMAS StandardThe game shader DH materials are moved onto
ReferenceShaderKeyPackages/com.unity.render-pipelines.universal/Shaders/LitMAS/LitMAS.shaderWhere the catalogs hold that shader, when no loaded material or shader has its name: an Addressables key or the asset path (see Game shaders)
HeadRendererNameshead,face,eye,brow,lash,teeth,tongue,mouthName fragments that make a skinned renderer an Avatar head mesh
HairRendererNameshair,bang,ponytail,braidName fragments that make a skinned renderer an Avatar hair mesh. Hair wins over head
PreviewMeshQuality0.5How much of the mesh the body log’s hologram preview keeps when simplified, passed to MeshCroncher; cfg-only, with no menu row
ReferencedBlendShapesOnlytrueImport only the blend shapes an avatar references. See Blend shape import. Restart after changing
BuildUploadBudgetMs4Milliseconds per frame the warm-up spends uploading the textures it decoded off the main thread; at least one uploads each frame. Applies at once; cfg-only
BuildTexturesAhead2How many decoded avatar textures may wait for their upload. Each holds its whole mip chain (about 85 MB for a 4K texture), so lower it where memory is short. Applies to the next avatar read; cfg-only
SpringBonestrueSimulate DH spring bones. Applies at once; off, every body stands as animated
SpringInteractionstrueLet hands touch, grab and pose spring bones at all, where a pallet allows it (see Touch)
SpringOthersOnMinetrueLet other players’ and NPCs’ hands reach your avatar’s spring bones
SpringMineOnOtherstrueLet your hands reach other players’ and NPCs’ spring bones
SpringWorldCollisiontrueKeep spring bones out of the level (floors and walls)
SpringWorldSweepfalseAlso cast each spring particle along its path every tick, against a thin wall a fast tail would cross between ticks
SpringTouchMaxVolumes12The most hand volumes one chain is pushed by in a tick, the nearest first; cfg-only
SpringTouchRange3Hands are read only from bodies within this many meters of a body a hand may touch, plus yours; cfg-only
SpringTouchRigs8The most bodies whose hands are read in a frame, nearest first; cfg-only
SpringTouchLodDistance6A body farther than this from the camera skips touch; cfg-only
SpringGrabHoverReach0.12A free hand arms on the nearest grab point within this many meters of its palm; cfg-only
SpringGrabMinRadius0.035The smallest grab handle, in meters; cfg-only
SpringGrabHysteresis0.02How much nearer, in meters, another grab point must be to take an armed hand; cfg-only
SpringGrabMass0.3The grab handle’s mass in kilograms while held; cfg-only
VisemesForRemotePlayerstrueIn a LabFusion server, shape other players’ DH avatars’ mouths from their voices, keeping LabFusion’s jaw flap off them. Off leaves their jaws to LabFusion (see Mouth)
VoiceMouthFromMicrophonefalseIn single player, record your microphone (LabFusion’s input device when set, else the system default) to move your DH avatar’s mouth. Never while a LabFusion server runs
MouthAnalysisAutoShapes (uLipSync’s MFCC), Volume (loudness only) or Auto (Shapes for an avatar with visemes, Volume otherwise)
MouthVoiceGapSeconds0.1A voice that sends nothing for this long counts as silent, in seconds
MouthTraceSeconds0.25With LogMouth at Verbose, every this many seconds while a DH mouth hears your microphone or moves, log the microphone’s level, floor, gate and gain, the loudness, the strongest viseme and consonant, and the summed shape weight. 0 keeps it quiet
RemoteReactionstrueIn a LabFusion lobby, voice other players’ DH avatars’ footsteps, hard landings and hits from what LabFusion sends
RemoteMinSpeed0.5Horizontal speed (m/s) under which a remote player is standing and takes no footsteps
RemoteStepDistance0.8Meters a remote player covers between one footstep and the next
RemoteLandSpeed6Fall speed (m/s) from which a remote player’s sudden stop plays a hard landing
RemoteHitDamage1Damage handed to the pain vocal when another player hits a remote player
RemoteShotWoundstrueHit a DH NPC here along another player’s shot when the game’s bullet left no mark on it
RemoteShotWindow0.35Seconds a remote shot gets to mark a DH NPC on its own before DH hits it
RemoteShotRange100Meters a remote shot is followed to a DH NPC
SoundstrueLoad an avatar’s dh.reaction sounds into the game’s avatar sound slots and play them on DH NPC bodies (see Reaction sounds). Takes effect on the next avatar build
NpcstrueOffer every DH avatar as one NPC entry in the spawn gun, its disposition, base and body chosen in the info pane (see NPCs). Applies at once
NpcBodyDhRigfalseDevelopment body: offer DH rig in every entry’s Body row. Off hides it and builds nothing for it. Applies at once
NpcBodyDhAnimfalseDevelopment body: offer DH anim in every entry’s Body row. Applies at once
NpcBodyPlayerRigfalseDevelopment body: offer Player rig in every entry’s Body row. Applies at once
TintDhEntriestrueTint DigitalHeaven’s own entries pale pink (text #F7D3EE, the button fill a faint pink cast) in the spawn menu: the DigitalHeaven category button, every DH crate in the list and the DigitalHeaven word in the breadcrumb. Set after the game writes each page of buttons, and taken back off a button that now shows a stock crate. Row NPCs > Tint DH entries.
NpcPaneRowScale1The size of the spawn pane’s NPC rows against the Options menu’s rows they are copied from (see the spawn pane). Applies the next time an entry is selected
NpcFitHeighttrue[DH rig] only: scale an NPC’s avatar uniformly to Ford’s height before its ragdoll is built. Off keeps its own size. Takes effect on the next launch
NpcReportFrames600Frames between each DH NPC’s health line, with LogNpc at Verbose. 0 turns it off
FusionSweepSeconds1In a LabFusion lobby, seconds between sweeps for LabFusion props whose Rigidbody or MarrowEntity was destroyed under them (LabFusion otherwise throws on each, every frame, and the game slows to a crawl); each is unregistered and a [Npc] line at Quiet names it. 0 turns it off
BodyContactSeconds0.25Seconds between checks of what your physics body is inside of; a collider nothing draws that rides a rigidbody or that DH made is named in a [Rig] line at every level (see Body watch). 0 turns it off
BodyContactRepeatSeconds30How long one named invisible collider stays quiet
BodySpreadSeconds1Seconds between measurements of the headset against the physics head and the avatar’s hips against the physics pelvis. 0 turns it off
BodySpreadMeters0.3How far either pair may come apart before a [Rig] line says so
BodySpreadRepeatSeconds30While the body stays apart, seconds between repeats of that line
DhColliderCensusSeconds60Seconds between counts of the solid colliders DH made or holds, written when a count changed. 0 turns it off
NpcCensusSeconds60In a LabFusion lobby, seconds between [Npc] live DH NPC records lines (spawned, despawned, props unregistered), written only when a count grew. 0 turns it off
CounterLogSeconds60Seconds between rows of the counter log (UserData/DigitalHeaven/counters/). 0 turns it off
CounterCensusDelaySeconds10Seconds after a level starts before any object census runs: the level-start one, a requested one, a timed one
CensusAfterLeveltrueTake a census once each level has settled and write it to the session file, with a [Perf] line naming what grew since the last one
DiagnosticsSessiontrueAlso write each reading, with marks, censuses and requested profiles, to the live session file the Halcyon Diagnostics viewer follows
DiagnosticsAppPathemptyWhere HalcyonDiagnostics.exe is, for the overlay’s Diagnostics entry. Empty looks in UserData\DigitalHeaven\Diagnostics, where a -p:Deploy=true build publishes it
CensusTimerfalseTake a census every CensusTimerMinutes, never while a level loads or settles
CensusTimerMinutes10Minutes between timed censuses
NpcThicknessPercentile0.5Share of each body part’s mesh vertices inside its ragdoll collider’s radius, for [Ford] and [DH rig] NPCs (0.5 is the median distance). Takes effect on the next launch
NpcHeadSlices16[Ford] and [Player rig]: how many slices the head mesh is cut into along the avatar’s forward to tell the skull from a snout. Takes effect on the next launch
NpcHeadSkullShare0.7[Ford] and [Player rig]: a head slice at least this share of the widest slice’s radius belongs to the skull; the longest run of them is the skull capsule. Takes effect on the next launch
NpcHeadProtrusionMin0.2[Ford] and [Player rig]: the vertices beyond the skull that its capsule leaves out (a muzzle and jaw) get a second, thinner capsule when the slices beyond it, and those vertices, each cover at least this share of the head’s length; a round head stays one capsule. Takes effect on the next launch
NpcFootLengthMin0.85[DH rig] only: the shortest foot an NPC stands on, as a share of Ford’s foot at the NPC’s size. Takes effect on the next launch
NpcFootLengthMax1.3[DH rig] only: the longest foot an NPC stands on, as a share of Ford’s foot at the NPC’s size. Takes effect on the next launch
NpcFloorRuleHeight0.2Only a ragdoll collider whose stock part stands lower than this (meters, at the NPC’s size) above the floor is kept off the floor; a head, torso or hand never is. Takes effect on the next launch
NpcRigAiIkfalse[DH rig] only: let the AI’s IK (foot placement, arm reach, spine and head twist) act on the avatar’s own bones. Off keeps it on Ford’s hidden skeleton, since it turns bones about Ford’s bone axes. Takes effect on the next launch
NpcRigIgnoreSelftrue[DH rig] only: none of an NPC’s own colliders collide with each other while it lives, on top of what the puppet ignores itself. A test of whether self-collision mangles them. Applies to the next spawn
DebugCapturesfalseMaster switch of the automatic captures; off takes none (see Diagnostic captures). Run NPC test still photographs its NPCs
MenuCapturestrueWith DebugCaptures on: photograph the DigitalHeaven settings page when it is opened or turned, at most six a session
NpcCapturestrueWith DebugCaptures on: photograph each NPC crate’s first spawn of a session from the player’s eyes, three-quarter and above at 0.5 to 15 s, and each DH NPC once it dies (see NPCs)
NpcRigHealthScale1A [Player rig] NPC’s health as a multiple of its rig’s own maximum
NpcRigSightRange18How far a [Player rig] NPC notices someone to fight, in meters; it chases out to half as far again
NpcRigDespawnSeconds10How long a dead [Player rig] NPC lies ragdolled before it is removed
NpcRigArmStrength0.35How hard a [Player rig] NPC’s arms hold their pose while it is not fighting, as a share of a player’s arm force
NpcRigHitImpulse6How hard a hit pushes a [Player rig] NPC’s body part along the shot, in newton seconds. 0 turns it off
NpcReactionstrueDH NPCs voice a hit that hurts them (damaged, small or big) and their death and revival from the mouth, as a player does; the mouth follows a speak
NpcBigHitFraction0.25The share of an NPC’s hit points one hit takes from which its pain vocal is the big one
NpcHurtCooldown0.6Seconds after an NPC’s pain vocal before another hit voices another
NpcWoundstrueMark DH NPC bodies where they are hit with BONELAB’s own wounds, in every body mode (see Blood and wounds). Off leaves them unmarked at once; blood still sprays
NpcSpringLevelLimittrueA DH NPC’s spring bones skip the level queries while it stands farther than NpcSpringLevelMeters from your head (see Spring bones, NPC spring limits). Applies at once
NpcSpringLevelMeters6The distance, in meters, past which an NPC’s chains stop touching the level
NpcSpringFreezeLimittrueA DH NPC’s spring bones stop while it is beyond NpcSpringFreezeMeters or out of your camera’s view for half a second. Applies at once
NpcSpringFreezeMeters20The distance, in meters, past which an NPC’s chains stop
NpcWoundRampsblood,flesh,skinName fragments of a flesh wound ramp. DH NPCs take Ford’s own, else a loaded one so named, never another material’s (a crablet’s metal)
NpcWoundSize1Size of a wound on a DH NPC, as a multiple of the size the game gives Ford’s
NpcWoundLogHits12How many hits and sprays per DH NPC get a [Gore] log line, with LogNpc at Verbose
NpcRigHeadGriptrueLet hands grab a [Player rig] NPC’s head (its AvatarGrip). Applies to the next spawn
NpcRigTorsoGripsfalseLet hands grab a [Player rig] NPC’s chest, spine and pelvis. Off until the head’s grip has held up in a run. Applies to the next spawn
NpcRigWalkSpeed0.75How hard a [Player rig] NPC pushes its stick to walk (0 to 1); a run is a full push
NpcRigWalkTurnRate120Fastest a [Player rig] NPC’s body turns while it walks, in degrees per second
NpcRigRunTurnRate180Fastest it turns while it runs, fights or reacts, in degrees per second
NpcRigStepTurnRate90Fastest it steps round in place when nothing is urgent, in degrees per second
NpcRigGlanceYaw45How far its head turns to either side of its body to glance at something, in degrees
NpcRigUprightDegrees40Hip tilt past which it counts as toppled and stops steering, in degrees
NpcRigTurnDeadZone2Its turn stick rests while its play space is within this of the wanted facing, in degrees
NpcRigTurnFullDegrees30Its turn stick is pushed fully once its play space is this far off the wanted facing, in degrees
NpcRigMotionLogSeconds0.5Fewest seconds between two of its motion lines, with LogNpc at Verbose; changes in between are counted into the next
NpcRigStrikeSwingSeconds0.35How long its fist is sent at the target in one strike, in seconds
NpcRigGuardDrop0.3While it fights, how far below its eyes a hand that is not striking holds its guard, in eye heights
NpcRigGuardForward0.2While it fights, how far ahead of its eyes a guarding hand waits, in eye heights
NpcRigGuardSide0.1While it fights, how far to its side a guarding hand waits, in eye heights
NpcRigHeadFittrueFit its head to the avatar’s mesh as a [Ford] NPC’s is (a skull and a snout), beside the player body’s own head collider. Applies to the next spawn
NpcRigHeadFitDelay1How long after it is dressed its head is fitted, in seconds
NpcRigHeldStiffness0.15While a hand that is not its own holds it, how hard its spine and arms hold their pose, as a share of what they hold otherwise
NpcRigHeldHeadFollow1While held, how much of the way its head’s target follows where the hand moves the head sideways and forward or back (0 to 1)
NpcRigHeldHeadLift0.8While held, how much of the way its head’s target follows where the hand moves the head up or down (0 to 1); below 1 it keeps holding itself up
NpcRigHeadShapetrueGrow its own SLZ head (what a hand grabs, a shot hits and a blade meets) to the fitted skull and snout. Applies to the next spawn
FusionLanSharingAutomaticShare the LabFusion server you host on the local network: Automatic (Public and Friends-only), Always (Private too) or Never. Locked is never shared (see joining a server)
FusionLanPort47812The UDP port LAN advertisements go to and are heard on; the same on every machine. Not in the in-game settings
FusionLanIntervalSeconds2How often a shared server is advertised, in seconds. Not in the in-game settings
FusionLanExpirySeconds6How long a nearby server stays listed after its last advertisement, in seconds. Not in the in-game settings
FusionLanShown4The most nearby servers the Enter Code page lists. Not in the in-game settings
TransferStreams4Steam P2P connections a DH content transfer opens to the player it fetches from; ranges spread over them. 0 sends everything through LabFusion’s own connection and opens no Steam P2P socket of DH’s at all. Not in the in-game settings
TransferRangeKb256The size of one range a transfer asks for, in KB. Not in the in-game settings
TransferInFlightPerStream4Ranges kept in flight per Steam P2P connection. Not in the in-game settings
TransferSendRateMbps400The most each DH transfer connection may send, in megabits a second (Steam’s default ceiling is far lower); congestion control still backs off below it. Not in the in-game settings
KnockoutClosesEyestrueIn a LabFusion lobby, a knocked-out player’s DH avatar closes its eyes on the dying event (LabFusion sends no death for a knockout) until recovery or respawn
MenuClicksThroughWallsfalseWhile the utility menu is open, a press reaches a button inside a wall (see Menu clicks through walls)
ScaleJumptrueJump height follows your avatar scale: the launch speed is the stock one times the square root of the scale
ScaleShadowDistancetrueRaise the shadow distance by your avatar scale while you are scaled up
ScaleShadowMaxFactor8The most the shadow distance is multiplied by
PlayerRigHeadFittrueFit your own rig’s head to your DH avatar the way a [Player rig] NPC’s is. Applies to the next swap
RemoteRigHeadFittrueFit another LabFusion player’s rig’s head to their DH avatar. Applies to the next swap
NpcRigHeadCapsulesfalseKeep the fitted capsules once SLZ’s head has been grown to them. Applies to the next spawn
NpcRigHeldEaseSeconds0.3How long it takes to go loose when grabbed, and to stiffen again once let go, in seconds
NpcRigGetUpDegrees20Hips this far off upright with no hand on it count as down
NpcRigGetUpSeconds2How long it stays down before it is put back on its driven pose, in seconds. 0 leaves it down
NpcRigLetGoSeconds0.5How long a hand may keep hold of something before it lets go, in seconds. The brain never picks anything up on purpose, so anything it holds was caught by accident. 0 turns this off and a hand keeps what it catches
NpcRigStockAiTargetstrueStock NPCs on the other side within NpcRigSightRange are told of it, so they fight it
AlignFingerFramestrueTurn each finger and thumb bone to the index’s frame (see Finger bone frames). Takes effect on the next launch
ThumbRollDegrees0Extra roll of every thumb about its length, mirrored for the right hand. Takes effect on the next launch
CaptureOnBuildtrueWith DebugCaptures on: render each DH avatar offscreen when it is built, and the reference avatar once per session (see Diagnostic captures)
CaptureAfterSwapSeconds4With DebugCaptures on: seconds after a successful swap before the live capture. 0 turns it off
CaptureSettleSeconds3Quiet time after the rig’s first avatar load, a swap, a build or a scene load before any capture or reference load
OverlayHotkeysF1, -Keys that open and close the overlay, separated by commas, read without the game window’s focus
CaptureHotkeyF9Key for a live capture: F1 to F24, a letter, a digit or -, read without the game window’s focus. Empty turns it off
CaptureChordSeconds1Hold both thumbsticks clicked in this long for a live capture from VR. 0 turns it off
CaptureSize1024Width and height of each capture, rendered at twice the size and downsampled
CaptureReferenceAvatarSLZ.BONELAB.Content.Avatar.FordBWStock avatar captured beside the DH ones. Empty turns it off
CaptureLightIntensity1Key light intensity in offscreen captures
PaperDollfalseShow the paper doll (see Paper doll)
DollHotkeyF8Key that turns the paper doll on and off. Empty turns it off
DollInHeadsettrueShow the paper doll in the headset
DollOnDesktoptrueShow the paper doll in a desktop corner
DollMirrorImagetrueFlip the paper doll left to right, as a mirror would
DollPosedCopyfalseDraw a posed copy of the avatar instead of the live body
DollHeldItemstrueDraw what the hands hold and the holsters carry in the paper doll and the world mirror screen; never a fixture (see Paper doll)
DollItemMaxSize3A held thing whose longest side (its meshes only, not trails, lines or particles) is larger than this, in meters, is not drawn. A weapon that fits a holster always is
DollFadeSeconds0.6How long a held item fades in and out, in seconds. 0 shows and hides it at once
DollFadeStyledissolvedissolve (the game’s hologram dissolve) or alpha (see-through). Dissolve falls back to alpha when it cannot be used. Applies to the next fade
DollLingertrueKeep drawing a dropped or thrown item for DollLingerSeconds, then fade it out
DollLingerSeconds2How long a dropped item lingers, in seconds. 0 turns lingering off
DollFragmentWindow0.5How long after a shown item breaks a spawn with physics counts as one of its fragments, in seconds. Not in the in-game settings
DollFragmentRadius1.5How far from a broken item its fragments may spawn, as a multiple of its size across. Not in the in-game settings
DollShowUnfadabletrueDraw items that cannot fade (they appear and go at once). Off, they are never drawn in the paper doll, whatever their size
DollAttachedScanSeconds1How often the paper doll looks for things joined to your body with no hand holding them (the Descent noose), in seconds. Not in the in-game settings
DollGroupRecheckSeconds0.25How often a held thing of several parts is walked again, in seconds, so a part that came apart leaves with a linger and a fade. Not in the in-game settings
DollSize240Desktop paper doll height in pixels
DollRightfalseDesktop paper doll in the top right corner
UiRenderScale1Picture resolution multiplier (0.25 to 2)
DollResolution512Picture height in pixels before UiRenderScale (3:4)
DollRate60Pictures per second; 0 renders every frame
DollDistance3.2Camera distance from the chest, in meters
DollHeight0Camera aim above the chest, in meters
DollAngle20Camera angle round from straight ahead, in degrees
DollFov38Vertical field of view, in degrees
HudDistance0.8Headset picture distance ahead, in meters
HudSideLeftWhich side of the view the headset picture sits on: Left, Center or Right
HudOffsetX0.28Headset picture offset toward HudSide, in meters (a negative value from before HudSide is read as its size)
HudOffsetY-0.18Headset picture offset up (negative is down), in meters
HudSize0.3Headset picture height, in meters
HudOpacity0.6Headset picture opacity
HudFacesEyestrueTurn an off-center headset picture to face your eyes. Not in the in-game settings
WorldMirrorOffOff, Game mirror or Screen; choosing one places it in front of you. Back to Off at every level start
WorldMirrorHotkeyF7Key that places the world mirror in front of you again
WorldMirrorDistance1.5How far in front of you the world mirror is placed, in meters
WorldScreenHeight2Height of the world mirror screen, in meters
PreferencesPagetrueThe DigitalHeaven page in the game’s preferences menu
MapstrueList every DH map in the level select as a level of its own, singleplayer only. Restart after changing
MapPanelModsMods tags the crates Mod so the mods tab lists them; DigitalHeaven tags them for a tab of their own, which the stock menu does not have yet (why). Restart after changing
MapUseShellfalseFallback: load a stock level under every map and hide it, instead of building the level from nothing (below). Restart after changing
MapShellBarcodec2534c5a-61b3-4f97-9059-79155363656eThe stock level that fallback uses; the default is Baseline. Restart after changing
MapColliderLayer0The physics layer of everything a map creates: 0 is Default, 6 is Fixture. Takes effect on the next map load
MapColliderShapeSharedShared, UnitScale or Convex: how a map’s solid meshes collide. Takes effect on the next map load
MapKillFloortruePut you back at the map’s spawn when you fall out of it
MapKillFloorDrop30How far below the map’s lowest collider counts as out, in meters
MapSpawnLift0.25How far above the spawn point you are put down, in meters
MapSpawnClearMeters8How far above a voxel map’s spawn you may be raised when the spawn is inside the streamed terrain. 0 leaves it
MapLoadTimeoutSeconds120How long a map level may take to load before the mod leaves it for the level the player came from, in seconds. 0 waits forever
MapLightingtrueLight a map from its own sun, lights and ambient (How a map is lit). Takes effect on the next map load
MapSunIntensityScale, MapLightIntensityScale, MapAmbientIntensityScale1Multipliers on the map’s sun, light entities and ambient, to match the game’s exposure
MapSunShadowstrueLet the sun cast shadows. Other lights never do
MapMaxLights64The most point and spot lights a map creates
MapReflectionResolution128Edge in pixels of the reflection probe a map gets; 0 makes none
MapReflectionProbeSize200Edge in meters of the cube the probe covers, centered on the spawn
MapSkyboxShaderSLZ/Skybox/SLZ CubemapThe skybox shader a map’s sky is set as the scene’s skybox with; empty draws no sky
MapSkyboxShaderKeyPackages/com.unity.render-pipelines.universal/Shaders/Sky/SkyCubemap.shaderWhere the catalogs hold that shader, when no loaded shader has its name: an Addressables key or the asset path. Empty uses this stock path
MapSkyFaceSize256Edge in pixels of each face of the sky cubemap, at most the sky image’s own
MapSkyGeneratedtrueDraw a generated sky behind a map with no image sky. Off leaves such a map on the game’s own background
MapSkyZenith, MapSkyHorizon, MapSkyGround0.30 0.52 0.95, 0.76 0.86 1.0, 0.36 0.38 0.40The generated day sky’s colors, sRGB red green blue in 0 to 1
MapSkyFlat0.09 0.10 0.12The flat sky of a map that names no sky: the DH engine’s clear color
MapSkyIntensity1Multiplier on the generated sky
MapFogDistance0BONELAB’s volumetric fog over a map, as its mean free path in meters; 0 is none (Fog). Takes effect on the next map load
MapFogBaseHeight, MapFogTopHeight0, 50Where that fog is densest, and where it has thinned to the game’s 1 km haze, in meters
MapCutoutShaderUniversal Render Pipeline/Lit (PBR Workflow)The shader a map’s masked materials and voxel cutout blocks are drawn with (a real alpha test); empty blends them through LitMAS
MapCutoutShaderKeyPackages/com.unity.render-pipelines.universal/Shaders/Lit.shaderWhere the catalogs hold that shader, when no loaded shader has its name: an Addressables key or the asset path. Empty uses this stock path
MapVoxelGlowScale1Multiplier on the glow of cutout blocks (torches, fire). 0 turns it off
MapCompressTexturestrueBlock compress a map’s textures and drop their CPU copies (why)
MapRepackMaxEdge1024The largest edge of the normal and MAS textures a map’s materials are repacked into. 0 keeps the source size
MapUnlitShaderUniversal Render Pipeline/UnlitThe shader a map’s unlit materials use when the game has it loaded or its catalog holds it (at URP Unlit’s stock path); empty always uses LitMAS emission
MapGrabConvexColliderstrueGive every closed convex piece of a map node a convex collider so walls, rails and pipes are grabbable (see Playing a map). Takes effect on the next map load
MapGrabTolerance0.002How far a vertex may sit off a face for a piece to count as convex, in meters
MapGrabMaxHullVertices255The most distinct vertices a piece may have to be made convex
MapNavMeshtrueBake a navmesh for a map once you are in it so NPCs can walk
MapNavMeshMaxSize400The widest area, in meters, the navmesh covers; a larger map bakes a square around the spawn. 0 bakes it all
MapNavMeshTimeoutSeconds120How long a bake may take before the log says it gave up waiting. 0 waits forever
MapNavMeshRefreshMeters8How far you move before a voxel map’s navmesh is baked again around you. 0 bakes it once
MapVoxelstrueStream a map’s voxel volumes. Takes effect on the next map load
MapVoxelRadius128Meters from your head voxel terrain is drawn. Read every frame
MapVoxelMeshBudgetMb512Megabytes of voxel meshes every volume may hold together; the farthest regions are dropped past it
MapVoxelWorkers2Threads that mesh voxel regions. Read when the first volume streams in
MapVoxelCollisionRadius32Meters from your feet voxel terrain is solid. Read every frame
MapVoxelPrewarmSeconds4How long the loading screen waits for the terrain around the spawn. 0 does not wait
MapVoxelLogSeconds5How often a voxel map’s streaming state is logged at Normal. 0 never
MapInventorytrueCarry holstered items and ammo into and out of a map through the game’s save helpers. Writes no progress
MapInventoryKeyDigitalHeaven.MapsThe save key every map shares for it. A campaign key such as Hub starts maps with what that level saved
NpcTestDwellSeconds12Run NPC test: how long each body mode’s NPC stands before it is despawned, in seconds; also the wait allowed for its crate to become spawnable
NpcTestDistance2Run NPC test: how far ahead of the player’s eyes the NPCs are spawned, in meters, on the floor under that spot
CaptureAmbient0.35Flat ambient level in offscreen captures
FbtTrackersfalseExperimental: turn on BONELAB’s OpenXR Vive tracker profile before XR starts and log the trackers (see the probe). Restart after changing
FbtCalibrateDelaySeconds3Seconds between pressing Calibrate trackers and the calibration
FbtKeepCalibrationtrueWith FbtTrackers: keep the calibration until it is cleared or replaced, and apply it by itself (see the kept calibration)
FbtRestoreSettleSeconds1With FbtKeepCalibration: seconds a new rig or avatar must stay put before the kept calibration is applied to it
FbtRestoreGraceSeconds3With FbtKeepCalibration: when only some of the kept trackers are on, seconds to wait for the rest before applying it without them
FbtDebugDrawfalseWith FbtTrackers: always-on-top tracker markers and a calibration view (see the probe)
FbtDebugBallSize, FbtDebugAxisLength0.04, 0.1Marker ball diameter and axis tripod length, in meters
FbtCalibrationViewSeconds, FbtCalibrationViewScale8, 1.6How long the enlarged, labeled markers stay after a calibration you started, and how much larger they are
FbtCalibrationMirrortrueA mirror in front of you while a calibration you started is counting down or just finished, with or without markers
FbtCalibrationMirrorSeconds1.25How long the calibration mirror stays up after a calibration you started
FbtDiagnosticsInterval0Seconds between the foot diagnostics lines at LogFbt Normal, and in a LabFusion lobby a line per other player’s shared body; 0 logs none
FbtHipstrueWith a calibrated waist tracker: the pelvis follows the waist through SLZ’s tracked spine (see the probe)
FbtHipsWeight1With FbtHips: how far the pelvis target goes from the game’s pelvis (0) to the tracker’s (1)
FbtHipsFadeSeconds0.3With FbtHips: how long the pelvis eases back to the game’s when the waist drops out, and to the tracker’s when it returns or after a calibration; 0 switches at once
FbtHipsRotationtrueWith FbtHips: the pelvis turns with the tracker; off, only its position follows
FbtHipsYawOnlyfalseWith FbtHipsRotation: only the tracker’s heading, on the game’s tilt
FbtHipsCrouchFromPelvistrueWith FbtHips: the target’s height is the tracker’s, so the game’s crouch follows the hips; off, the height stays the game’s
FbtHipsKeepFeetCentertrueWith FbtHips: the feet center the locomotion ball stands under stays where the head-driven body puts it
FbtLegModevisualHow tracked legs are shown: visualOnly (the art legs follow the trackers and nothing else changes), visual (the art legs follow the trackers; a clearly raised foot kicks with the physics leg) or physics (the game’s physics legs chase the trackers through the foot hold)
FbtKickHeight0.15With FbtLegMode visual: how far a foot rises above its calibrated standing height, in world meters (scaled with the avatar, as the trackers are), for its physics leg to kick while the other foot is down; it stops at half that
FbtKickSpeed1With FbtLegMode visual: how fast a foot tracker must move, in real m/s, as it rises past FbtKickHeight for a kick to start
FbtKickSeconds1With FbtLegMode visual: the longest a kick holds its physics leg
FbtKickMinPelvis0.8With FbtLegMode visual: below this fraction of the standing pelvis height (sitting, crouching, lying) no foot kicks
FbtRemoteLegModesameHow other DH players’ tracked legs show in a LabFusion lobby: same (as your FbtLegMode), visualOnly and visual (their legs follow what they send) or physics (the game’s legs; their hips still follow). See full-body tracking over LabFusion
FbtNetTimeoutSeconds0.5In a lobby: how long another player’s body holds its last pose before it eases back to the game’s
FbtNetPlantedCorrection1In a lobby: how far another player’s foot that is down is pulled onto where it really is (0 none, 1 all the way)
FbtNetFreeCorrection0.3In a lobby: the same pull for a foot that is up
FbtNetBufferedtrueIn a lobby: show another player’s body between the two ticks around a delayed moment; off eases toward the newest tick as LabFusion does for heads and hands
FbtNetDelaySeconds0.075With FbtNetBuffered: how far behind the newest tick the body is shown
FbtNetCorrectionEaseSeconds0.15In a lobby: how long a foot takes to go between the light and the full pull onto the sent ankle; 0 switches with each tick
FbtNetRequestSeconds1In a lobby: the least time between two asks for (or answers with) a calibration between the same two players
FbtLegFadeSeconds0.25With FbtLegMode visual: how long the visible legs ease between the game’s stepping legs and the trackers; 0 switches at once
FbtLocomotionAnimationtrueWith a calibration: off keeps the legs on the trackers while the stick walks the body (see the Locomotion animation row)
FbtLegWalkSpeed0.15With FbtLegMode visual: above this stick-walking speed (m/s) the visible legs are the game’s stepping legs; below it they follow the trackers
FbtHoldFoottrueWith FbtLegMode physics: a tracked foot leaves the step planner at FbtLiftThreshold and stays where its tracker is while it is up (see the probe)
FbtLiftThreshold0.03With FbtLegMode physics and FbtHoldFoot: raw tracker rise above calibrated standing height to be held, as a fraction of the leg length. The game’s planner uses 0.06
FbtPhysicalLegstrueWith FbtLegMode physics and FbtHoldFoot: only a held leg goes physical; the planted leg stays kinematic
FbtFloorClearance, FbtFloorProbeMeters0.015, 0.2Collider clearance and ray start height in world meters, before the leg solve
FbtMirrorFollowfalseWith FbtTrackers: the calibration mirror stays up and follows you, facing the body 1.6 m ahead of the pelvis at eye height
FbtMirrorDeadZoneMeters, FbtMirrorDeadZoneDegrees0.5, 30With FbtMirrorFollow: how far the pelvis walks or the body turns before the mirror re-places itself
FbtMirrorEaseSeconds0.5With FbtMirrorFollow: how long the mirror takes to glide to its new place; 0 moves it at once
FbtRecordMaxSeconds600Record movement stops by itself after this many seconds (see Recording movement)
FbtRecordRemotestrueIn a LabFusion lobby, Record movement also records every other player (see Recording other players)
FbtRecordRemoteHz30With FbtRecordRemotes on: how many times a second each other player’s rig is written, 0 for every frame (messages are always written whole)
FbtLogBurstSeconds, FbtLogBurstInterval, FbtLogInterval30, 0, 0The tracker report runs every FbtLogBurstInterval seconds for the first FbtLogBurstSeconds of a level, then every FbtLogInterval. 0 logs none in that window
LogNpc, LogAvatar, LogSpring, LogFace, LogMouth, LogSound, LogPaperDoll, LogCapture, LogMap, LogFbt, LogInterface, LogPerfQuietHow much each log category says: Quiet, Normal or Verbose (see Logging)

Category [DigitalHeavenAnim] holds one entry per eye brain setting, named and defaulted as the engine’s client.anim.* preference: blinkRate here is client.anim.blinkRate there. Both are built from Core’s one table, so the list, ranges and defaults are the engine avatars page’s. The entries are read every frame, and a value outside its range lands at the nearest end. The overlay’s Config window and the in-VR page show eyeLookEnabled (Eye look), blinkEnabled (Blinks) and blinkRate (Blink rate); the rest are edited in the file.

SettingDefaultDescription
eyeLookEnabledtrueWhether avatars look around on their own
blinkEnabledtrueWhether avatars blink on their own
blinkRate17Spontaneous blinks per minute at rest
eyeLookMaxPois8Heads one avatar’s eyes weigh per frame, nearest first
eyeLookMutualGazeAngle10Degrees off a player’s view an avatar still counts as looked at
every other client.anim.eyeLook* and client.anim.blink* fieldas the engineSee the engine’s table

The mouth’s tuning sits in [DigitalHeavenAnim] beside the eyes’: one entry per field of Core’s MouthSettingFields and MfccMouthSettingFields, named and defaulted as on the rig page and read every frame. mouthEnabled is the master switch for every DH mouth. The voice settings are in [DigitalHeaven] (see the table above). The overlay’s Config window and the in-VR page show these:

SettingDefaultRowDescription
mouthEnabledtrueMouth: moves to voices (DH visemes)Every DH mouth, the player’s own included. Off leaves every jaw to LabFusion
VisemesForRemotePlayerstrueMouth: other players’ voices (LabFusion)Other players’ DH avatars
VoiceMouthFromMicrophonefalseMouth: my microphone (single player)DH’s own microphone capture, single player only
mouthAutoLevelEnabledtrueMouth: microphone auto levelThe noise gate and automatic gain on your own microphone, DH’s capture or LabFusion’s
MouthAnalysisAutoMouth: how it reads a voiceAuto: vowels if the avatar has visemes, Vowels (MFCC) or Loudness only
mouthMinOpen0.5Mouth: least opening while voicedOff, 25%, 50%, 75% or 100% of the voice’s level
mouthConsonantsfalseMouth: consonant guessesThe MFCC analyzer’s SS, CH, FF and PP guesses
mouthJawBonetrueMouth: jaw bone when it has no mouth shapesOpen the Jaw bone on an avatar with no visemes and no mouth-open shape
every other mouth* fieldas the rig pageEdited in the file; see the rig page’s two tables
  1. Install MelonLoader 0.7 into BONELAB and start the game once, so that it generates MelonLoader/Il2CppAssemblies.
  2. Copy DigitalHeaven.Mods.Bonelab.csproj.user.example to DigitalHeaven.Mods.Bonelab.csproj.user and set BonelabDir if the game is not at D:\SteamLibrary\steamapps\common\BONELAB.
  3. Run dotnet build Platforms/DigitalHeaven.Mods.Bonelab -p:Deploy=true. With Deploy=true the build copies the mod to Mods\ and the DigitalHeaven libraries it runs on (DigitalHeaven.Core, .Unity, .Unity.IL2CPP, .Overlay, .Unity.Overlay, VJson, K4os.Compression.LZ4, System.IO.Hashing, and the audio decoders NVorbis and NLayer) to UserLibs\, which MelonLoader loads before any mod. A plain build never touches the game install.
  • Not everything is checked in game. See the Untested rows in Core Features.
  • Mouth profiles. The Shapes analysis reads every voice against uLipSync’s sample female profile. Nothing calibrates one to yours yet, so vowels are read only roughly.
  • Soft body at defaults. The torso heights, ellipse radii, limb rings and bulges keep the SLZ.VRMK.Avatar component’s defaults. They are fractions of the eye height, so they follow the avatar’s size but not its shape. Fitting them to the mesh is a later step, and the log prints every value under [SlzAvatar].
  • Head meshes by name. A single-mesh avatar has no separate head mesh, so the game cannot hide the head in first person.
  • No hot reload. An avatar is built once per session, the first time the game asks for it. Changing a pallet needs a restart.
  • First use still costs a frame. The read-ahead takes model parsing and texture decoding off the main thread, but the build itself (the meshes, materials, rig, humanoid, LitMAS repacks and the hologram bake) still runs in one frame. A swap to an avatar the warm-up has not reached builds inside the game’s call, as before. With LogPerf at Normal, each build logs where that frame went (see Logging).
  • NPCs are experimental. [Ford] NPCs were usable in run 29, before their joints and colliders were fitted to the avatar. Every entry is built from Ford unless the player picks another base in the spawn pane or its author’s npcBase names one; the other bases are unproven. A [Ford] NPC’s ragdoll keeps Ford’s 17 muscles and joint limits, moved onto the avatar’s joints and fitted to its mesh; its toes and fingers have no muscle of their own, and big shapes such as tails, ears and hair have no collider. Hit wounds are not drawn on the avatar (Ford’s damage renderers are hidden), and the NPCs are silent until a per-avatar voice exists. In a LabFusion lobby [Ford], [DH rig] and [DH anim] NPCs are synced as pooled objects (see Multiplayer); [Player rig] NPCs are not.
  • [Player rig] NPCs are experimental. Following and limp death worked in prior tests, but run 33 lost player grabbing after NPC removal and later reset the scene for out-of-bounds movement. The player-reference guard, grip layer changes and hand cleanup need an in-game retest; the reset cause is not established. Torso grips stay closed by default. They punch only: grabbing and weapons are not driven yet.
  • Maps are singleplayer and unchecked in game. See Maps: lit in real time only, no thumbnails, the DigitalHeaven tab needs a menu panel the mod does not make, and the shell fallback is untested.
  • Required bones. The Marrow SDK requires every humanoid bone except the eyes, jaw, upper chest and the middle and little fingers. Missing finger bones are added. Any other missing bone is logged by name, and PrecomputeAvatar is expected to fail on that body.