Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Sideload
Write a mod interface as HTML, CSS and JavaScript. Sideload renders the web bundle into real Unity UI and puts it on the in game phone, so an app is three files instead of a thousand lines of panel code.
| Date uploaded | 19 hours ago |
| Version | 1.7.0 |
| Download link | DooDesch-Sideload-1.7.0.zip |
| Downloads | 16 |
| Dependency string | DooDesch-Sideload-1.7.0 |
This mod requires the following mods to function
LavaGang-MelonLoader
The World's First Universal Mod Loader for Unity Games compatible with both Il2Cpp and Mono
Preferred version: 0.7.3README
Sideload - Web UIs for Schedule I Mods
🛟 Need help or found a bug? Get support at support.doodesch.de/sideload.
A framework, not a feature. Sideload renders mod interfaces written as
index.html+app.css+app.jsinto real Unity UI and TextMeshPro. No browser, no native code, no subprocess. Install it because a mod you want depends on it.
Source + docs · Reference app (WhatsDab) · Support
Players
Sideload adds no gameplay, no menu and no phone app of its own. Mods that build their interface with it list it as a dependency and your mod manager installs it for you. It sits idle until one of those mods registers an app, and every developer tool inside it is off until you switch it on.
Installed by hand? Sideload.dll goes in Mods/, and AngleSharp.dll + Jint.dll + Esprima.dll go in
UserLibs/ (the mod also finds them next to itself in Mods/). Without them no page will build.
Mod authors
- HTML instead of nested panels, CSS instead of
sizeDeltaarithmetic. Real flexbox, a real cascade, real selectors, TextMeshPro text. - One call for a phone app.
Apps.Register(id, bundlePrefix, title)and your bundle renders behind an icon on the in game phone. Registering before Sideload loads is fine; the call replays. - A two way bridge.
s1.call('name', arg)reaches a C# handler in the same frame and returns its string;app.Emit('name', payload)pushes back intos1.on(...). - Chrome DevTools. One preference and the real inspector attaches to a page running inside the game: console, evaluate, Elements tree, reload from disk.
- No hard dependency. The
Sideload.Apishim is one file with zero references. It is a no op when Sideload is absent and lights up when it is there, so your mod ships one DLL either way. fetchis default deny. A page reaches no host until your mod names it withAllowHost.
Start here: WhatsDab is a complete chat app for the in game phone, written as three web files, that you can install, read and copy. The step by step guide, the exact CSS subset and the layout rules that differ from a browser are in the wiki.
Requirements
Schedule I (IL2CPP) with MelonLoader 0.7.3+. No S1API.
Settings
Under Sideload_01_Main in UserData/MelonPreferences.cfg. All of it
is developer tooling and all of it is off by default.
DevTools (false) turns on the Chrome DevTools Protocol server on 127.0.0.1; anything that can reach
that port can run code in your pages, so leave it off unless you are building an app. DevToolsPort
(9333) is the loopback port. DevToolsAutoOpen (true) opens the browser at the landing page once the
first page mounts. DevToolsFetchFrontend (true) lets Sideload download the DevTools interface once into
UserData/, so the inspector works offline afterwards. DevToolsFrontend (empty) overrides that with a
folder holding your own copy.
License
MIT. See the included LICENSE.md. Bundles AngleSharp (MIT) and Jint with Esprima (BSD-2).
CHANGELOG
Changelog
All notable changes to Sideload are documented here. This project adheres to Semantic Versioning.
[1.8.2] - 2026-08-05
Fixed
- An app you reopen has its caret back in the text field. Closing one releases the keyboard, and nothing handed it back unless the app happened to ask - so a terminal or search box came up looking ready, ate nothing, and every key you typed went to the game and started opening things instead.
[1.8.1] - 2026-08-05
Fixed
- Sideload reports its own version correctly again. Since 1.6.0 it announced itself as "1.5.0" to MelonLoader, so the console, mod managers and update checks all showed you an ancient build and kept telling you to update one you already had.
[1.8.0] - 2026-08-05
Changed
- Opening an app for the first time takes about a fifth of the time it did. Measured on the same app in the same session: 674 ms before, 159 ms after. Almost none of that was ever the app - it was the HTML parser, the script engine and TextMeshPro all being used for the first time, and whoever opened the first app paid for all of it. Sideload now does that work on a throwaway page while the scene is still loading.
- A page fades in over a fifth of a second the first time its app is opened, instead of appearing at the end of the build all at once. Later opens are unchanged - there is nothing to cover once the page exists.
[1.7.0] - 2026-08-04
Added
font-family: monospacedraws in a real monospaced face. The game ships none - Open Sans, a pixel face and three decorative ones - so a page that wanted a terminal or a table had nothing honest to render in. Sideload now builds the font from the machine's own file, trying Consolas, Cascadia Mono, Lucida Console, Courier New and DejaVu Sans Mono in that order, and says in the log which one it took. Nothing is shipped with the mod; it is the player's own font. Where none of them exists, the pixel face still steps in.
[1.6.0] - 2026-08-04
Added
app.Icon(true)andapp.Icon(false)put an app's home-screen square there or take it away while the game runs. For an app that is opened by a key and only makes sense sometimes: hash shows its square exactly while the game's console is switched on.- A text field can show what accepting a suggestion would type. Put the remainder in
data-ghostand it is drawn behind the caret in the field's own font;-s1-ghost-colorstyles it. Nothing to measure on your side, and it does not need a monospaced font. caret-colorand-s1-caret-widthlet a field draw a block cursor instead of the thin line the game uses.keydowncarrieshasSelection, so a page can tell Ctrl+C-as-copy from Ctrl+C-as-interrupt.data-reject-firststops a dead key from opening a fresh line.
Fixed
- Closing an app while its text field still had focus left the game convinced you were typing: no movement, no Escape, no phone key, and nothing on screen to click. The keyboard is let go when an app hides and when the phone goes down.
- Ctrl+Backspace deletes a word. It deleted one character, because key suppression ignored the modifier keys. Shift+Up still selects.
- A row built from coloured spans stays one row.
white-space: prenow also tells the layout "this is text", so a block holding nothing but spans becomes a single text object. Each span used to turn into its own full-width box, which stacked them down the page and cost a rebuild apiece. - Focus asked for before the first render is applied after it instead of being dropped.
[1.5.0] - 2026-08-04
Added
- An app can ask for keys. Name them on a text field with
data-keys="Tab ArrowUp Ctrl+R"and they arrive askeydown, withctrlKey,shiftKey,altKeyandrepeat. Only the keys an app names are taken from the field, so a page that names none behaves exactly as before. Letters, digits, the editing keys and F1 to F12; Enter and Escape are refused because they already arrive askeydownandback. - Holding a key repeats it: 0.35 seconds before the first repeat, then one every 0.06, dropping to 0.03 after
1.2 seconds so a long list stays reachable.
e.repeattells a page which is which. white-space: preandpre-wrapkeep the spaces a page was written with. Until now every run of whitespace collapsed to one, which made a column padded to twelve characters arrive as a single space.-s1-mono-advance: 7pxgives every glyph the same width, so a column of values lines up under its heading. None of the game's fonts are monospaced, and this is the only way to get an aligned table out of them.app.Show()takes the phone out of the player's pocket and opens the app;app.Hide()reverses both, andPhoneScreen.Raise()and.Lower()move the phone on its own. Refused while the game is paused or the player is asleep, dead or arrested. Opening an app still does not raise the phone by itself, so a background update cannot yank it up.
[1.4.2] - 2026-08-02
Fixed
- A slow mod no longer fails the page it is answering. The time budget a page handler runs under was counting
the mod's own work in
s1.call, so an app whose script did almost nothing still died with "The operation has timed out" when the mod took a moment. The clock restarts once the mod answers; a script that will not stop is still caught, by the statement limit.
[1.4.1] - 2026-08-02
Fixed
- The seconds an app passes to
app.Notifyare used. In 1.4.0 the API took them and the notification still ran at the default length: the shim never looked the new host method up, so every call fell through to the old one. Nothing else changes, and an app that leaves the seconds off behaves as it always did.
[1.4.0] - 2026-08-02
Added
- An app decides how long its own notification stays up:
app.Notify(title, subtitle, seconds). Leave the seconds off and Sideload picks the timing, as before. Between 2 and 30 - the slide-in has no dismiss button, so an app does not get to hold a corner of the screen.
[1.3.1] - 2026-08-02
Fixed
- App notifications no longer cut their text off mid-word. The box sizes itself to what the app sent, so a headline plus a sentence fits, and a short line gets a small box instead of a mostly empty one.
Changed
- An app notification stays up for 9 seconds instead of 5. It cannot be clicked and it does not come back, so it has to still be there when you look up.
[1.3.0] - 2026-08-01
Fixed
- Sideload works on Schedule I 0.4.6f11. That update reworked the phone's input handling and the key-hint strip at the bottom of the screen.
- Turning the phone uses your rotate keys again, whatever you have them bound to. Those keys moved into the game's new input system, so Sideload now reads the same binding the build ghost reads: rebind once, in the game's own options, and both pick it up.
- The "Rotate Phone" hint is back next to Tab and Escape. The game rebuilt that strip, and the line survives the game swapping the whole panel out mid-session.
- Right-click steps back inside an app again. The game collapsed its exit types, and right-click is the secondary one now.
- An open app costs 4.4 ms a frame. Finding the game's hint strip on 0.4.6f11 means searching the scene, and searching it every frame measured 81 ms, 62% of the whole frame. The strip is remembered while it stays on screen and looked up again four times a second at most.
[1.2.0] - 2026-07-31
Fixed
-
A scrolled list still swallowed clicks meant for whatever sat above it. Scroll a list, then press a button in a bar fixed above it, and a list row took the press instead - the row was off screen, but only visually.
Clipping in this engine is a rendering instruction and nothing else: uGUI decides what a click hit by raycasting Graphics, and the only thing that filters that is a component implementing
ICanvasRaycastFilter, which is whatRectMask2Dis. This engine cannot useRectMask2D(it collapses on a rotated panel) and cannot implement the interface either (a Unity interface on a managed type is the unreliable virtual-override path a customGraphicwas already ruled out for). So every hit target stayed live at wherever its rect had scrolled to.Hit targets are now shrunk to the visible part of their element whenever the list moves, and switched off entirely when none of it is in view. No interface needed: the hit target is a child stretched over its element, so insetting it is enough, and the work happens in the content's own local space, which is rotation-free by construction.
Until a page put something FIXED above a scrolling list, this only ever let a click land on a row that happened to be off screen - unwanted, but invisible.
-
A repaint escaped its scroll area. Hovering a list row that had scrolled out of its viewport drew the row's background across whatever sat above the list - a sticky bar, a header - hiding it completely, and with no text, which made it look like the bar had gone blank rather than been covered.
Clipping lives on the CanvasRenderer and is taken from a static that only holds a value while the paint walk is inside a scroll area. A repaint happens long after that walk - a hover, a transition frame - with the static back at null, so the box was redrawn with clipping switched off and reappeared wherever its rect happened to be. Each painted box now remembers the clip it was built under, and both repaint paths restore it.
It needed a page with something FIXED above a scrolling list to be visible at all; until one existed the escaped box drew over other list rows, where it was indistinguishable from a hover.
-
A border on a box with no background drew nothing at all. An outlined chip -
border: 1px solid, no fill - was invisible, while the same border on a filled box was fine. A uniform border is drawn as the shader's rounded ring inside the fill's own quad, so with a transparent fill there was nothing for it to modulate. A uniform border over a transparent fill with square corners is now drawn as four strips instead, which is what a single-sided border already did. Rounded boxes keep the ring.It shipped invisibly because every border anyone had drawn until now was either single-sided (a list row's hairline) or sat on a fill (an input, a button). An app whose design language is outlined chips had no outlines anywhere and nothing in any log said so.
-
border: 1px dashed <colour>lost its colour. The shorthand parser recognised onlysolid,noneandhidden, so every other line style fell through to the colour parser and consumed the declaration's actual colour. All ten CSS line styles are now recognised; the engine still draws them solid, because it has no dash pattern.
[1.1.0] - 2026-07-28
Added
-
Paint-only styles no longer rebuild the page. Writing
transform, a background, a border colour, a corner radius or a box shadow from script now repaints just that box - the same path:hoverhas always taken. Everything else still rebuilds, and so doescssText, which can contain anything. This is the difference between an animation being possible and not: a rebuild destroys and recreates every GameObject on the page, measured at roughly half a millisecond per box, so a 200-box page cost ~100ms per frame to move something one pixel.colorandopacityare deliberately NOT on the fast path even though they only affect appearance: both are inherited, and a repaint redraws one box, so descendants would keep the old value. They rebuild, where they are correct. -
e.offsetX/e.offsetY/e.normX/e.normYon a click. Where inside the element the pointer landed, in the element's own CSS pixels and as a 0..1 fraction of its size. A page that stands for something with its own coordinate space - a map - can finally answer "where did they point at". -
Apps without a home-screen icon, via
.NoIcon(), for an app whose way in already exists somewhere else. With no icon,.Open()is the only route in. -
.Open(),.Close()and.IsOpenon an app handle: open an app from code exactly as pressing its icon would, closing whatever else is open and turning the phone.AppHandle.CanOpenProgrammaticallyreports whether the installed host understands any of this, so a mod that supplies its own entry point can refuse to set up against an older Sideload rather than leaving the player an app they cannot reach. -
A companion seam on the bridge: the app list, bundle files, framework assets and runtime images can be read, a handler can be invoked without a page, and host events, badges and notifications can be tapped. For serving the same bundle to a second screen. It grants no capability a page does not already have.
Fixed
overflow: hiddennow clips. It was parsed and then ignored: onlyautoandscrollever produced a clip rectangle, and only once the content was tall enough to need scrolling. A box meant as a window onto something bigger - a map, a graph, anything panned by a transform - let its contents draw across the rest of the screen and off the phone entirely.- A clip now follows the transforms above it. The rectangle was derived from the layout, which by design
knows nothing about a
transformon an ancestor - so inside a panned or zoomed window the clip sat where the boxes would have been rather than where they are, and everything in it vanished.
All of it is additive - the bridge ABI stays at 1, and an older shim binds exactly as before.
[1.0.1] - 2026-07-27
Fixed
- A mod asking
IsOnScreengot "yes" while the phone was closed. Vanilla'sSetIsOpen(false)leaves the app panel active and still registered as the phone'sActiveApp, so the check that is supposed to mean "the player is looking at this" stayed true with the phone in their pocket - and an app that politely asks before raising a notification stayed silent in exactly the case the notification exists for.IsOnScreennow also requires the phone itself to be open.
[1.0.0] - 2026-07-27
First release. Sideload turns a folder of index.html / app.css / app.js into real Unity UI, so a mod
can write its interface the way the web writes interfaces instead of assembling panels by hand. The in game
phone is the first host; the core mounts into any RectTransform.
Added
- HTML rendering through AngleSharp: the parser, the DOM and the selector engine are the real thing, so
querySelectorAllsemantics are correct rather than approximated. - A CSS cascade with custom properties,
var(),!important, inline styles and specificity ordering. Supported: flexbox and absolute positioning, the box model, backgrounds and two stop linear gradients, per side borders and per corner radii, outer box shadows, opacity, the text properties,transform,transitionand@media (orientation: ...). Units arepxand%. - A flexbox implementation with the parts that usually get skipped: the automatic content based minimum from Flexbox 4.5, the iterative grow/shrink resolution from 9.7, and flex basis measured at the stretched size. Border box sizing everywhere, and width never depends on height, which is what keeps a layout pass finite.
- Painting through one UI shader and one shared material: radius, border, shadow and gradient travel in
vertex channels. Meshes are written straight into a
CanvasRendererand converted to linear colour on a linear rendering build. Nodes come from a pool. - Text as TextMeshPro. An element with direct text compiles to a single text leaf, with
b i strong em spanand friends turned into TMP rich text inside it, so a sentence with inline markup is one draw and keeps its spaces. <input>and<textarea>become realTMP_InputFieldcontrols, and focus sets the game'sIsTypingso typing in an app does not drive the player.- JavaScript on Jint 3.1.5 with
ExperimentalFeature.All: ES2015 through ES2024, a DOM wrapper API,console, timers driven by the mod's update loop rather than by threads, and a 250 ms budget per handler so a runaway loop is one hitched frame instead of a hung game. - The
s1bridge.s1.call(name, arg)runs a C# handler on the main thread in the same frame and returns its string;app.Emit(name, payload)reachess1.on(...). Handlers are keyed per app, so two mods may both own a call namedlist. s1.storage: string key/value per app inUserData/Sideload/<appId>.json. Deliberately not the game save, so UI state never travels with a save or diverges between co-op peers.fetchwith a default deny allowlist that belongs to the mod, not to the page. Exact host or one*.wildcard, https only outside loopback, every redirect hop rechecked, 10 s timeout, 4 MB response cap, 8 requests in flight. A blocked request rejects with the exactAllowHostcall that is missing.Sideload.Api, the modder shim: one file, zero references, found by reflection. Every call is a no op without the host and is replayed once it appears, so registration is load order proof and a mod that uses it needs no hard dependency.- Bundle resolution with a file override: an embedded resource in the mod's own DLL is the shipped default,
and a file of the same name under
Mods/<appId>/wins over it. One mechanism for authoring and for players reskinning an app. - A Chrome DevTools Protocol server on
127.0.0.1, off by default behind theDevToolspreference. Attach the real DevTools to a page inside the game for console, evaluate, the Elements tree with computed styles, andPage.reloadre-reading the bundle from disk. Optionally served from a local frontend copy so it works offline. - Own Harmony postfix on
HomeScreen.Startfor the phone integration: one panel and one icon per registered app, no S1API dependency. The panel is built rather than cloned from a vanilla app, and the icon comes from the game's own icon prefab, so neither carries a vanilla app's components onto the phone. icon.pngin an app's bundle becomes its home-screen icon. An app without one gets a flat coloured square derived from its id.- Both phone orientations. An app names the ones it supports in preference order -
.Orientation("landscape", "portrait")- and the first is what it opens in. Naming two is what lets the player turn the phone with the game's own rotate keys (Q and E out of the box, gamepad triggers too, rebindable in the game's own controls). The viewport is re-measured,@media (orientation: ...)re-evaluated and the page laid out again with its document and script intact. Landscape is 733 x 400 CSS pixels, portrait 400 x 733. Naming one locks the app, which is also what saying nothing gets you. - A "Rotate Phone" line in the game's own key-hint strip while a turnable app is open, built from the game's prompt component so it shows the key that is really bound. Nothing is drawn inside the app: that screen belongs to whoever wrote it.
- The player's choice is remembered per app in
UserData/Sideload/orientation.json, and dropped silently if a later version of that app no longer supports it. s1.setOrientation(...)still turns the phone from the page, now refused with a log line when the app never declared that orientation.s1.orientationreads it back.- A cancellable
backevent. Right-click and Escape - which the game raises through one chain - reach the page first:document.addEventListener('back', e => e.preventDefault())keeps the app open so it can step back inside itself, exactly as right-click leaves a conversation in the vanilla Messages app. A page that does not listen closes, as before.e.sourceis"rightClick"or"escape". - An
orientationchangeevent, raised after the page has been laid out at its new shape, withe.valueset to"portrait"or"landscape". A layout that changes SHAPE also has state to decide - which of two panes to land on after a turn - and a stylesheet cannot decide that. - Clipping that survives a rotated panel. Scroll areas and form controls used Unity's
RectMask2D, which builds its clip rectangle from world corners in fixed order and so inverts under a rotated ancestor - culling every masked child. Text now clips through the same sorted-corner rectangle the box meshes use. <img src="...">paints a file from the app's bundle. Sized by CSS alone, because the layout runs without Unity and cannot open a PNG to learn an intrinsic size; the aspect ratio is preserved inside the box you give it, andcolortints it, so one white glyph serves a dark bar and a light one. Sprites are cached per app and dropped on reload.app.Badge(count)puts an unread count on the app's home-screen icon - the same badge the vanilla apps use - and it survives the phone being rebuilt.app.Notify(title, subtitle)raises one of the game's own phone notifications carrying the app's icon, andapp.IsOnScreenanswers the question that has to come first: whether the player is already looking.s1.css, the game's own design tokens as CSS variables, embedded in Sideload so<link rel="stylesheet" href="s1.css">resolves for every app of every mod.- Fail soft error handling: broken HTML, CSS or JavaScript produces a visible error page plus a log entry
with
file:line, and a throwing handler kills only that handler. A page is never laid out while its panel is hidden either: text is measured by a TextMeshPro probe, TMP initialises inAwake, andAwakenever runs on an inactive object - so a page rebuilt off screen used to come back at one character per line.