Skip to content

Assets and Harmony

m4bwav edited this page Oct 18, 2026 · 2 revisions

The madnesswright-modtools skill covers the two routes past MadCards: new art through the developer's ModTools project, and new behaviour through a Harmony assembly that the game loads itself. Check the cheaper route first. Swapping one built-in look for another is a card ("ObjRef": "*M_Pipewrench"), and most tuning is a card value. Bosses and other Automatons cannot be modded yet (roadmap, October 2026).

New art in ModTools

ModTools (github.com/MPN-Modders/MPN_ModTools, Unity 2019.2.6f1, no license file) turns models, textures, icons, animations and fonts into .mobj bundles.

  1. Put the model inside an empty parent and add the matching MadObj_ component to the parent (Melee, Ranged, ranged addon, Armor, BodyPart, Character_Ref). Rotate the child, not the parent, so blue Z points forward and green Y up.
  2. Use the component's "Create Points" for the grip, business-end, muzzle and casing points, and fit the Box Collider.
  3. Make the parent a prefab. Its name is the ObjRef value cards use.
  4. Give every asset an AssetBundle name, then build from Madness, AssetBundle. The .mobj lands in %USERPROFILE%\AppData\LocalLow\Gibbing Tree\Madness Project Nexus ModTools\MadObjs\, and mpn.py where lists the newest under tool_outputs.modtools.
  5. Copy the .mobj into the mod with a card whose ObjRef names the prefab, not the bundle.

The skill's references/madobjects.md has the axis rules, the Weapon Animator states and parameters, and the Character_Ref setup.

Harmony assemblies

Since beta v2.19.a (2026-04-15) the game loads Harmony 2.4.2 assemblies named in a mod's info.txt. No BepInEx is needed. What follows was read from release build 25615280 on 2026-10-17 (madnesswright-modtools RESEARCH.md R-20261017-6); re-check it after a game update.

Key Meaning
AssemblyPath the DLL's path relative to the mod folder, for example Managed/MyMod.dll
HarmonyID a unique patch id; spaces become _. Blank gives workshop.<id> for a Workshop item and a new local.<guid> every launch for a local mod, so set it
EntryType the entry class's full name (Namespace.Class, or Class in the global namespace): public, with a parameterless constructor, implementing IModEntrypoint
PatchOrder patches apply in ascending order and are removed in descending order
ManualPatch true stops the automatic PatchAll(); patch yourself in Initialize()
  • IModEntrypoint sits in the game's global namespace, with Initialize() and Unload(). An exception in Initialize() makes the game call Unload(), unpatch everything under your HarmonyID and mark the mod inactive. Disabling the mod also calls Unload().
  • Target net48. The game's Managed folder has no netstandard.dll, and its 0Harmony.dll (2.4.2.0) targets net48; a test build at net472 failed with MSB3274 against it (madnesswright-modtools TESTS.md, run T-20261017-3).
  • Reference Assembly-CSharp.dll, the UnityEngine modules you use and the game's 0Harmony.dll from Managed with Private=false, so none is copied into the mod. Never ship game DLLs or decompiled code.
  • Keep the project folder outside the mod folder. The game reads every .json in a mod as cards, and a build's obj folder is full of them.
  • Other DLLs your mod needs go beside it: the game resolves a missing reference from loaded assemblies first, then from the AssemblyPath folder.

The skill decompiles the user's own Assembly-CSharp.dll before naming any patch target, and quotes the file and line it found. Without a decompile it says the patch is a guess. The project file and an entry class to start from are in the skill's references/harmony.md.

Worked example

examples/SwapUnaware (on main since 2026-10-18, after the 0.1.2 release) is a complete mod built this way: take control of any character (F5), make everyone else unaware (F6, F7), go back to your own body (F8). It has the net48 project, a pack script that finds the game and makes an install zip, and a table of the bugs each playtest found. Two lessons from it:

  • Guard the game's own follow-up code. Dying in a taken-over Playground spawn threw inside KillMe, because the game's death swap reads a squad the spawn did not have; a Harmony prefix handles that state.
  • Change AI state through the game's calls, not its fields. Clearing an NPC's target with myKillTarget = null left it in the target's claimant list, the AI claimed again, and the duplicate crashed the next swap in AI_TargetClaimer.SetPriority. Release targets with AI_TargetClaimer.UnclaimMyTarget, and release both controllers' claims before moving a controller, as KillMe does.

It is on the Workshop as Swap And Unaware.

What the log says

The loader writes these lines; the first three are documented in Mods/mods.log, and for the last two the skill's references do not say which log carries them.

Line Meaning
> APPLIED HARMONY PATCH: <HarmonyID> the patches applied
! ERROR: Harmony Patch <HarmonyID> Failed! patching failed; the exception is in Player.log
FAIL: Entrypoint "<type>" Not Found EntryType misspelt or missing its namespace
HARMONY FAIL: Path <path> not found. no DLL at AssemblyPath
does not implement IModEntrypoint the class exists but is not an entry class

mpn.py log lists the applied patches under mods_log.applied_harmony (Troubleshooting). Log a line from Initialize() with your HarmonyID in it, so the load also shows in Player.log.

Checking the info keys

check-mod checks the keys, not the code. An info.txt with an AssemblyPath that points at nothing, no HarmonyID, and an EntryType without a namespace:

python mpn.py check-mod harmony-mod
{
  "mod": "harmony-mod",
  "errors": [
    "AssemblyPath Managed/FasterReload.dll not found relative to the mod folder (the game logs 'HARMONY FAIL: Path ... not found.')"
  ],
  "warnings": [
    "EntryType ModEntry has no namespace: fine only if the class sits in the global namespace; otherwise write Namespace.Class",
    "no HarmonyID: the game makes up local.<random guid> each launch, so other mods cannot order against yours"
  ],
  "info": {
    "name": "Faster Reload",
    "cards": 0,
    "card_files": 0,
    "card_list": [],
    "bundles": [],
    "preview": false
  }
}

Exit code 0.

With the DLL in place, a HarmonyID and FasterReload.ModEntry, it passes. A mod with only an assembly and no cards is fine:

python mpn.py check-mod harmony-mod
{
  "mod": "harmony-mod",
  "errors": [],
  "warnings": [],
  "info": {
    "name": "Faster Reload",
    "cards": 0,
    "card_files": 0,
    "card_list": [],
    "bundles": [],
    "preview": false
  }
}

Exit code 0.

The DLL in that run was a placeholder file of a few bytes. check-mod tests only that the file exists; whether it loads, and whether its patch targets exist in your build, shows only in the game's log.

Clone this wiki locally