-
Notifications
You must be signed in to change notification settings - Fork 0
Dev Loop and Testing
🛟 Need help or found a bug? Get support at support.doodesch.de/sideload.
Any file under Mods/<appId>/ overrides the copy embedded in your DLL. Save it and the page rebuilds about
250 ms after the last write - no game restart, no rebuild of your mod.
Mods/mystash/index.html
Mods/mystash/app.css
Mods/mystash/app.js
Hard rules of the loop, each of which has cost someone an hour:
- The folder must exist when the page first builds, and this only works in a development build. If the overlay says "not watching - create Mods// to edit live", quit, create the folder, relaunch.
- A leftover override outranks a fresh build. Once that folder exists it wins over the embedded copy for every file it holds, so rebuilding your mod appears to change nothing and you debug a version of the page you stopped editing hours ago. Copy the bundle over after every build, or delete the folder when you are done.
- A reload drops the script engine, so JS state and every listener start over. That is deliberate - the script itself may be what changed.
- C# changes still need a rebuild and a game restart. Only the web files hot reload.
- An editor's UTF-8 BOM is stripped for you.
The same mechanism is what lets players reskin an app, so it is not a development-only hack.
F9 shows, per mounted page: fps, the viewport size and scale, box / rule / wired counts, the render count with the last render time in ms, the reload count, the script status with its last error, and whether the watcher is running.
The render count is the one to watch. A page that rebuilds once per change is healthy; a page whose render count climbs every frame is being told to rebuild by something that runs every frame.
Ctrl+F10 outlines every box in magenta and every text leaf in cyan.
[Sideload/layout] in the log is a full rect dump after every render - x y w h right bottom per node, plus the
compiled text verbatim for leaves. When a box is the wrong size, this says so numerically instead of you
squinting at a screenshot.
The font list is dumped once per session, which is where font-family names come from.
Turn on the DevTools preference and Sideload speaks the Chrome DevTools Protocol on 127.0.0.1. Attach the
real inspector to a page running inside the game: console, evaluate, the Elements tree with computed styles, and
Page.reload re-reading your files from disk.
| Setting | Default | What it does |
|---|---|---|
DevTools |
false |
The gate. Off means nothing listens and no page can be inspected from outside the game. Anything that can reach the port can run code in your pages, so leave it off unless you are building an app. |
DevToolsPort |
9333 |
The loopback port. Change it only on a clash. Clamped 1024-65535. |
DevToolsAutoOpen |
true |
Opens Chrome or Edge at the landing page once the first page mounts. Off writes the address to the log instead. |
DevToolsFrontend |
empty | Point it at your own copy of the DevTools frontend to override everything below. |
DevToolsFetchFrontend |
true |
Downloads the frontend once per machine (~4.5 MB) so DevTools works offline afterwards. Never while DevTools is off, never blocking the game. |
Nothing about your page leaves the machine either way: the frontend is static JavaScript talking to 127.0.0.1.
The same app.js and app.css can run in Chrome with a mock bridge, so layout and logic work costs no game
launch. The reference app
ships preview.html + s1-mock.js to copy.
The preview shell needs three rules, because the engine's defaults are not the browser's:
:where(#viewport, #viewport *) {
display: flex;
flex-direction: column;
box-sizing: border-box;
}:where() so they carry zero specificity - written plainly they would outrank every class in your stylesheet.
Chrome lays the page out with its own engine, so the preview proves your logic and roughly the look, never the exact pixels. Layout itself belongs in the headless tests below and, finally, in the game.
Your page's JavaScript can run against your real C# handlers, with no Unity and no game, in about a second. One second against a game launch is why this is the first thing to set up.
The pattern: a net8.0 console project that links your mod's logic files, embeds app.js as a resource, and
runs it on the same Jint version with a stand-in document, s1 and console.
var app = new AppHarness(); // runs app.js against the real handlers
app.Click(app.El("add"));
app.Input(app.El("entry"), "wire up the phone");
Check.Eq("1", app.El("count").TextContent);Structure your mod so this is possible: keep the MelonMod entry point free of logic and put the handlers and data in their own files. Keep those files free of any engine reference - that is what lets the suite compile them without Unity and catch an accidental dependency in a second rather than in a game launch. Anything that does need the game goes in a separate folder behind an interface.
Two things to know about such a harness:
-
Adding an
s1.callname means adding a case to the fake bridge's switch. Miss it and the call returns""with no warning: your page renders an empty state and the test asserts happily on the wrong thing. - Assert on behaviour, then break it on purpose. A test that stays green after you delete the line it covers is not testing that line.
What headless tests cannot cover: layout, text measurement, painting, anything touching TextMeshPro. A suite of green tests still shipped a text box one line too short. Verify visuals in the game.
The reference app's suite is a working example, at Workspace/Tests/WhatsDab.Tests in the development monorepo.