Pallets & Assets
DigitalHeaven organizes everything into pallets: packages of related assets like avatars, materials, textures, and models.
Naming Conventions
Section titled “Naming Conventions”One rule covers every identifier DigitalHeaven mints: dots separate namespaces, and every segment is camelCase. That applies uniformly — component $types (dh.springBone), entity types (spawnPoint), input/output names (onPressed, open), and preference keys (world.jumpHeight, client.camera.roll.dampingRatio) all follow it. JSON property keys are camelCase too (baseMesh, eyeRotationLimits).
Acronyms are treated as ordinary words, never as all-caps runs: glbPath, setHdrSky, enableCsg — not GLBPath or enableCSG.
Hyphens still show up, but only where they’re structural rather than identifier style: file extensions (.dh-obj, .dh-mat), pallet IDs (dev.example.avatars.mayu-custom), and file paths. None of those are camelCase identifiers, so the hyphen stays.
Definition files (.dh-obj, .dh-mat, pallet.dh, etc.) are JSONC in practice: comments and trailing commas are accepted.
Pallets
Section titled “Pallets”A pallet is a folder with a pallet.dh manifest and whatever assets it needs.
Pallet IDs
Section titled “Pallet IDs”Pallets use reverse-DNS identifiers:
dev.example.avatars.mayu-customtech.azuki.avatars.mayucom.example.my-avatar
Platform IDs are pallet IDs: a game or tool such as com.mojang.minecraft names its pallet the same way (see Platform IDs, which also lists each platform’s built-in short aliases).
This prevents naming conflicts and shows ownership. The ID also determines the folder path: dev.example.avatars.mayu-custom lives at Source/dev.example/avatars/mayu-custom/.
Manifest: pallet.dh
Section titled “Manifest: pallet.dh”Every pallet needs a manifest:
{ "id": "dev.example.avatars.mayu-custom", "name": "Mayu Custom", "version": "0.0.1", "author": "example", "dependencies": { "tech.azuki.avatars.mayu": "^3.0.0" }}The manifest also accepts an optional look — a built-in slug or a .dh-look reference — which becomes the pallet’s house art direction. Every map in the pallet picks it up, and it is the weakest authored layer, so a map that names its own look or authors its own render fields still wins:
{ "id": "dev.example.maps.harbor", "name": "Night City", "version": "0.0.1", "look": "looks/night.dh-look"}Compiled pallet size
Section titled “Compiled pallet size”A compiled pallet has no 2 GiB ceiling: every offset and size in its format is 64-bit. Once a pallet’s data passes 256 MiB the compiler packages it through a scratch file beside the output instead of memory, so building a large pallet needs about twice its size free on the output drive. A single file inside a pallet is still limited to under 2 GiB, and to 1 GiB when it is read back. A file the compiler cannot package fails the build and names the file; it is never left out quietly.
Model stand-ins
Section titled “Model stand-ins”Every model an object names (its model, or the source of a record it adds) (a .glb, or an .fbx the build converts to one) ships a stand-in beside it in the compiled pallet: a .dh-proxy companion at the model’s own pallet path plus .dh-proxy, holding the model’s bounds and a tiny merged silhouette of it. An engine draws it the moment an object is placed, long before the model itself has been read, parsed and uploaded; see while a model loads. A model nothing imports, such as a map’s own geometry, gets none.
The silhouette is bounded by size, never by proportion: a model with more triangles than the cap is simplified down to at most the cap, however large it was, and then further until it fits the byte budget; a model already under the cap keeps its own triangles. A million-triangle model and a ten-thousand-triangle one both ship a few hundred. Both limits are workspace settings (proxyTriangles and proxyBytes), 512 triangles and 6 KiB by default. Everything is in the model’s root frame at rest, with a skinned mesh in its bind pose, the same picture the engine resolves the model to before anything poses it. UVs, materials and bones are dropped.
The simplifier is a pure C# port of Sven Forstmann’s Fast Quadric Mesh Simplification (MIT), the algorithm Whinarn’s UnityMeshSimplifier and BONELAB’s MeshCroncher run, in DigitalHeaven.Core so the compiler, the engine and a game mod share one implementation with no native code. Vertices are welded by position first, since a GLB splits them along every seam; a mesh the simplifier cannot bring under the cap is clustered on a coarsening grid instead, which always ends. Proxies are cached under the workspace cache directory (proxy-cache) by the model’s bytes and both limits, so an unchanged model is never simplified twice. A model the compiler cannot read ships no stand-in and one that will not simplify ships its bounds alone; both warn with the model’s path, and neither fails the build.
The format is little endian, a fixed 44-byte header followed by the vertices and then the indices:
| Offset | Type | Field |
|---|---|---|
| 0 | 8 bytes | Magic, DHMPROXY |
| 8 | u16 | Format version, currently 1 |
| 10 | u16 | Flags; bit 0 set when a silhouette follows |
| 12 | u32 | The source model’s triangle count |
| 16 | u16 | Vertex count |
| 18 | u16 | Triangle count |
| 20 | f32 x 3 | Bounds minimum |
| 32 | f32 x 3 | Bounds maximum |
| 44 | 8 bytes per vertex | Position as three u16, quantized within the bounds; normal as two s8 on the octahedron |
| then | 6 bytes per triangle | Three u16 vertex indices |
A reader that only wants a model’s size reads the header alone: PalletAssetSource.TryReadModelBounds never opens the GLB, and ReadModelProxy returns the whole stand-in. An object’s stand-in is the proxies of every model its definition imports, joined (ObjectProxies.Read). A proxy of another version, or one that does not parse, is no stand-in and a warning, never a failed load, like every other companion; the format version is folded into the companion epoch, so a version bump rebuilds every pallet.
Compiled pallet import foundation
Section titled “Compiled pallet import foundation”The shared toolchain provides a host-independent import validator and private library installer. The experimental iOS adapter exposes a Documents Import pallets inbox and an optional Import pallets action in the shared launch chooser. The folder is the interface: whatever sits in Import pallets when the app opens is validated and installed then, so dropping a file in over Files and launching is the whole gesture. The chooser action stays for a file dropped while the app is already running. Installing is content-addressed and short-circuits on bytes that are already active, so re-reading the same dropped file every launch costs a hash and nothing else. Opening the app still never launches a game, and original files remain untouched. The private library lives in Application Support, not the Files-visible inbox or a network cache. These adapter changes require physical-device validation before availability is claimed for an installed build. Source authoring remains separate.
There is no limit on a pallet’s size, its number of files, or the library’s total. Validation streams through the file: the data checksum and the content fingerprint are computed a piece at a time and entries are decoded one at a time, so a pallet of several gigabytes validates in the memory of its largest entry. The one limit left is the JSON header, 64 MiB by default (PalletImportPolicy.MaximumHeaderBytes), because the header is the only part parsed whole; it names every entry, so even a block library of tens of thousands of files stays well under it. A single entry cannot exceed 2 GB, the most one array holds. Validation checks the header limit before parsing, rejects unsafe or duplicate paths, reserves core for the host, and treats every checksum mismatch as fatal. A valid container is not necessarily playable: asset semantics and dependency availability are separate checks. Fingerprints and storage hashes verify consistency, not publisher authenticity.
A pallet built by another toolchain version is not a mismatch. The fingerprint folds the companion format epoch, so that a format bump rebuilds an unchanged pallet. The header records the epoch it was built under (buildInfo.companionEpoch), and the validator re-derives the fingerprint with that recorded epoch rather than its own. A pallet from before the epoch was recorded is checked through the epoch-free half instead: the header’s content checksum, the data checksum and every entry’s checksum. Either way it loads, and the library lists it with the note “compiled by another DigitalHeaven toolchain version; recompile it with this one to refresh”. A companion too old for its loader (lightmap, light probes, reflection cook, cooked collision, lightmap UVs) degrades with a warning of its own. A container version this build cannot read is still refused, and the refusal names the version.
PalletLibrary leaves the caller’s original source untouched. It bounded-copies the source into private staging, validates that copy, publishes exact-byte SHA-256 filenames, and atomically activates an ID-to-filename manifest. Reimporting identical bytes is a no-op; a new version of the same ID intentionally replaces the active version without deleting the old private file. Failure or cancellation before activation preserves the previous manifest. Orphaned immutable files may remain after failed activation; the importer never evicts them or touches authoring workspaces. Hosts must supply a dedicated app-private directory and separately decide when to rebuild their runtime catalog. They use ReadSnapshot to read only verified active versions, never enumerate retained blobs. Snapshot reads return immutable entries or fail closed without writes; defaults also limit the whole library scan to 512 MiB stored and 1 GiB decoded. Bundled core and dependency availability remain host concerns.
Sharing pallets with other players
Section titled “Sharing pallets with other players”In a game DigitalHeaven supports in multiplayer, a player who lacks a pallet someone else is using (their avatar, the lobby’s map) can get it from that player directly. Every game uses the same transfer stack (TransferManager in Core); only the pipe differs per game.
- What travels. The receiver names what it needs (a pallet, or the game’s barcode for a map or avatar). The sender answers with the pallet and its
dependencies, walked to their end, each named by the fingerprint in its own header. The receiver skips every pallet it already has with those exact bytes, wherever it lives, so a shared base travels once. - Who. Pallets from someone on your DH friends list arrive automatically. For anyone else the game asks first: download, download and add them as a friend, or decline.
incomingTransfersEnabledandoutgoingTransfersEnabledin the workspace config turn receiving and sending off. - How. The receiver pulls each pallet one range at a time, with several ranges in flight. Ranges ride the game’s own network or, where a game has one, a separate bulk pipe that carries no gameplay. Bytes land on disk as they arrive (
<workspace>/.dh/transfers/<hash>.partial), so an interrupted transfer resumes from where it stopped. A finished pallet is re-hashed against its fingerprint and validated like any imported pallet before it is installed in<workspace>/.dh/received/. A pallet that fails either check is discarded. - Your own copy wins. When you already have a pallet with the same id but other bytes, yours is kept and the difference is named. Received pallets are listed after your workspace’s own.
Assets
Section titled “Assets”An asset is any file inside a pallet: definition files (.dh-avatar, .dh-mat, etc.) or binary resources (.png, .glb, .wav). The exception is sidecar files (e.g. sprite.png.dh-tex-settings, robot.fbx.dh-model), which carry import settings or edits for a neighboring file and are consumed during compilation rather than included as assets. See dh.texture — Sidecar Files and dh.object — Sidecar Files for details.
Referencing Assets
Section titled “Referencing Assets”Within the same pallet, use the path from the folder the file you are editing sits in:
{ "texture": "textures/body-diffuse.png"}A file in a subfolder therefore names its neighbors by their bare names, and reaches the pallet root with a leading slash:
{ "baseColor": "base.dh-tex", // outfits/jacket/base.dh-tex — the sibling "normal": "/textures/shared-normal.png" // from the pallet root, wherever this file moves to}This is one rule for every local reference in every asset kind — textures, materials, geometry, looks, inherits — so an author never has to know which field they are filling in. Resolution happens once, at compile: the compiler rewrites every local reference to its pallet-root path before packing, and the engine opens what it is handed rather than resolving anything itself.
Across pallets, prefix with the pallet ID and a : separator:
{ "baseMesh": "tech.azuki.avatars.mayu:models/body.glb"}This is called a barcode: pallet.id:path/to/asset.
Resolution Rules
Section titled “Resolution Rules”- No
:in the path → local reference within the current pallet - Has
:→ everything before it is the pallet ID, everything after is the path - A local path is read from the folder of the file that wrote it; a leading
/reads it from the pallet root instead - The path after a
:is always read from the target pallet’s root - A path after a
:names the file as authored, so a.dh-texin another pallet is found as the.pngit compiles to, and the build checks that the pallet holds it
Declaring What You Point At
Section titled “Declaring What You Point At”A barcode may name your own pallet, the engine’s core pallet, or a pallet you declare in dependencies. Anything else is a build error — the compiler will not let an asset reach into a pallet the manifest never mentions, so a pallet’s outside edges are always written down in one place.
At load, dependencies are followed transitively: a dependency’s own declared dependencies are opened with it, so an asset in a dependency (a Source map’s material in com.valvesoftware.hl2, say) can reach the pallets that dependency declares even when the map never named them. A dependency that cannot be found is reported by the pallet that declared it.
{ "id": "dev.example.maps.harbor", "version": "0.0.1", "dependencies": { "dev.example.shared.props": "^1.0.0" }}Naming your own pallet is always legal, and never a dependency. A barcode whose pallet id is the pallet being compiled is a self-reference: it means exactly what the unprefixed path means, and a manifest never declares itself. It is still held to the local rules — a self-qualified path to a file that isn’t there is the same error the plain path would be. (Declaring yourself as a dependency anyway is harmless, just noise.)
An asset’s own references anchor to the pallet that owns the asset. When a material in dev.example.shared.props says "/textures/neon.png", that is a texture in dev.example.shared.props — not in the map’s pallet, even though the map is what asked for the material. Unprefixed paths mean “here” from the point of view of the file they are written in, at build and at load alike, so a shared pallet can be authored once and read the same from any map that depends on it.
The engine honors the same list at load. Opening a map opens its declared dependency pallets alongside it, so a material slot pointing at dev.example.shared.props:materials/neon.dh-mat resolves from the pallet that actually holds it — no copying the material into every map that uses it. Those pallets stay open for as long as the scene does and are closed with it, so a recompile-and-reload picks up new bytes.
A declared dependency that isn’t installed on the machine loading the map is named out loud: a warning at load says which pallet the map declared and could not find, and every reference into it fails with the same pallet id in the message. Nothing is silently substituted, and the rest of the map still loads.
Inheritance
Section titled “Inheritance”Assets can inherit from other assets, even across pallets, and override specific properties.
{ "inherits": "dev.example.avatars.mayu-custom:avatar.dh-avatar", "children": [ // Only what differs { "path": "Body", "components": [ { "$type": "dh.renderer", "materials": { "Skin": "materials/custom-skin.dh-mat" } } ] } ]}Inheritance works at every level: avatars, objects, materials, rigs. You only define what’s different.
The compiler validates every inherits reference at build time; the chain itself folds when the asset loads, child-first, with each level’s own references (a material’s textures, say) reading from the pallet that authored them.
Platform Overrides
Section titled “Platform Overrides”Platform-specific data lives in a platforms/ subdirectory alongside your universal definitions:
Directorymy-avatar/
- avatar.dh-avatar Universal
Directorymaterials/
- skin.dh-mat Universal material
Directoryplatforms/
Directoryvrchat/
- avatar-descriptor.jsonc
Directoryunity/
Directorymaterials/
- skin.dh-mat Unity-specific shader overrides
Directoryresonite/
- avatar-metadata.jsonc
When building for a platform, the compiler loads the universal definition first, then applies any matching overrides from platforms/{platform}/.
The folder is named with the platform’s built-in alias (platforms/bonelab/, the form these docs use) or with its full platform ID (platforms/com.stresslevelzero.bonelab/). Both work, and files in either are read. If the same file exists under both, the ID’s copy wins and a warning names both paths.
Organizing Your Pallet
Section titled “Organizing Your Pallet”Shared assets go in type-based folders at the pallet root. Self-contained features (accessories, outfits) get their own subdirectory with everything co-located:
Directorymy-pallet/
- pallet.dh
- avatar.dh-avatar
Directorytextures/ Shared textures
- body-diffuse.png
- body-normal.png
- body-orm.dh-tex
Directorymodels/ Shared models
- body.glb
Directoryrigs/ Shared rigs
- humanoid.dh-rig
Directorymaterials/ Shared materials
- skin.dh-mat
- eyes.dh-mat
Directoryaccessories/ Self-contained features
Directoryglasses/
- glasses.dh-obj
- model.glb
- material.dh-mat
- lens.png
Directorypiercing/
- piercing.dh-obj
- model.glb
- material.dh-mat
The rule: shared/base assets in type folders at root; self-contained features get their own directory with everything they need inside.
Version Control
Section titled “Version Control”Designed for Git:
- Commit:
Source/(your source files),Scripts/(your custom scripts) - Ignore:
Pallets/(compiled output),.dh/(cache)
The workspace init generates .gitignore and .gitattributes automatically. The .gitattributes file sets up Git LFS for binary assets (.glb, .fbx, .png, .jpg, etc.), so make sure you have Git LFS installed if you’re using those.