Skip to content

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

  • 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.Distribution project, referencing Core only, reads manifests and receipts and runs installs for Studio, the CLI and the tests alike.

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.

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 package

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

PackageLicenseShip in the libraryNote
MelonLoaderApache-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 6LGPL-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 APIApache-2.0✅A library for Minecraft, installed into mods/ beside the DH jar
Fabric LoaderApache-2.0🟡Redistributable, but a Prism instance gets its loader from the launcher, not from files DH places. See Detection
ZombieBuddyMIT✅The PZ build downloads it from GitHub today. The library holds it instead
Unity runtime libraries a loader fetchesUnity 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 contentThe owner’s❌Never. A descriptor names what the user supplies; see Recipes

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": ["…"] }
]
}
FieldMeaning
idReverse-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
versionSemVer 2.0. Precedence decides “newer”
identityThe hash of every file’s path and hash, in the same shape RecipeIdentity.targets stamps a bridge. Two builds of one version differ here
kindmod, loader, library or recipe
platformThe platform ID, or absent for a shared loader or library
requiresPackage 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
filesEvery 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
installWhere each file or folder lands. Destinations are tokens; see below
quirksOperations beyond copying. Data, from the closed set below
variantsOptional. 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
cacheLoaders 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
TokenResolves 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.

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.

OperationUsed forReversal
place (implied by install)Every file a package installsDelete if the hash still matches
removeIfHashRetiring a file an older DH build left: Schedule I’s DigitalHeaven.Games.ScheduleOne.dll, BONELAB’s pre-rename namesRestored from the receipt’s backup
ensureFolderA loader folder that must exist (UserLibs)Removed only if DH created it and it is empty
jsonEditProject Zomboid’s launcher vmArgs, where ZombieBuddy’s agent must be firstThe backup, unless the file changed since, in which case only DH’s entry is taken out
lineInFileProject Zomboid’s default.txt mod lineThe line is removed; the file is left otherwise
hashApprovalZombieBuddy’s mod_approvals.json entry for the DH jarThe entry is removed
iniSetA loader setting in its config file (such as BepInEx/config/BepInEx.cfg)The previous value is restored
requireFirstRunA loader that must start the game once before a mod can loadNone: 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.

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’s Assert-GameStopped matches 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.

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.
  • knownBuilds maps marker values to a named game version. name is the stable key the rest of the data uses; label is what Studio shows. The list is in release order, so “from patch6 on” 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 buildid means 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:

KindReadsGood for
fileHashThe SHA-256 of a file in the install: GameAssembly.dll, the exe, a data fileExact identification of a build. For an IL2CPP game, GameAssembly.dll is the file a loader’s generated assemblies depend on
unityVersionThe Unity version string in the player’s globalgamemanagersLoader compatibility that follows the engine build rather than the game’s own number
exeVersionThe executable’s file version resourceGames that stamp their version into the exe
versionFileA 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
steamBuildbuildid in the game’s appmanifest_<appid>.acf, the file detection already readsOne marker a platform may list. Only meaningful through knownBuilds
prismInstanceThe net.minecraft component version in the instance’s mmc-pack.jsonMinecraft, per instance. Its value already is a version name, so knownBuilds may simply list the versions DH supports
customA named reader compiled into the installer, for “some magic” specific to one gameA 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": {
"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" }
]
}
  • range is what works: a loader inside it is used as found and never nagged about.
  • pin is 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.
  • variants override range and pin for 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). when takes a list of names (in) or a span of the release order (from, until). The first variant whose when matches wins; otherwise the top-level values apply, and they are also what an unrecognized build gets.
  • Validation: dh package and 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 from knownBuilds. A pin that cannot be installed is caught before it ships.

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 saysLibrary hasTile shows
Nothing, no DH files presentAny versionInstall 0.2.0
0.1.00.2.0Update 0.1.0 → 0.2.0
0.2.0, identity A0.2.0, identity BRebuild 0.2.0 (a dev build; release versions never share a version)
0.2.00.2.0 onlyInstalled, up to date
0.3.0Up to 0.2.0Installed 0.3.0, not in the library. Rollback still lists what is
Nothing, but files match a manifest’s hashes0.1.0Installed 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 foundRange and pin (for this game version)Tile shows
NonePin 0.7.1Install offers MelonLoader 0.7.1 in the same click as the mod
0.7.3, installed by the userRange >=0.7.0 <0.8.0, pin 0.7.1MelonLoader 0.7.3, yours, compatible. Studio stays quiet about the pin (decided 2026-10-03)
0.7.1, installed by DHPin 0.7.1MelonLoader 0.7.1, installed by DigitalHeaven
0.6.6, installed by the userRange >=0.7.0 <0.8.0, pin 0.7.1MelonLoader is too old for this BONELAB build: suggests 0.7.1 under Needs you
0.7.1, installed by DHLibrary now pins 0.7.2 for this buildUpdate MelonLoader 0.7.1 → 0.7.2, suggested, never applied on its own
0.7.1Game updated; its variant now needs >=0.8.0, none in the libraryThis 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 fromAnyMelonLoader, version unknown, with an offer to replace it
0.7.1Game build matches no knownBuilds entryUnrecognized BONELAB version: the default range and pin apply, with a note saying so

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.

  1. Resolve: the package, its requires closure, the variant for this install.
  2. Verify every source file against its manifest hash.
  3. Check that the game is not running (below), and that every destination is writable.
  4. Stage the new files beside their destinations and write the new receipt as pending.
  5. Swap: rename staged files into place, back up and apply quirks, remove files the old version had and the new one does not.
  6. 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.

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’s interop) 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/ or BepInEx/plugins/ will stop loading. The loader’s generated folders are listed in its manifest as generated and 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.

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.dev on first launch, into BepInEx/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.

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 cache section, 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 removes downloaded, 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 a cache section, and a manifest that listed one would fail validation in dh 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:

