Skip to content

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.

FeatureStatus
Avatar replacement (local player, world render)Yes
Held items at the avatar’s handsYes
Occlusion by walls, fences, furniture and treesYes
Spring bones, with wall and floor contactYes
Spring collidersYes
Lit like a vanilla survivor: night, room and lamp light, fogYes
After death: the dying body, the corpse and the zombie it rises asYes
Character dolls (Info tab, DigitalHeaven tab, character creation)Yes
In-game avatar menu (character window tab, creation screen)Yes
Live avatar swap, no restartYes
World and doll resolution settingsYes
Other players / NPCs / split-screen playersNo
OverlayNo

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.

Game versionProject Zomboid build 42.21 or later (the Java build; not the legacy Kahlua build)
Mod loaderZombieBuddy, as a native Java agent
EngineA 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.

The install script sets up a real Steam install for the normal “Play” button — no harness, no scripted menu.

  1. Build the engine host (DigitalHeaven.Engine.Host, Release configuration)
  2. From Platforms/DigitalHeaven.Mods.ProjectZomboid, run:
    Terminal window
    powershell -NoProfile -ExecutionPolicy Bypass -File tools\install-steam-pz.ps1
  3. 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 DigitalHeaven and ZombieBuddy mod folders into the cachedir’s mods/
  • copies zbNative.dll/ZombieBuddy.jar into the game install root, beside ProjectZomboid64.exe
  • adds both mods to default.txt’s mods { } 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.json under the cachedir, only where a value isn’t already set
  • rewrites ProjectZomboid64.json’s vmArgs: -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.

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
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.

ControlWhat it doesApplies
Doll (left)The same DH doll the Info tab draws. Drag it to turn.—
AvatarThe 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 resolutionrenderScale, 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 resolutionuiRenderScale, 25–200 % in 5 % steps, for every doll of your character.on every step
Show avatar in the worldOff draws the vanilla survivor in the world, and the dolls keep the avatar.at once
Show avatar on character dollsOff draws the vanilla doll, and the world keeps the avatar.at once
Keep my avatar after deathOff hands your body back to Project Zomboid the moment you die: the dying survivor, the corpse and the zombie are vanilla.at once
Status lineEngine 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 engineStops 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 detailsShown 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 %.

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 ...):

OSWhere the frame lives
WindowsA 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.
LinuxA file on a tmpfs: /dev/shm, or $XDG_RUNTIME_DIR when that is the tmpfs.
macOSA 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 abovedigitalheaven-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.

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.

KeyDefaultDescription
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
renderScale1.0World avatar resolution: the share of its on-screen pixels the engine renders, clamped to 0.25–2.0. The tab’s World avatar slider
uiRenderScale1Doll resolution, clamped to 0.25–2.0. The tab’s Dolls slider
holdFrames2How 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
showInWorldtrueDraw the avatar over your survivor in the world. The tab’s Show avatar in the world tick
showOnDollstrueDraw the avatar on your character’s dolls. The tab’s Show avatar on character dolls tick
keepAfterDeathtrueKeep 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.

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.

  • 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.

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).

Terminal window
cd Platforms/DigitalHeaven.Mods.ProjectZomboid
./gradlew.bat build

The 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.

Terminal window
./gradlew.bat stageRun

The 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.