Project Zomboid
Platform ID com.theindiestone.projectzomboid
DigitalHeaven.Mods.ProjectZomboid Java + Lua composites a DigitalHeaven avatar over the local player in Project Zomboid build 42: the world render, the character creation preview and the Info panel doll, all occluded correctly by walls, fences and furniture. Unlike the Unity-based mods, this one shares its architecture with the Minecraft mod: a sidecar DigitalHeaven engine process renders the avatar and Project Zomboid composites the frame in.
Core Features
Section titled “Core Features”| Feature | Status |
|---|---|
| Avatar replacement (local player, world render) | Yes |
| Held items at the avatar’s hands | Yes |
| Occlusion by walls, fences, furniture and trees | Yes |
| Spring bones, with wall and floor contact | Yes |
| Spring colliders | Yes |
| Lit like a vanilla survivor: night, room and lamp light, fog | Yes |
| After death: the dying body, the corpse and the zombie it rises as | Yes |
| Character dolls (Info tab, DigitalHeaven tab, character creation) | Yes |
| In-game avatar menu (character window tab, creation screen) | Yes |
| Live avatar swap, no restart | Yes |
| World and doll resolution settings | Yes |
| Other players / NPCs / split-screen players | No |
| Overlay | No |
When single-player time is stopped (game speed paused), the avatar stops too: spring bones, eye look and blinking hold where they are, on the world avatar and the dolls, and carry on from there when the game resumes, with no catch-up jump. A multiplayer client never holds them.
Requirements
Section titled “Requirements”| Game version | Project Zomboid build 42.21 or later (the Java build; not the legacy Kahlua build) |
| Mod loader | ZombieBuddy, as a native Java agent |
| Engine | A built DigitalHeaven.Engine.Host.exe (Release), run as a sidecar process |
Project Zomboid’s own launcher (ProjectZomboid64.exe) creates its JVM through JNI, where -javaagent can’t attach — ZombieBuddy’s native agent (-agentlib:zbNative) is the mechanism that actually works for a Steam install, and it needs zbNative.dll/ZombieBuddy.jar sitting beside the game executable. ZombieBuddy also gates mods by jar hash (policy=deny-new): a mod jar has to be approved in mod_approvals.json before it loads, which the install script does automatically.
Install
Section titled “Install”The install script sets up a real Steam install for the normal “Play” button — no harness, no scripted menu.
- Build the engine host (
DigitalHeaven.Engine.Host, Release configuration) - From
Platforms/DigitalHeaven.Mods.ProjectZomboid, run:Terminal window powershell -NoProfile -ExecutionPolicy Bypass -File tools\install-steam-pz.ps1 - Launch Project Zomboid from Steam as usual
By default it targets D:\SteamLibrary\steamapps\common\ProjectZomboid and the standard %USERPROFILE%\Zomboid cachedir; both can be overridden with -PzInstallDir/-CacheDir. The script is idempotent — rerunning with nothing to change touches no files — and it:
- builds and copies the
DigitalHeavenandZombieBuddymod folders into the cachedir’smods/ - copies
zbNative.dll/ZombieBuddy.jarinto the game install root, besideProjectZomboid64.exe - adds both mods to
default.txt’smods { }block - approves the mod’s jar hash in
zb-config\mod_approvals.json, so a freshly built jar doesn’t trip ZombieBuddy’s approval prompt - writes default settings into
digitalheaven\digitalheaven.jsonunder the cachedir, only where a value isn’t already set - rewrites
ProjectZomboid64.json’svmArgs:-agentlib:zbNative=policy=deny-new,config_dir=...as the first entry, plus-Ddh.session.dir=...
Every file the script edits in place gets a timestamped .bak-<yyyyMMdd-HHmmss> copy first. Run the same script with -Uninstall to revert the launcher, strip the mod lines from default.txt, and remove the mod folders and the two ZombieBuddy files from the game root; it deliberately leaves zb-config\ and digitalheaven\ alone, since those hold config and other mods’ approvals.
Choosing Your Avatar
Section titled “Choosing Your Avatar”Open the character window (the heart icon on the sidebar, or its key) and pick the DigitalHeaven tab, the last one after Temperature. Click the Avatar header to open the list, then click an avatar. The swap is live: your survivor stands in, in the world and on the doll, for the few seconds the engine takes to load the new avatar, and the header reads Loading … until it lands. None puts your survivor’s own body back at once.
- The list is only there while you hold it open. At rest, the header, the Info tab row and the creation combo name only the avatar you wear, so a screenshot or a stream of the menu shows nothing else. Esc or a click outside closes the list without a change, and closing the window closes it too.
- The Info tab has an Avatar … Change row after Hair (and Beard). Change switches to the DigitalHeaven tab.
- Character creation has a DigitalHeaven section under Beard with the same picker in a single line. The big doll shows the pick live.
- One avatar per install. The pick is saved to
digitalheaven.json, so it applies to every save and every survivor, including one picked on the creation screen. - Only you see it. Other players, and split-screen players 2 to 4, see the vanilla survivor.
The avatar can also be set in the session’s config file, normally:
C:\Users\<you>\Zomboid\digitalheaven\digitalheaven.json{ "avatar": "io.mltn.avatars.mltn-taidum:mltn-taidum.dh-avatar", "enginePath": "H:/Projects/mltn/Tools/DigitalHeaven/Engine/DigitalHeaven.Engine.Host/bin/Release/net10.0/DigitalHeaven.Engine.Host.exe", "renderScale": 1.0, "uiRenderScale": 1}avatar is a full barcode (<pallet ID>:<asset path>). An edit to the file is read at the next launch; the menu is the way to switch mid-game. A workspace rebuild of the worn avatar hot-reloads on its own, and the tab’s status line says reloading after a pallet edit while it does.
-Ddh.avatar=<barcode> on the launcher’s vmArgs overrides the saved avatar at launch. -Ddh.enginePath=<path> overrides the engine executable path the same way. Both are written back to the file once the session starts the engine, like a pick from the menu.
The DigitalHeaven Tab
Section titled “The DigitalHeaven Tab”| Control | What it does | Applies |
|---|---|---|
| Doll (left) | The same DH doll the Info tab draws. Drag it to turn. | — |
| Avatar | The picker above. Disabled while an avatar loads. When the engine failed, it reads Engine unavailable, and a click retries. When only the worn avatar failed to load, it reads Avatar unavailable, the engine stays up, your survivor draws its own body, and a click opens the list so you can pick another (or the same one again once its pallet is fixed). | at once |
| World avatar resolution | renderScale, 25–200 % in 5 % steps. The engine renders the world avatar at this share of its on-screen pixels, and the frame is stretched over the same rectangle. | when the slider is released |
| Dolls resolution | uiRenderScale, 25–200 % in 5 % steps, for every doll of your character. | on every step |
| Show avatar in the world | Off draws the vanilla survivor in the world, and the dolls keep the avatar. | at once |
| Show avatar on character dolls | Off draws the vanilla doll, and the world keeps the avatar. | at once |
| Keep my avatar after death | Off hands your body back to Project Zomboid the moment you die: the dying survivor, the corpse and the zombie are vanilla. | at once |
| Status line | Engine running, Engine loading …, Engine stopped, Engine off (None), Engine unavailable: and the reason, or Engine running - could not load the avatar and the reason. | — |
| Restart engine | Stops the engine and loads the worn avatar again. It rereads enginePath from the file first, so a new engine path takes effect without restarting the game. | at once |
| Retry / Copy details | Shown instead of Restart when the engine failed. Copy details puts the full diagnostic report (versions, reason, stack, engine path, the engine’s last stderr lines) on the clipboard. | — |
The world slider waits for the release because a resolution above the mapped frame (at least 1920×1080) restarts the engine, never the game: a few seconds of your survivor, then the avatar again. Dragging the doll slider costs nothing, since the doll’s preview map already holds 200 %.
Where the frame lives
Section titled “Where the frame lives”The engine hands every frame over through a mapping the mod shares with it. Every frame rewrites all of it, so the mod keeps it in memory, never on disk, where the OS allows. A mapped regular file would be written back to disk for nothing: on Linux continuously while you play, on Windows each time the mapping closes. It picks once per launch and logs which one it used (Avatar frames live in ...):
| OS | Where the frame lives |
|---|---|
| Windows | A page-file-backed named section the mod creates through java.lang.foreign. Project Zomboid runs on its bundled Java 25, whose launcher (ProjectZomboid64.json) already passes --enable-native-access=ALL-UNNAMED, so the JDK prints no warning. Needs an engine that knows --frameSection. |
| Linux | A file on a tmpfs: /dev/shm, or $XDG_RUNTIME_DIR when that is the tmpfs. |
| macOS | A POSIX shared-memory object (/dh-...) the mod creates through java.lang.foreign; the engine removes its name as soon as it has it open, so a crash leaves nothing behind. Needs an engine that knows it. Not yet tried on a Mac: the same code is tested on Linux. |
| Anything else, or any failure above | digitalheaven-frame.bin in the game directory, as before. |
The Minecraft mod shares this choice through the shared bridge; the rules are in the contract note’s “Frame backing” section.
Options › MODS › DigitalHeaven has one setting: a key that opens the character window on the DigitalHeaven tab and closes it on a second press. It is unbound by default, and Project Zomboid saves it in ModOptions.ini, not digitalheaven.json. The avatar list is deliberately not on that page. A Mod Options combo saves an item’s index rather than a barcode, and the page is built before the engine has listed anything.
Lighting
Section titled “Lighting”The avatar is lit the way Project Zomboid lights its own survivors. It darkens at night, goes black in an unlit room, takes the color of a room, a lamp or a fire, and is covered by the same fog and night vision. What its materials light themselves stays lit: unlit surfaces such as glowing eyes, and emission, look the same in a dark room as the engine draws them in a dark scene.
Project Zomboid lights a character model in display space. It multiplies the texture by 0.45 times the light of the square the character stands on, plus up to five point lights, and clamps the total at one (basicEffect.frag). The mod splits that product in two:
- The engine draws only the shape. One light, fitted to Project Zomboid’s five-light result over every direction, always at the same brightness.
- The composite applies the level. It multiplies the avatar’s color by the brightness and tint that Project Zomboid would multiply the vanilla texture by.
- Self-lit surfaces skip the level. Every frame also carries a glow plane (
glow1): the same frame drawn with every light off, which holds only what the materials light themselves. The composite darkens only the rest:glow + max(color - glow, 0) * level. Nothing is special-cased; an unlit layer at 25% opacity shows at 25% in the dark, as it does in the engine.
The split keeps the engine’s tone mapping out of the night-to-noon ratio. The dolls take their level from the doll’s own light, so, as in vanilla, they stay daylit when the world is dark.
Fog and night vision need nothing from the mod. Fog is drawn over the world after the characters, depth-tested, and night vision and desaturation are a pass over the whole screen, so both already cover the avatar.
-Ddh.light.reference=<n> (default 3) sets the brightness the engine draws the lit side at before the level scales it. The default keeps a noon avatar as bright as earlier builds drew it.
Settings
Section titled “Settings”| Key | Default | Description |
|---|---|---|
avatar | (none) | The avatar’s barcode. Empty or missing leaves the mod inert — the vanilla body renders as normal |
enginePath | (none) | Path to DigitalHeaven.Engine.Host.exe. Left unset, the workspace’s own dh-config.jsonc decides |
renderScale | 1.0 | World avatar resolution: the share of its on-screen pixels the engine renders, clamped to 0.25–2.0. The tab’s World avatar slider |
uiRenderScale | 1 | Doll resolution, clamped to 0.25–2.0. The tab’s Dolls slider |
holdFrames | 2 | How many late world frames in a row the last avatar frame is drawn again, into the current crop, before the vanilla character shows; 0–10, and 0 falls back at once. No held frame crosses an avatar change, a new player or a zoom or window change. Set in the file only |
showInWorld | true | Draw the avatar over your survivor in the world. The tab’s Show avatar in the world tick |
showOnDolls | true | Draw the avatar on your character’s dolls. The tab’s Show avatar on character dolls tick |
keepAfterDeath | true | Keep the avatar on your body after death: the dying survivor, the corpse and the zombie it rises as. The tab’s Keep my avatar after death tick |
avatar, enginePath, renderScale, uiRenderScale and holdFrames are the keys this mod shares with the Minecraft mod; showInWorld, showOnDolls and keepAfterDeath are Project Zomboid’s own.
After Death
Section titled “After Death”Your avatar stays on your body after you die, until you continue with a new character:
- Dying. The death animation plays on the avatar.
- Corpse. Project Zomboid normally draws a corpse as a pre-rendered sprite. For your own corpse the mod draws your dead survivor’s model instead, which keeps its final pose. The avatar then replaces it like the living body, with the same lighting, occlusion and fog. The vanilla shadow stays.
- Zombie. An infected corpse rises as a zombie, and Project Zomboid remembers which zombie was you. That zombie wears your avatar, posed by its own animation, until it is destroyed or unloaded.
The avatar is drawn as it was in life: no zombie coloring yet.
Current Limitations
Section titled “Current Limitations”- One avatar per install. Every save and every survivor wears the same avatar. Per-character avatars are an open design question.
- One DH doll on screen at a time. The dolls share one preview frame. The tabs of one character window never show two dolls at once, but a DigitalHeaven tab torn off beside the Info tab would make the two fight over it.
- Single-player, local player only. The composite only replaces
IsoPlayer.players[0]’s own render; other characters (NPCs, other players’ avatars) are untouched. - Your zombie turns vanilla once you continue. The avatar service draws one body. When a new character spawns, it takes that body, and the zombie that was you goes back to a vanilla zombie. If that zombie is killed, its corpse is vanilla too.
- Stairs give no floor contact. Spring bones (tail, ears) rest on floor squares for contact, but a stairway’s tread isn’t flat, so no floor patch is generated there.
Development Setup
Section titled “Development Setup”The mod project lives at Platforms/DigitalHeaven.Mods.ProjectZomboid/ and is a Gradle project, not part of DigitalHeaven.sln. Its Java source splits into io.mltn.digitalheaven.zomboid (the plugin itself — avatar/doll compositing, session, rig, world contact — always active) and io.mltn.digitalheaven.zomboid.harness (scripted acceptance-run machinery, active only under -Ddh.harness=true, never during a plain Steam launch).
Building
Section titled “Building”cd Platforms/DigitalHeaven.Mods.ProjectZomboid./gradlew.bat buildThe mod shares ../DigitalHeaven.Mods.Java (the :bridge subproject) with the Minecraft mod for the game-agnostic avatar service client, frame file and composite shader body; it’s packed into the mod jar since ZombieBuddy loads one jar per mod.
The harness
Section titled “The harness”./gradlew.bat stageRunThe Lua side of the menu lives in src/mod/42/media/lua/client/DigitalHeaven/ (the shared picker widget, the tab and Info row, the creation section, the Options key), with its strings in src/mod/42/media/lua/shared/Translate/EN/UI.json. It reaches Java through the static @LuaMethod(global = true) functions in LuaApi (dhState, dhAvatars, dhSelectAvatar and the rest). They have to sit in the root package, beside Main and Patches, because ZombieBuddy scans only that one. The list, select and restart logic is the bridge’s AvatarChooser, the same one the Minecraft menu uses.
-Ddh.script=death sets a clear noon and zooms in. It sets zombies on the survivor, and kills it outright if they have not after ten seconds. It then waits for the corpse and turns Keep my avatar after death off and on again while the corpse lies there. Next it clears the zombies, forces the corpse to rise with reanimateNow, and walks the zombie that was you away and back. Every stage is a waypoint, and the log counts how many times the corpse was drawn as the avatar.
-Ddh.script=lighting stands the survivor, unclothed so one skin compares across runs, at noon, dusk, night outdoors and in heavy fog. It then goes to a windowless room out of every street lamp’s reach at night, with the building’s lights off, under the save’s Night Darkness and again under Pitch Black, and ends at night indoors by a warm lamp. At each it captures the avatar and the vanilla survivor, and logs their mean color over a fixed torso crop. It also logs what the avatar adds where the vanilla front shot is black, split into bright pixels (glowBright, eyes) and faint ones (glowFaint, a translucent unlit layer); in the dark rooms that is only what the materials light themselves.
stageRun stages a scripted, throwaway Project Zomboid install and drives it end to end (menu, character creation, a scripted walk/sneak/aim, screenshot/video capture) for repeatable acceptance runs — this is the day-to-day dev loop, never mltn’s real game. It sets -Ddh.harness=true, which gates the harness package’s Watchdog, RunCapture and DemoDirector; a real Steam launch never sets this property, so none of that machinery runs there. Under it the game also stays in the background: harness/Unfocused turns off GLFW’s focus-on-show and skips PZ’s own glfwFocusWindow, and stageRun pins focusloss=false and lockCursorToWindow=false, so a run keeps rendering and ticking unfocused and never grabs the cursor. Only the window’s first show, before any mod loads, can still take the focus for about a second.