Offline library
Design only Nothing here is built. Every design question has been ruled on by mltn (see Decided); what remains is listed under Still to verify.
mltn’s brief (2026-10-03): “When I eventually do a Steam release maybe for this, I’m just gonna ship stuff, and I think I’m gonna just have a local folder with all the available libraries and versions of stuff. So it’s available locally, and you don’t have to connect to the internet and pull shit from the web. It’s just on your computer, like in the good old days.”
Summary
Section titled “Summary”- The library is a plain folder beside the app, readable by a person. It holds every DH mod version, the loaders DH depends on, the shared libraries, and the importer recipes, each in a version folder with one manifest.
- A manifest is data. It names the package, its version, its files with their hashes, what it requires, where each file goes in the game, and the game’s quirks. Quirks are a small closed set of operations the installer implements once, never per-game code.
- Every install writes a receipt in the game. Uninstall removes exactly what the receipt lists, and only while each file still has the hash DH wrote. A file DH did not place is never touched.
- Nothing is fetched from the web by DH. The one exception is not DH’s: MelonLoader and BepInEx 6 fetch Unity’s runtime libraries once on an IL2CPP game’s first launch, and Studio says so. A Steam update replaces the library folder and nothing else. It never changes an installed game; Studio shows “Update v1 → v2” and waits.
- The dev loop is the shipped path. A build writes a package into a dev library, and installing into a game is the same installer Studio uses. Builds stop copying into game folders.
- One reader. A new
DigitalHeaven.Distributionproject, referencing Core only, reads manifests and receipts and runs installs for Studio, the CLI and the tests alike.
Why a folder, and why beside the app
Section titled “Why a folder, and why beside the app”Two constraints from the distribution research shape this. A Steam depot may not write into another game’s folder, so DH’s mods ship in DH’s own depot and a mod manager places them at run time, the way r2modman and Gale do. And DH depends on no service mltn hosts. A library folder that ships with the app satisfies both: Steam delivers it as ordinary depot content, the installer copies from it, and nothing on the network is ever asked.
Beside the app, not in the workspace, because the library is DH’s property and the workspace is the user’s. Steam updates and “Verify integrity of tool files” rewrite the app folder freely. They must never get near source pallets.
Layout
Section titled “Layout”The library is library/ (name decided 2026-10-03) beside the app’s executable. On a Steam install that is steamapps/common/DigitalHeaven/library/. On a zip or installer copy it is the same relative path.
library/ library.json format version, the library's name platforms/ com.stresslevelzero.bonelab/ platform.json detection, folders, loader choice, process names mod/ 0.1.0/ dh-package.json files/Mods/DigitalHeaven.Mods.Bonelab.dll 0.2.0/ dh-package.json files/Mods/DigitalHeaven.Mods.Bonelab.dll com.mojang.minecraft/ platform.json mod/0.2.0+26.3/ ... one variant per game version recipe/0.1.0/ the block import, today's `dh import minecraft` com.facepunch.gmod/ platform.json mod/0.2.0/ ... loaders/ org.lavagang.melonloader/0.7.1/ shared by BONELAB and Liar's Bar io.bepinex.bepinex/5.4.23.3-win-x64/ io.bepinex.bepinex/6.0.0-be.735-il2cpp-win-x64/ com.github.zed-0xff.zombiebuddy/<version>/ libraries/ io.mltn.digitalheaven.core/0.2.0/ one folder per target framework inside net.fabricmc.fabric-api/0.161.0+26.3/ avatars/ <base-id>/<version>/ descriptors for paid avatar bases, no assets NOTICES/ every redistributed license, by packageMods and game recipes live under their platform ID, the reverse-DNS ID from PlatformRegistry (see Platform IDs). An alias is accepted wherever a person types a name, but a folder on disk is always the ID, so two copies never compete.
Loaders and libraries are top level (decided 2026-10-03), because one MelonLoader build serves BONELAB and Liar’s Bar and one DigitalHeaven.Core serves every Unity mod. A mod reaches them through requires, never by holding a copy. Sharing the storage does not mean sharing the choice: a platform can still pin the exact loader version it gets, per game version where needed (see Loader compatibility and pins).
Each version folder is complete and immutable. A version is never edited after it ships. A fix is a new version. That is what makes rollback trustworthy, and every released version stays in the library for good (decided 2026-10-03).
files/ mirrors the destination. A package’s files sit under the same relative path they take in the game (files/Mods/…, files/UserLibs/…, files/BepInEx/plugins/DigitalHeaven/…), so a person browsing the folder sees where each file goes. The manifest’s install map is still the authority, since some destinations depend on the install (a Prism instance’s minecraft/mods).
What may be redistributed
Section titled “What may be redistributed”| Package | License | Ship in the library | Note |
|---|---|---|---|
| MelonLoader | Apache-2.0 (bundles Il2CppInterop, LGPL-3.0) | ✅ | Ship LICENSE.md and NOTICE.txt. The LGPL component ships as the unmodified binary, with a pointer to its source |
| BepInEx 5 and 6 | LGPL-2.1 (UnityDoorstop LGPL-2.1, HarmonyX MIT) | ✅ | Ship the license and a pointer to the exact source tag. DH never modifies the binaries |
| Fabric API | Apache-2.0 | ✅ | A library for Minecraft, installed into mods/ beside the DH jar |
| Fabric Loader | Apache-2.0 | 🟡 | Redistributable, but a Prism instance gets its loader from the launcher, not from files DH places. See Detection |
| ZombieBuddy | MIT | ✅ | The PZ build downloads it from GitHub today. The library holds it instead |
| Unity runtime libraries a loader fetches | Unity Terms of Service | ❌ | Decided: never shipped. The loader fetches them once on first launch; see Loaders that go online |
| .NET desktop runtime (MelonLoader IL2CPP on Windows) | MIT, Microsoft redistributable | ✅ | MelonLoader installs it on first run when missing |
| Paid avatar bases, game content | The owner’s | ❌ | Never. A descriptor names what the user supplies; see Recipes |
Manifests
Section titled “Manifests”Every version folder holds one dh-package.json. It is generated by the package step, never written by hand.
{ "format": 1, "id": "io.mltn.digitalheaven.mods.bonelab", "version": "0.2.0", "identity": "3FA9C1…", "kind": "mod", "platform": "com.stresslevelzero.bonelab", "requires": [ { "id": "org.lavagang.melonloader", "range": ">=0.7.0 <0.8.0", "role": "loader" }, { "id": "io.mltn.digitalheaven.core", "range": "0.2.0", "framework": "net6.0" } ], "files": [ { "path": "Mods/DigitalHeaven.Mods.Bonelab.dll", "sha256": "…", "size": 812544 } ], "install": [ { "from": "Mods/", "to": "{install}/Mods/" } ], "quirks": [ { "op": "removeIfHash", "path": "{install}/Mods/DigitalHeavenBonelab.dll", "sha256": ["…"] } ]}| Field | Meaning |
|---|---|
id | Reverse-DNS, like a pallet ID, under io.mltn.digitalheaven.* for DH’s own packages. The same across versions. Freely renamable until a public release: before then a rename is a find-and-replace. It only becomes fixed once players have it installed |
version | SemVer 2.0. Precedence decides “newer” |
identity | The hash of every file’s path and hash, in the same shape RecipeIdentity.targets stamps a bridge. Two builds of one version differ here |
kind | mod, loader, library or recipe |
platform | The platform ID, or absent for a shared loader or library |
requires | Package ID, a SemVer range, an optional framework (net6.0, netstandard2.0) and an optional role. loader marks the one the platform must have before anything else installs |
files | Every file, with SHA-256 and size. The installer verifies each one before it copies, so a damaged library fails loudly instead of installing half a mod |
install | Where each file or folder lands. Destinations are tokens; see below |
quirks | Operations beyond copying. Data, from the closed set below |
variants | Optional. Picks among builds by the install’s named game version (see Game versions), such as a Minecraft instance’s 26.2 or BONELAB’s patch6 |
cache | Loaders only. The folders and files the loader generates or downloads in the game, in two groups: generated (rebuilt from the game on the next launch) and downloaded (fetched again on the next launch). Reset loader cache reads it; nothing else deletes these paths |
Destination tokens
Section titled “Destination tokens”| Token | Resolves to |
|---|---|
{install} | The game’s install folder, the one Studio detected or the user added |
{instance} | A launcher instance’s game folder (<prism>/instances/<name>/minecraft) |
{userData} | The game’s own data folder: AppData/LocalLow/<company>/<game>, %USERPROFILE%/Zomboid, and so on. Under Proton, inside the game’s compatdata prefix |
{loaderPlugins}, {loaderLibs} | The loader’s folders: Mods/UserLibs for MelonLoader, BepInEx/plugins/BepInEx/core for BepInEx |
Tokens keep a manifest the same on Windows and Linux. The platform descriptor resolves them per OS, so no manifest is written for one OS.
Quirks as data
Section titled “Quirks as data”The installer implements a fixed vocabulary, each operation reversible and recorded in the receipt. A manifest picks from it. A game that needs something outside the vocabulary gets a new operation in the installer, reviewed once and then available to every manifest. A manifest never carries a script.
| Operation | Used for | Reversal |
|---|---|---|
place (implied by install) | Every file a package installs | Delete if the hash still matches |
removeIfHash | Retiring a file an older DH build left: Schedule I’s DigitalHeaven.Games.ScheduleOne.dll, BONELAB’s pre-rename names | Restored from the receipt’s backup |
ensureFolder | A loader folder that must exist (UserLibs) | Removed only if DH created it and it is empty |
jsonEdit | Project Zomboid’s launcher vmArgs, where ZombieBuddy’s agent must be first | The backup, unless the file changed since, in which case only DH’s entry is taken out |
lineInFile | Project Zomboid’s default.txt mod line | The line is removed; the file is left otherwise |
hashApproval | ZombieBuddy’s mod_approvals.json entry for the DH jar | The entry is removed |
iniSet | A loader setting in its config file (such as BepInEx/config/BepInEx.cfg) | The previous value is restored |
requireFirstRun | A loader that must start the game once before a mod can load | None: a check, shown in Studio as a step |
BONELAB’s “UserLibs as well as Mods” is not a quirk at all. It is two lines of the install map: the mod to {loaderPlugins}, the DigitalHeaven libraries it requires to {loaderLibs}. Most of what reads as a quirk today is a destination.
The platform descriptor
Section titled “The platform descriptor”platform.json is the per-game data Studio needs before any package is involved:
- Detection: the Steam app ID, the folder name under
steamapps/common, and a marker file that proves a folder is the game (BONELAB_Steam_Windows64.exe). - Process names for the running-game check, matched the way
GameInstallCommon.psm1’sAssert-GameStoppedmatches them today (name and command line, and an unreadable command line counts as a match). - Folders for the open buttons: install, user data and logs, as tokens.
- The loader the platform uses: its package ID, the version range that works, and an optional exact pin. Both can be set per game version (below).
- Game version markers and known builds: how Studio tells which build of the game is installed, and what that build is called. See Game versions.
- Display: the name comes from
PlatformRegistry, never from the descriptor, so one list stays the list.
Because detection, folders and quirks are data, a Steam update can add a game to Platforms without a new Studio, as long as the existing operations cover it.
Game versions
Section titled “Game versions”A game’s version is read from build markers that each platform.json declares for itself (decided 2026-10-03). No marker is the default for every platform: what identifies a build is different for every game, and some games have no Steam build at all (Minecraft).
"gameVersion": { "markers": [ { "kind": "fileHash", "path": "GameAssembly.dll" }, { "kind": "steamBuild" } ], "knownBuilds": [ { "name": "patch5", "label": "BONELAB patch 5", "fileHash": "9C41…", "steamBuild": "13372512" }, { "name": "patch6", "label": "BONELAB patch 6", "fileHash": "E0B7…", "steamBuild": "18000144" } ]}- Markers are ordered, first match wins. Studio reads each marker in turn and looks its value up in
knownBuilds. The first marker whose value names a known build decides; a marker that cannot be read (no Steam manifest on a manual install) or whose value is not in the table falls through to the next. knownBuildsmaps marker values to a named game version.nameis the stable key the rest of the data uses;labelis what Studio shows. The list is in release order, so “frompatch6on” means something. A build may list a value for several markers, so a Steam copy and a copy added by hand resolve to the same name.- Ranges, pins and mod variants key off the name, never off a raw marker value. A raw Steam
buildidmeans nothing on its own (a beta branch has its own, and two builds can ship the same game), so it only ever counts through this table. - An unrecognized build reads as “unrecognized version”. Studio uses the platform’s default range and pin and says so on the tile: “BONELAB build not recognized, using the default MelonLoader range. A newer DigitalHeaven may know it.” It never guesses the nearest version.
Marker kinds are a fixed set, like the quirk operations:
| Kind | Reads | Good for |
|---|---|---|
fileHash | The SHA-256 of a file in the install: GameAssembly.dll, the exe, a data file | Exact identification of a build. For an IL2CPP game, GameAssembly.dll is the file a loader’s generated assemblies depend on |
unityVersion | The Unity version string in the player’s globalgamemanagers | Loader compatibility that follows the engine build rather than the game’s own number |
exeVersion | The executable’s file version resource | Games that stamp their version into the exe |
versionFile | A file and a pattern, for a game that writes its version down (a version.txt, a JSON field) | Games whose version is printed in their data, on any store |
steamBuild | buildid in the game’s appmanifest_<appid>.acf, the file detection already reads | One marker a platform may list. Only meaningful through knownBuilds |
prismInstance | The net.minecraft component version in the instance’s mmc-pack.json | Minecraft, per instance. Its value already is a version name, so knownBuilds may simply list the versions DH supports |
custom | A named reader compiled into the installer, for “some magic” specific to one game | A game that needs something none of the above can read. Added to the installer once, reviewed like a quirk operation, then usable by name |
New entries reach knownBuilds the same way new versions reach the library: mltn adds the new build’s marker values to platform.json and it ships with the next library update. A dh command that prints every declared marker’s value for an installed game makes that a copy and paste.
Loader compatibility and pins
Section titled “Loader compatibility and pins”"loader": { "id": "org.lavagang.melonloader", "range": ">=0.7.0 <0.8.0", "pin": "0.7.1", "variants": [ { "when": { "until": "patch5" }, "range": ">=0.6.4 <0.7.0", "pin": "0.6.6" } ]}rangeis what works: a loader inside it is used as found and never nagged about.pinis the exact version DH installs, and the one it suggests when the installed loader falls outside the range. Without a pin, DH offers the newest library version inside the range. A pin is a feature, not a policy: most platforms may never set one, but any platform must be able to.variantsoverriderangeandpinfor particular named game versions, where a loader’s compatibility depends on the game build (an IL2CPP game whose update moved its Unity version, a Minecraft version that needs a newer Fabric API).whentakes a list of names (in) or a span of the release order (from,until). The first variant whosewhenmatches wins; otherwise the top-level values apply, and they are also what an unrecognized build gets.- Validation:
dh packageand the release build refuse a descriptor whose pin is not in the library or lies outside its own range, and a variant that names a version missing fromknownBuilds. A pin that cannot be installed is caught before it ships.
Comparing versions
Section titled “Comparing versions”Studio reads three things for a platform tile: the receipt (what is installed), every manifest of that package across the library and any added sources, and the platform descriptor.
| Receipt says | Library has | Tile shows |
|---|---|---|
| Nothing, no DH files present | Any version | Install 0.2.0 |
| 0.1.0 | 0.2.0 | Update 0.1.0 → 0.2.0 |
| 0.2.0, identity A | 0.2.0, identity B | Rebuild 0.2.0 (a dev build; release versions never share a version) |
| 0.2.0 | 0.2.0 only | Installed, up to date |
| 0.3.0 | Up to 0.2.0 | Installed 0.3.0, not in the library. Rollback still lists what is |
| Nothing, but files match a manifest’s hashes | 0.1.0 | Installed by hand, matches 0.1.0, with an offer to adopt it under a receipt |
| Nothing, but DH-named files match no manifest | — | Unknown DH build, with an offer to replace it |
“Newer” is SemVer precedence on version. Build metadata after + never decides it, which is why identity exists.
The loader gets its own row in the tile’s detail, compared against the range and pin for the detected game version:
| Loader found | Range and pin (for this game version) | Tile shows |
|---|---|---|
| None | Pin 0.7.1 | Install offers MelonLoader 0.7.1 in the same click as the mod |
| 0.7.3, installed by the user | Range >=0.7.0 <0.8.0, pin 0.7.1 | MelonLoader 0.7.3, yours, compatible. Studio stays quiet about the pin (decided 2026-10-03) |
| 0.7.1, installed by DH | Pin 0.7.1 | MelonLoader 0.7.1, installed by DigitalHeaven |
| 0.6.6, installed by the user | Range >=0.7.0 <0.8.0, pin 0.7.1 | MelonLoader is too old for this BONELAB build: suggests 0.7.1 under Needs you |
| 0.7.1, installed by DH | Library now pins 0.7.2 for this build | Update MelonLoader 0.7.1 → 0.7.2, suggested, never applied on its own |
| 0.7.1 | Game updated; its variant now needs >=0.8.0, none in the library | This BONELAB build needs a newer MelonLoader than the library holds. The DH mod stays installed but will not load |
| A loader DH cannot read a version from | Any | MelonLoader, version unknown, with an offer to replace it |
| 0.7.1 | Game build matches no knownBuilds entry | Unrecognized BONELAB version: the default range and pin apply, with a note saying so |
Install, update and uninstall
Section titled “Install, update and uninstall”The receipt
Section titled “The receipt”Every install writes dh-receipt.json into a DH-owned folder in the game, {install}/.digitalheaven/. It lists, per package: the ID, version and identity, every file placed with the hash DH wrote, every folder DH created, and every quirk applied with what is needed to reverse it (a backup path, the previous INI value, the line that was added). Backups of edited files sit beside it.
The receipt lives in the game rather than in DH’s config (decided 2026-10-03) so that it travels with the game. A library moved to another drive, a Steam reinstall of DH, a second DH install on the same machine: all of them read the same truth. GMod’s addon installer already works this way, with install-manifest.json inside the addon folder; this generalizes it.
Uninstall walks the receipt in reverse. A file is deleted only if its hash is still the one DH wrote; a file the user changed is kept and listed in the result. Config the mod writes at run time (MelonLoader’s UserData/, BepInEx’s config/) was never placed, so it is never in the receipt and never removed. That is the user’s data.
Install and update are one transaction
Section titled “Install and update are one transaction”- Resolve: the package, its
requiresclosure, the variant for this install. - Verify every source file against its manifest hash.
- Check that the game is not running (below), and that every destination is writable.
- Stage the new files beside their destinations and write the new receipt as pending.
- Swap: rename staged files into place, back up and apply quirks, remove files the old version had and the new one does not.
- Commit the receipt.
A failure at any step before 6 rolls back from the pending receipt, so a game is never left with half of two versions. An update is an install whose previous receipt is non-empty; nothing about it is special.
Loaders
Section titled “Loaders”The descriptor names the loader and its marker (MelonLoader/net6/MelonLoader.dll, BepInEx/core/BepInEx.dll, winhttp.dll or version.dll beside the exe). Its version is read from the marker’s file version, without loading it. The range and pin come from the descriptor’s variant for the detected game version (see Loader compatibility and pins).
How ownership is known. The receipt in .digitalheaven/ lists everything DH placed or replaced. Anything not in it is the user’s. There is no other record and no guess: a loader file is DH’s exactly when the receipt lists it with the hash now on disk.
Three situations are first class:
- The user’s own loader, installed before DH was ever on the machine, by hand or by r2modman. Its files are not in the receipt, so it is the user’s. DH uses it as found while it is in range and never claims or removes it. A loader in range that is not the pinned version is left alone, and Studio stays quiet about it (decided 2026-10-03): the pin decides what DH installs, not what it nags about.
- DH installs the loader, because none is present. Installing the mod offers the missing loader in the same click (decided 2026-10-03): the confirmation reads “Install DigitalHeaven 0.2.0 and MelonLoader 0.7.1 from the library”, never a silent extra. The loader’s files go into the same receipt, which is what makes them DH’s.
- The installed loader is too old for the game, or for this build of the game, whoever installed it. Studio suggests updating it as part of the ordinary flow: a step under Needs you that says what is wrong and what the update touches, for example “BONELAB’s current build needs MelonLoader 0.7 or newer. You have 0.6.6. Updating it also affects the 4 other mods in Mods/.” The count is every mod in the loader’s folders that the receipt does not list, so Studio always remembers the loader is shared. Nothing happens until the person accepts. Declining is remembered for that game build and loader version, and the DH mod is shown as installed but not loading.
Updating a loader the user installed (decided 2026-10-03). Before writing, Studio asks: “Back up your current loader?” If the person says yes, the files about to be replaced are copied into .digitalheaven/backups/, and the receipt notes that the backup exists. Either way, the new loader files DH writes go into the receipt, so from then on they are DH’s.
- Uninstall still just uninstalls. Removing the loader removes what the receipt lists and never puts the backup back on its own. In mltn’s words, “we shouldn’t be assuming they wanted to revert by uninstalling.”
- The backup is its own action. While a backup exists, the tile’s detail says so (“A backup of your MelonLoader 0.6.6 is kept”) and offers Restore backup as a separate button, with the same running-game check as an install. Restoring writes the backed-up files back, takes them out of the receipt (they are the user’s again), and removes the backup. The backup can also be deleted on its own.
A Steam update to the game is when a loader most often falls out of range. Studio re-reads the game version each time it opens, so the suggestion appears the first time Studio sees the new build, before the person launches into a game where the mod silently fails.
Three more rules apply whoever owns the loader:
- First run: a loader that generates assemblies on first launch (MelonLoader’s
Il2CppAssemblies, BepInEx 6’sinterop) shows “Start BONELAB once” as a pending step under Needs you. The mod can be installed before it; it just won’t load until it has run. - Uninstall of a loader happens only on its own button, never as a side effect of removing the DH mod, and only for a loader the receipt says DH installed. Studio warns that other mods in
Mods/orBepInEx/plugins/will stop loading. The loader’s generated folders are listed in its manifest asgeneratedand are removed only when the person confirms that too. - After a loader update, Studio offers “Reset the loader cache?” once, since stale generated files are the usual cause of a loader that breaks after updating. See below.
Loaders that go online
Section titled “Loaders that go online”Two loaders fetch files on first launch, and “nothing is fetched” has to account for them. Decided (2026-10-03): DH does not pre-place Unity’s runtime libraries. The loader fetches them once, on the game’s first launch, and Studio says so.
- MelonLoader downloads Unity’s runtime libraries (
MelonLoader.UnityDependencies) and Cpp2IL from GitHub on an IL2CPP game’s first launch, and skips each download once the file is in place. - BepInEx 6 IL2CPP downloads Unity base libraries from
unity.bepinex.devon first launch, intoBepInEx/unity-libs.
Pre-placing them would mean redistributing Unity’s runtime assemblies, which Unity’s terms govern, not the loaders’ licenses. So the loader keeps its own first-run download, and Studio states it on the “Start BONELAB once” step: “MelonLoader downloads Unity libraries on BONELAB’s first launch. DigitalHeaven itself never connects.” After that first run the files are cached in the game folder and nothing is fetched again. Both loaders also have a documented offline path (a file already at MelonLoader’s expected path; BepInEx 6’s UnityBaseLibrariesSource set to a bare file name), which a person can use by hand; DH does not drive it.
Reset loader cache
Section titled “Reset loader cache”A troubleshooting action, in each tile’s ⋮ menu and on its detail page. mltn: both loaders “can suffer from issues when you update the loader and you have cache… typical troubleshooting, and it would be nice if it was easy.” It deletes the files the loader generates, so they are rebuilt on the next launch.
- Data-driven. The paths come from the installed loader’s manifest, its
cachesection, so a new loader or a loader that moves its folders needs no Studio change. Studio only deletes paths listed there, under the game folder, and resolves each before deleting so nothing outside the game can be reached. - Two levels. By default it removes
generated. A second checkbox, off by default, also removesdownloaded, labeled with what it costs: “These are downloaded again on the next launch, which needs an internet connection.” - Refuses while the game runs, with the same check and message as an install.
- Never touches mods, plugins, config (
UserData/,BepInEx/config/), logs or user data. None of those are in acachesection, and a manifest that listed one would fail validation indh package. - Works on any loader, including the user’s own: it deletes regenerable state only, never the loader itself. When the installed loader has no manifest in the library (a version DH does not hold), Studio uses the manifest of the nearest version of the same loader and shows which paths it will delete before it does.
- Offered after a loader update, once, as “Reset the loader cache?”, and listed under Needs you until it is answered.
What the loaders actually write, checked against their sources:
| Loader | generated | downloaded | Confidence |
|---|---|---|---|
| MelonLoader 0.7 (IL2CPP) | MelonLoader/Il2CppAssemblies/, and MelonLoader/Dependencies/Il2CppAssemblyGenerator/Config.cfg | MelonLoader/Dependencies/Il2CppAssemblyGenerator/UnityDependencies/ with its UnityDependencies_*.zip, and the Cpp2IL download in the same folder | 🟡 Folder names are from MelonEnvironment.cs and the generator’s package classes on master. The Cpp2IL file name was not confirmed |
| MelonLoader (Mono) | None | None | ✅ A Mono game has nothing to generate |
| BepInEx 6 (IL2CPP) | BepInEx/interop/, BepInEx/cache/ | BepInEx/unity-libs/ | 🟡 interop, unity-libs and assembly-hash.txt are from Il2CppInteropManager.cs on master. Whether the IL2CPP base path is always BepInEx/ was not confirmed |
| BepInEx 5 (Mono) | BepInEx/cache/ | None | ✅ Paths.CachePath is BepInEx/cache on the v5-lts branch |
Why MelonLoader needs Config.cfg too. MelonLoader decides whether to regenerate by comparing GameAssembly.dll’s hash with the one stored in the generator’s Config.cfg, not by whether Il2CppAssemblies/ exists. Deleting only the folder can leave the game with no generated assemblies and no regeneration. Removing the state file makes the next launch rebuild. Decided (2026-10-03): the reset deletes Config.cfg along with Il2CppAssemblies/, rather than setting the loader’s ForceRegeneration option. The file holds generator state, not user settings, so it belongs in generated. It must be verified with a real reset on BONELAB before it ships.
BepInEx 6 regenerates when BepInEx/interop/ is missing, so deleting the folder alone is enough there.
When the game is running
Section titled “When the game is running”The installer refuses to write while the descriptor’s process is running, and says why: “Close BONELAB to update.” It never closes the game. The request waits in Home’s queue under Needs you and Studio re-checks when the process exits, then asks once more before writing. A file lock found during the swap, after the check passed, rolls the transaction back the same way. The Minecraft docs explain why this matters beyond Windows file locks: a jar replaced under a running JVM crashes it later with invalid LOC header.
Rollback
Section titled “Rollback”Every version the library holds is installable, so rollback is “install an older version”: the Rollback menu on a tile lists every version in the library and every added source, newest first, with the installed one marked. Since version folders never change, an older version installs exactly the bytes it shipped with. A downgrade runs the same transaction as an update, with the same receipt.
Detection
Section titled “Detection”| Source | How Studio finds games | Builds on |
|---|---|---|
| Steam | Each Steam root, then every library its libraryfolders.vdf lists, then appmanifest_<appid>.acf for the descriptor’s app ID, whose installdir names the folder. The marker file confirms it | SourceGameInstall.SteamLibraries, moved to Core |
| Prism Launcher | Prism’s data root, then instances/*/mmc-pack.json. Each instance is its own row, labeled with its name and game version, and offered only when its components include Fabric Loader | MinecraftInstall’s Prism roots, already per-OS |
| Manual | Add a platform: pick a folder, then Studio checks it against every descriptor’s marker and names the match, or asks which platform it is. Kept in Studio’s settings under %APPDATA% | New |
Detection never writes anything. A platform Studio finds is a tile; it is not installed until someone presses Install.
Minecraft is different in one respect: the launcher owns the loader. Prism installs Fabric Loader from its own metadata, so DH places only the DH jar and Fabric API into {instance}/mods and checks that the instance has Fabric. Adding Fabric to an instance is a Prism action, and Studio points at it rather than editing mmc-pack.json. The official launcher is out of scope until it is asked for.
The folder buttons
Section titled “The folder buttons”Each tile and its detail view offer:
- Open install folder:
{install}or{instance}. - Open user data:
{userData}, the AppData or LocalLow folder. - Open logs: the loader’s log (
MelonLoader/Latest.log,BepInEx/LogOutput.log) and Unity’sPlayer.login{userData}, or the game’s own (logs/latest.login a Prism instance,Zomboid/console.txt). - Open in workspace: the platform’s pallet folder, where its imported content lives.
All of them are descriptor data. On Linux they resolve inside the Proton prefix when the game runs under Proton.
Per-platform install quirks
Section titled “Per-platform install quirks”| Platform | Loader | Install map | Quirks | Loader in library | First run offline | Reset loader cache |
|---|---|---|---|---|---|---|
| BONELAB | MelonLoader 0.7 | Mods/ and UserLibs/ | Retire pre-rename DLLs | ✅ | 🟡 the loader fetches Unity libraries once | 🟡 Il2CppAssemblies and generator state; Unity libraries optional |
| Liar’s Bar | MelonLoader | Mods/DigitalHeaven/ | None | ✅ | ✅ Mono, nothing fetched | ❌ nothing to reset |
| MegaBonk | BepInEx 6 IL2CPP | BepInEx/plugins/DigitalHeaven/ | None | ✅ | 🟡 the loader fetches Unity libraries once | 🟡 interop and cache; unity-libs optional |
| ULTRAKILL | BepInEx 5 | BepInEx/plugins/DigitalHeaven/ | None | ✅ | ✅ | ✅ cache |
| Risk of Rain 2 | BepInEx 5 | BepInEx/plugins/DigitalHeaven/ | None | ✅ | ✅ | ✅ cache |
| Schedule I | BepInEx 5 | BepInEx/plugins/DigitalHeaven/ | Retire DigitalHeaven.Games.ScheduleOne.dll | ✅ | ✅ | ✅ cache |
| Minecraft (Prism) | Fabric, by the launcher | {instance}/mods/ | Variant per game version (26.2, 26.3) | 🟡 Fabric API only | 🟡 Prism fetches its own loader | ❌ the launcher’s job |
| Project Zomboid | ZombieBuddy | Mod folders, two files beside the exe | jsonEdit vmArgs, lineInFile, hashApproval | ✅ | ✅ | ❔ ZombieBuddy’s cache not yet surveyed |
| Garry’s Mod | None | garrysmod/addons/digitalheaven/, garrysmod/lua/bin/ | Linux needs the x86-64 branch | ✅ none needed | ✅ | ❌ no loader |
| POSTAL 2 | None (Workshop mutator) | Four files in System/ | None | ✅ none needed | ✅ | ❌ no loader |
| Resonite | None | Nothing installed | ResoniteLink push only | ❌ not applicable | ✅ | ❌ no loader |
PEAK (com.landcrab.peak, alias peak) and Lethal Company (com.zeekerss.lethalcompany, alias lethalcompany) are in PlatformRegistry (decided 2026-10-03). Each still needs a platform.json before it gets a tile.
Offline by default
Section titled “Offline by default”DH’s install path contains no HTTP client. The distribution project takes no network dependency, and a test asserts it, so a future convenience cannot add one quietly.
What a Steam update changes: the contents of library/, and nothing else. New versions appear, and a version mltn stops shipping disappears from the folder. Installed games are untouched, because receipts live in the games. The next time Studio opens it compares again, and tiles that have something newer show “Update”. Steam’s file verification restores the library if anyone damages it. A user should never put files of their own in library/, because Steam owns that folder and may replace it; their own packages go in an added source.
A non-Steam install carries the same library/ beside the executable, in the zip or laid down by the installer, with the same layout and the same manifests. Updating a zip install means unpacking a newer zip over the folder. Studio cannot tell, and does not need to tell, how the folder arrived.
Added sources are folders the user lists in Studio’s settings: a second drive, a USB stick, a folder a friend handed over, a dev library. Each is read by the same reader and shown with its name beside every version it contributes. If two sources offer the same ID and version with different identities, Studio shows both and installs neither until the person picks one. A manifest from an added source may use quirk operations (decided 2026-10-03): the same closed set the shipped library uses, never a script, so the installer’s reviewed operations are the whole of what any manifest can do. There is no URL source, no index to download, and no service mltn runs.
No signing (decided 2026-10-03). Studio trusts the library folder as Steam or the zip delivered it. The SHA-256 of every file is still checked before it is copied, so damage or a partial update is caught; the hashes are an integrity check, not a claim about who built the package.
Dev loop
Section titled “Dev loop”Today each mod project copies into its game folder after build, with its own flag: BONELAB only with -p:Deploy=true, ULTRAKILL and MegaBonk unless -p:SkipDeploy=true, Schedule I unless -p:DisableGameDeploy=true, the rest whenever the folder exists. The Gradle mods, GMod and POSTAL 2 have their own scripts (install-prism.ps1, install-steam-pz.ps1, install.ps1, install-runtime.bat, install-linux.sh). Every one of them is a separate installer.
The replacement has two steps, and the shipped path uses the same two:
- Build, then package. After a build, a shared
Platforms/Package.targetsrunsdh package, which writes a version folder into the dev library (artifacts/library/, gitignored), generatingdh-package.jsonfrom the output and a per-project package template that carries the install map and quirks. The version is the project’s version plus its identity (0.2.0+3fa9c1), so two builds of one version never overwrite each other. The Gradle builds and the GMod and POSTAL 2 build scripts call the samedh packagewith their staging folder. A build never touches a game. - Install.
dh install bonelabinstalls the newest package from the libraries Studio reads, through the same installer, with the same receipt, the same running-game check and the same rollback. Studio’s Install button does the same thing.
The dev library is just another source, so mltn’s Studio shows “Rebuild 0.2.0” on BONELAB after a build, exactly as a player’s Studio shows “Update” after a Steam update. BONELAB’s existing rule, that a plain build from any session must never replace the build under test, becomes the rule for every game, because installing is always a separate step.
A release is the same step run by CI: the release builds package into a clean library/, which becomes the Steam depot’s content and the zip’s folder. The dev library keeps the newest builds of each package and prunes older dev builds; release versions in the shipped library are never pruned: every released version ships forever (decided 2026-10-03).
The per-game install scripts are removed once the installer covers their game, not kept beside it. GMod’s remote Linux install (install-linux.sh --ssh deck@host) has no equivalent here; it stays until a remote target is designed.
Recipes and avatar descriptors
Section titled “Recipes and avatar descriptors”A recipe package is a descriptor, not code. It names the importer compiled into DH that does the work, the ingredients the user supplies, and the pallet it produces, with the shared platform or pallet ID everyone gets (see Platform IDs).
- Game recipes:
com.mojang.minecraft’s recipe names the Minecraft importer (MinecraftPalletImporter, today’sdh import minecraft) and says its input is a client jar from detection. The POSTAL 2 and Source importers become recipes the same way. A recipe’s version moves when the importer’s output would change, so Studio can say “Re-import: the Minecraft recipe changed” in the queue. - Avatar descriptors: for a paid base, the descriptor lists the package file names and SHA-256 hashes of the versions it knows (a specific Unity package or zip), and the conversion settings. The user drops the file they bought; Studio matches the hash, runs the importer, and writes a local pallet under the base’s shared ID. An unknown hash is refused with the versions the descriptor does know.
Nothing a user owns or bought is ever in the library: the ingredients stay with the user. A recipe is how DH ships knowledge without shipping content.
What reuses what
Section titled “What reuses what”| Piece | Builds on | New |
|---|---|---|
| Platform IDs in folders and manifests | PlatformRegistry.Resolve, Spellings | Nothing |
| Detection test seam | Core.Installs.InstallHost | Nothing |
| Steam libraries | SourceGameInstall.SteamLibraries and SteamRoots, moved into Core.Installs so GMod’s export and the installer share one reader | appmanifest_*.acf lookup by app ID |
| Prism instances | MinecraftInstall’s Prism and MultiMC roots | Instance listing from mmc-pack.json |
| Build identity | The RecipeIdentity.targets hashing shape | The same shape over a package’s output files |
| Running-game check | Assert-GameStopped’s rule (name plus command line, unreadable counts as running) | Its C# form, the one copy |
| Receipt | GMod’s install-manifest.json and install-manifest.txt | Hashes, folders and quirk reversal |
| Quirk operations | install-steam-pz.ps1’s launcher edit and backups, Schedule I’s legacy-DLL removal | The operations themselves, in C# |
| Install maps and exclusions | Each csproj’s DeployToGame item lists | Per-project package templates; the targets are deleted |
| Recipes | MinecraftPalletImporter, the POSTAL 2 and Source importers | The recipe descriptor and importer registry |
| Platforms mode | Studio’s Platforms tiles | Reads everything through DigitalHeaven.Distribution |
DigitalHeaven.Distribution is the new project, under Toolchain/, referencing Core only and never the Compiler or Imaging. It owns the manifest, descriptor and receipt models, the one reader, version comparison, the installer and its operations, and dh package. Studio, the CLI and the tests call it; none of them parse a manifest themselves.
Decided
Section titled “Decided”- The folder is
library/(2026-10-03). - Loaders and shared libraries are top level and shared (2026-10-03), and a platform can pin an exact loader version, per game version where needed. All three loader situations (the user’s own, DH installs one, too old) are first class, and an out-of-date loader is suggested under Needs you, never updated silently.
- The receipt lives inside the game, in
.digitalheaven/(2026-10-03). - Every released version ships forever (2026-10-03). Release versions are never pruned from the shipped library; only dev builds are.
- Unity’s runtime libraries are never shipped (2026-10-03). MelonLoader and BepInEx 6 fetch them once on first launch, and Studio says so on the first-run step.
- No package signing (2026-10-03). Studio trusts the library as delivered; the SHA-256 hashes still catch damage.
- Added sources may use quirk operations (2026-10-03), from the same closed set, never scripts.
- Installing a mod offers its missing loader in the same click (2026-10-03).
- A user’s loader in range but not the pin gets no suggestion (2026-10-03). Studio stays quiet.
- Updating a user’s loader asks “Back up your current loader?” (2026-10-03). Uninstall never restores the backup; Restore backup is its own action. The receipt is the only record of ownership: anything not in it is the user’s.
- Lethal Company’s platform ID is
com.zeekerss.lethalcompany(2026-10-03), aliaslethalcompany. - Game versions come from per-platform build markers (2026-10-03): an ordered list, first match wins, mapped through a known-builds table to named versions that ranges, pins and variants key off. The Steam
buildidis one marker a platform may list, never the default. An unknown build is “unrecognized version” and gets the default range. - DH’s package IDs are
io.mltn.digitalheaven.*(2026-10-03). They stay freely renamable until a public release, and only become fixed once players have them installed. - PEAK’s platform ID is
com.landcrab.peak(2026-10-03), aliaspeak. - Reset loader cache is a per-game troubleshooting action (2026-10-03), driven by each loader manifest’s
cachesection and offered after every loader update. - The MelonLoader reset deletes the generator’s
Config.cfgalong withIl2CppAssemblies/(2026-10-03), verified with a real reset on BONELAB before it ships.
Still to verify
Section titled “Still to verify”Every design question is resolved. These need a real-world test before the piece they describe ships:
- The MelonLoader reset on BONELAB: deleting
Il2CppAssemblies/and the generator’sConfig.cfgmakes the next launch regenerate, and the game loads mods afterward. - BepInEx 6’s IL2CPP base path: that
interop/,cache/andunity-libs/always sit underBepInEx/(MegaBonk). - MelonLoader’s Cpp2IL download: its exact file and folder name under
Dependencies/Il2CppAssemblyGenerator/, for the reset’sdownloadedlist. - ZombieBuddy’s cache: whether it generates anything a reset should clear.
- Reading loader versions: that the marker DLL’s file version gives the real loader version for MelonLoader 0.7 and BepInEx 5 and 6, without loading it.
- Game version markers: that
unityVersioncan be read fromglobalgamemanagerson the Unity games DH supports, and thatGameAssembly.dllhashes and Steambuildidvalues tell BONELAB’s and MegaBonk’s builds apart.