LoadergenerateddownloadedConfidence
MelonLoader 0.7 (IL2CPP)MelonLoader/Il2CppAssemblies/, and MelonLoader/Dependencies/Il2CppAssemblyGenerator/Config.cfgMelonLoader/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)NoneNone✅ 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.

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.

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.

SourceHow Studio finds gamesBuilds on
SteamEach 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 itSourceGameInstall.SteamLibraries, moved to Core
Prism LauncherPrism’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 LoaderMinecraftInstall’s Prism roots, already per-OS
ManualAdd 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.

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’s Player.log in {userData}, or the game’s own (logs/latest.log in 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.

PlatformLoaderInstall mapQuirksLoader in libraryFirst run offlineReset loader cache
BONELABMelonLoader 0.7Mods/ and UserLibs/Retire pre-rename DLLs✅🟡 the loader fetches Unity libraries once🟡 Il2CppAssemblies and generator state; Unity libraries optional
Liar’s BarMelonLoaderMods/DigitalHeaven/None✅✅ Mono, nothing fetched❌ nothing to reset
MegaBonkBepInEx 6 IL2CPPBepInEx/plugins/DigitalHeaven/None✅🟡 the loader fetches Unity libraries once🟡 interop and cache; unity-libs optional
ULTRAKILLBepInEx 5BepInEx/plugins/DigitalHeaven/None✅✅✅ cache
Risk of Rain 2BepInEx 5BepInEx/plugins/DigitalHeaven/None✅✅✅ cache
Schedule IBepInEx 5BepInEx/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 ZomboidZombieBuddyMod folders, two files beside the exejsonEdit vmArgs, lineInFile, hashApproval✅✅❔ ZombieBuddy’s cache not yet surveyed
Garry’s ModNonegarrysmod/addons/digitalheaven/, garrysmod/lua/bin/Linux needs the x86-64 branch✅ none needed✅❌ no loader
POSTAL 2None (Workshop mutator)Four files in System/None✅ none needed✅❌ no loader
ResoniteNoneNothing installedResoniteLink 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.

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.

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:

  1. Build, then package. After a build, a shared Platforms/Package.targets runs dh package, which writes a version folder into the dev library (artifacts/library/, gitignored), generating dh-package.json from 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 same dh package with their staging folder. A build never touches a game.
  2. Install. dh install bonelab installs 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.

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’s dh 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.

PieceBuilds onNew
Platform IDs in folders and manifestsPlatformRegistry.Resolve, SpellingsNothing
Detection test seamCore.Installs.InstallHostNothing
Steam librariesSourceGameInstall.SteamLibraries and SteamRoots, moved into Core.Installs so GMod’s export and the installer share one readerappmanifest_*.acf lookup by app ID
Prism instancesMinecraftInstall’s Prism and MultiMC rootsInstance listing from mmc-pack.json
Build identityThe RecipeIdentity.targets hashing shapeThe same shape over a package’s output files
Running-game checkAssert-GameStopped’s rule (name plus command line, unreadable counts as running)Its C# form, the one copy
ReceiptGMod’s install-manifest.json and install-manifest.txtHashes, folders and quirk reversal
Quirk operationsinstall-steam-pz.ps1’s launcher edit and backups, Schedule I’s legacy-DLL removalThe operations themselves, in C#
Install maps and exclusionsEach csproj’s DeployToGame item listsPer-project package templates; the targets are deleted
RecipesMinecraftPalletImporter, the POSTAL 2 and Source importersThe recipe descriptor and importer registry
Platforms modeStudio’s Platforms tilesReads 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.

  • 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), alias lethalcompany.
  • 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 buildid is 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), alias peak.
  • Reset loader cache is a per-game troubleshooting action (2026-10-03), driven by each loader manifest’s cache section and offered after every loader update.
  • The MelonLoader reset deletes the generator’s Config.cfg along with Il2CppAssemblies/ (2026-10-03), verified with a real reset on BONELAB before it ships.

Every design question is resolved. These need a real-world test before the piece they describe ships:

  1. The MelonLoader reset on BONELAB: deleting Il2CppAssemblies/ and the generator’s Config.cfg makes the next launch regenerate, and the game loads mods afterward.
  2. BepInEx 6’s IL2CPP base path: that interop/, cache/ and unity-libs/ always sit under BepInEx/ (MegaBonk).
  3. MelonLoader’s Cpp2IL download: its exact file and folder name under Dependencies/Il2CppAssemblyGenerator/, for the reset’s downloaded list.
  4. ZombieBuddy’s cache: whether it generates anything a reset should clear.
  5. 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.
  6. Game version markers: that unityVersion can be read from globalgamemanagers on the Unity games DH supports, and that GameAssembly.dll hashes and Steam buildid values tell BONELAB’s and MegaBonk’s builds apart.