Schedule I
Platform ID com.tvgs.scheduleone
DigitalHeaven.Mods.ScheduleOne Unity Mono is a BepInEx 5 mod that brings DigitalHeaven avatar replacement to Schedule I.
Requirements
Section titled “Requirements”| Mod loader | BepInEx 5 |
| Game version | Mono (alternate branch on Steam) |
| Rendering pipeline | URP |
| Humanoid rig | Full Unity Avatar rig (17 required bones) |
This mod depends on the DigitalHeaven.Unity library for model loading, rig setup, and material creation.
Core Features
Section titled “Core Features”| Feature | Status |
|---|---|
| Overlay | Yes |
| DigitalHeaven map visits | Yes |
| Voxel volumes in maps | Untested |
| DigitalHeaven noclip | Untested |
| Avatar replacement | Yes |
| Spring bones | Yes |
| Spring colliders | Untested |
| Eye look and blink | Native targets |
The avatar follows the game’s character through Unity’s humanoid muscles, with the shared HumanoidPoseMirror. The game’s Mixamo rig maps no jaw, so the avatar’s Jaw is kept out of the transfer and stays closed. The [PoseMirror] log line on the first mirrored frame says so, with how far the transfer had turned the jaw.
Multiplayer Features
Section titled “Multiplayer Features”| Feature | Status |
|---|---|
| Avatar sync | Yes |
| Pallet transfers | Yes |
| NPC replacement | Yes |
| Lobby expansion | Yes |
Multiplayer avatar sync includes automatic pallet transfers. When you see another player’s avatar, any missing pallets (including dependencies) come from that player over the game’s network, through the shared transfer stack described in sharing pallets with other players.
DigitalHeaven Map Visits
Section titled “DigitalHeaven Map Visits”The Schedule I host includes the map visit lifecycle and a Maps overlay window. The shared Unity content bridge reads the selected compiled map and builds it into an isolated additive Unity scene.
The scene keeps the map’s source hierarchy, the way Unity itself holds a scene: one GameObject per node of the map’s geometry, with its own local transform, and every mesh node with its own mesh, renderer and MeshCollider. A large map therefore becomes thousands of ordinary colliders rather than one multi-million-triangle mesh. Map objects are marked static, and each material is created once and shared by every node that uses it, as are the textures it samples. A material’s uvScale tiles every texture it binds, so an ambientCG surface authored at [0.33, 0.33] repeats as authored instead of stretching one tile over the whole mesh. The map’s lightmap, GI volume and reflection cubemaps are not read from the pallet at all, since the Unity bridge applies none of them.
Collision follows what the map says:
| Map content | In Schedule I |
|---|---|
autoCollision: "mesh" (the default) | Each mesh node collides with its own triangles. Skybox material slots never collide. |
A part removed or disabled, or its dh.collider state set to off or trigger, by a part override | No collider. When the map ships a compiled collision cook, a node the cook dropped gets none either. |
autoCollision: "boxes" or "hulls" | The compiler’s baked boxes and hulls, with no mesh colliders. |
A dh.collider on a part, a brush or a plain object | Its box, sphere, capsule and hull shapes, as authored, in the owner’s frame. |
boxBrush entities | One GameObject per brush, meshed exactly as the engine meshes it and drawn with the map’s materials, with a BoxCollider. A brush whose dh.collider state is off or trigger gets no collider, and a disabled brush is not built. |
Object instance records (props) | Drawn from the object each one names, each object uploaded once and shared by all its instances. Only an object with a dh.physicsProp body collides, with a static box; imported Source props carry no body and have no collider. See Unity → Maps. |
Authored part overrides are applied through the same resolvers the engine uses (DigitalHeaven.Content.Patches), and on the engine’s terms: each override is about one part, never its subtree.
- A part an override removes (
"removed": true) or switches off ("enabled": false) is not built. No mesh is uploaded and no material, texture or collider is created for it. If something under it is still kept, it stays as an empty GameObject that its children hang from, and those children still draw. - An override’s
position,rotationandscalemove that part alone, about its compiled pivot. Its children keep their own world poses. - An override’s
namebecomes the GameObject’s name. - If the map’s part table no longer matches its model (a stale build), no override is applied, every part draws where it was built, and the log says so. An override that no longer resolves is skipped with a warning, and the log reports a count of each kind of override applied.
- A removed part is never decoded: the map’s model is read with that part left out, so none of its vertex data is read from the pallet. A part switched off is still decoded and then dropped, since a switch is a state rather than a removal.
What is not recreated yet, and is reported in LogOutput.log when a map uses it:
- A trigger is not a trigger: it simply has no collider. This holds for brushes too, so a trigger brush’s outputs never fire.
- Water brushes are ordinary solid brushes; there is no swimming.
- Water is drawn on a clone of Schedule I’s own ocean material (Stylized Water 2), tuned toward the DH water block’s color, clarity and foam. Its waves are flattened, and the DH block’s waves, flow and optics are not carried over. See Unity → Water.
- Lighting, lightmaps and entities other than spawn points, box brushes, object instances and voxel volumes. An object instance parented to another entity is not built.
- A node transform with a shear, which Unity cannot hold, takes the closest pose.
StaticBatching hands the finished map to Unity’s runtime static batching. It is off by default: it trades memory for fewer draw calls and has not been measured on a large map.
The window shows, from top to bottom:
- Current map: the map you are standing on and its placement, or Not visiting. It is separate from the status note underneath, which reports the last thing that happened.
- The map dropdown. Maps are named by their file name alone, without pallet, folders or extension (
debug-map, notcore:maps/debug-map.dh-map). Two maps with the same name keep their pallet in parentheses, or their folders when they share a pallet too. - Placement and Load Map, while you are in Schedule I. The placement is chosen before loading and is remembered in the config file.
- Switch to map, while visiting, whenever the dropdown names a different map. It loads the new map, moves you to its spawn and only then unloads the old one, so you never stand on nothing. It keeps your original pre-visit position for the final return, and saving stays blocked across the switch. If the new map fails to load, you stay on the old one.
- Return to Schedule I, while visiting.
The tab does not blink for a visit or for noclip; those are ongoing states, and the Current map line and the noclip button already show them.
The dropdown lists every map in the compiled pallet directories: your workspace Pallets, any additionalPalletDirectories, and, when enginePath is set in dh-config.jsonc, the engine’s built-in maps (debug-map, bake-eval, …) from the content folder next to it. No copies in the workspace are needed. A map’s core: references resolve against the core pallet found there, and its declared pallet dependencies resolve against the same directories.
Voxel volumes
Section titled “Voxel volumes”A map’s voxelVolume nodes (a Minecraft world, a structure, a .vox model) stream in around you while you visit, through the same streamer the engine runs (DigitalHeaven.Content.Voxels). Nothing is meshed whole: regions near the camera are meshed on worker threads and land a few per frame, and regions you leave behind are dropped.
| What | In Schedule I |
|---|---|
| Drawing | One GameObject per region of chunks (2 × 2 × 2 by default) under the map root, with one mesh and four materials: opaque, cutout, translucent, and a magenta checker for blocks nothing resolved. Every material of a volume samples one atlas texture with its own mip chain, point filtered, so texels stay crisp up close, and a block that gives off light (lava, fire, a torch) glows by the atlas’s glow levels, a second texture at the same UVs. Animated block textures (water, lava, fire) play in both, and only on the GPU: every way a tile looks over its cycle is composed once into a frame sheet, and a tile that changes is copied from there over its cells at every mip level, at most once per game tick. A tile with too many looks for the sheet (a slow blend) is composed into a one-cell staging texture and copied from that. Nothing uploads a whole atlas unless the device cannot copy between textures. |
| Collision | One MeshCollider per chunk within 32 m of your feet, on the map’s layer and ColliderShape (Convex makes them convex too). A chunk within 4 m is built on the frame that needs it, so you never stand on one still in the queue. |
| Block textures and shapes | From the Minecraft platform pallet (com.mojang.minecraft) in your compiled pallet directories. Without it, LogOutput.log says so once, blocks draw as flat colors and collide as whole cubes. |
| The physics world | A volume’s colliders only stand near you, so its whole extent is checked once, when it opens. A volume reaching past the physics world grows the physics box to cover it; the box is rebuilt only when that makes it bigger. |
Streaming follows the engine’s defaults, and [Voxels] in the config tunes the three that matter most on a game’s budget: Radius, MeshBudgetMb and Workers. They apply on the next frame. The drawn look (translucent sorting, the surface shader the game’s pipeline picks) has not been checked in a game yet.
Placement
Section titled “Placement”| Placement | Schedule I’s world | Where the map loads |
|---|---|---|
| Overlay | Stays loaded and fully active | With its origin at OverlayOffsetX/Z and its spawn standing at OverlayOffsetY, by default 46 m up, just above the town |
| Replace (default) | Hidden and not solid; NPCs and traffic paused; the game’s systems keep running | At the map’s own coordinates |
Overlay touches nothing in Schedule I. Walking off the edge of the map drops you back onto the town. The height goes by the spawn rather than the map’s origin because a map’s floor can sit far from its origin: gm_bigcity_improved spawns 282 m below its own, and an origin at 46 m put the whole city under the ocean, whose trigger warped the player back to the town.
The physics world
Section titled “The physics world”Schedule I’s physics runs in a fixed box: its broadphase is multibox pruning over a world of ±250 m in X and Z and ±50 m in Y around the origin. A collider or rigidbody wholly outside that box gets no contacts. Queries still see it, so you can walk on it, but a skateboard, a vehicle or a dropped item passes straight through. An earlier default put Overlay maps 1000 m up, where every rigidbody fell through every wall.
For a map visit DigitalHeaven resizes that broadphase (Physics.RebuildBroadphaseRegions, 16 subdivisions) to the union of the stock box, every map collider and every voxel volume’s whole extent, padded by 16 m, and logs the new box. Return, a map switch, a scene change and quitting put the stock box back. Only if the game refuses the resize does the log and a toast name what lies outside, and a skateboard passes through those parts again.
The town fills almost the whole box horizontally, with colliders reaching about 40 m. So the default Overlay offset is the free level left above it: the map’s spawn at 46 m. Only the map’s lowest few meters are then inside the box. Anything above 50 m, or below the town’s rooftops, is not reliably solid to rigidbodies. That is why Replace is the default: it loads the map at its own coordinates in the middle of the box, so everything of the map that lies within ±250 m and ±50 m is solid.
After every load, the mod checks the map’s colliders against the box. If any lie outside or cross its edge, a warning appears in LogOutput.log, as a toast, and in the Maps window’s status line. It names the counts and the map’s extent. The map is not moved. Replace placement is checked the same way, so a map larger than the box is reported too.
Replace hides all of Schedule I’s world without unloading or destroying anything, so Return can put back exactly what was there. Only the player and what it needs stay: its body and avatar, the camera and what it holds, the skateboard or vehicle in use, the UI and the sun. Each category is handled the way the game tolerates:
| Category | What Replace does | Return |
|---|---|---|
The town (ReplaceRoots, by default Map/Hyland Point) | A part that holds nothing live is deactivated whole. Where something live sits (a networked object, a navmesh, a game manager, a saveable or GUID-registered object) it stays active and only loses its renderers, colliders and lights | Reactivated |
| NPCs | Hidden through the game’s own SetVisible, the call it uses when an NPC goes indoors: the body goes, the navmesh agent stops, so the NPC stays where it stood. Its sight and hearing are switched off | Shown again where the game wants them |
| Vehicles, parked and driving | Hidden through the game’s own SetVisible, which also turns their physics off, so traffic stops and nothing falls | Shown and simulated again |
| Terrain | The heightmap, trees and grass stop drawing, and the terrain collider is disabled | Drawn and solid again |
| The ocean and ponds | Renderers hidden, and the ocean trigger that warps a player back to land is disabled, so a map’s underground below sea level can be reached | Back, with the trigger |
| Everything else in the scene | Islands, backdrops, distant LODs, props, particles: renderers hidden and colliders disabled. Point, spot and area lights and reflection probes are switched off, so the town’s lamps do not light the map. Loose rigidbodies are made kinematic | Back as they were |
| Ambience | The town’s audio zones and positional one-shots are muted, along with the sound of hidden NPCs and vehicles. Music and the game’s shared sound pools stay, since the player’s own sounds play through them | Unmuted |
Every change is recorded, and Return undoes exactly those, ground first: something that was already off before the visit stays off. An NPC or vehicle the game shows during the visit (an NPC leaving a building, a goon or a delivery arriving, a car leaving a parking lot) stays hidden, and Return shows it if the game still wants it shown. NPCs and vehicles that spawn during the visit are caught by a rescan every 2 seconds. LogOutput.log gets one line per category with what was deactivated, hidden, disabled, frozen, paused and muted, the time the hide took, and on Return a line for anything the rescan caught. If none of the roots is found, Replace refuses to load and says so.
What Replace does not do, so expect it:
- The game’s clock and schedules keep running. A hidden NPC stands still, but its schedule moves on, so after Return some NPCs walk off to where they should be by now.
- Things other than NPCs and vehicles that appear during the visit, such as an item the game drops, are not hidden.
- Schedule I’s own fall recovery still applies: a player below a height of -20 m is put back on the stock navmesh, so a map whose floor sits that low cannot be stood on in Replace.
- The audio zones and one-shots keep their sources in private fields that are read by name; if a game update renames them, that ambience stays audible.
- Visits are available only after the
MainorTutorialworld has finished loading, and not from a vehicle or skateboard. - They are disabled when another player is present. A visit that becomes multiplayer ends and returns you.
- The player’s original position and rotation are restored on return.
- Every
SaveManager.Saveoverload is blocked from the beginning of a visit until you are back at your original position, so neither a foreign map pose nor a hidden world can enter the save. - If Schedule I starts a normal host scene transition, the map is torn down without restoring a pose into the new scene.
DigitalHeaven Noclip
Section titled “DigitalHeaven Noclip”The Maps window carries a Noclip toggle that flies the player’s body through walls with the DigitalHeaven engine’s own noclip feel: the stick and view pitch steer, Jump climbs, Crouch descends, Sprint boosts. The flight math is the engine’s, shared through Core, so the two feel the same.
- It is single-player only, like map visits. With another player in the lobby the toggle is replaced by the reason, and a player joining mid-flight ends it.
- It is unavailable in a vehicle, on a skateboard, or while the game itself is moving the player.
- Turning it off hands the body back to Schedule I where it stands, as controllable as before; the player then falls from rest.
- A scene change or a lost local player ends flight without touching the old player object.
- There is no keybinding yet.
In-Game Configuration
Section titled “In-Game Configuration”Config file: BepInEx/config/io.mltn.digitalheaven.scheduleone.cfg
Set your avatar using a barcode — the full pallet.id:path/to/asset identifier from your compiled pallet.
| Setting | Section | Default | Description |
|---|---|---|---|
PlayerAvatar | [DigitalHeaven] | "" | Barcode for the player avatar |
NPCAvatar | [DigitalHeaven] | "" | Barcode for NPC avatars (empty = disabled) |
MaxPlayers | [Lobby] | 4 | Maximum lobby size when hosting (4–12). Clients adapt automatically. |
EyeLook | [DigitalHeaven] | true | Let avatars look around and blink on their own. The game’s own look targets are followed either way. |
Speed | [Noclip] | 12 | Top noclip fly speed in m/s |
Accelerate | [Noclip] | 5 | Noclip acceleration multiplier |
Friction | [Noclip] | 4 | How quickly flight bleeds to rest; keep it no higher than Accelerate to reach full speed |
StopSpeed | [Noclip] | 2.54 | Friction control floor in m/s |
SprintMultiplier | [Noclip] | 2.5 | Speed multiplier while sprint is held |
ToggleKey | [Noclip] | C | Key that toggles noclip outside the overlay (Input System key name); None disables it. Ignored while typing |
Placement | [Maps] | Replace | Overlay or Replace; the Maps window’s Placement dropdown writes it |
OverlayOffsetX | [Maps] | 0 | Overlay placement: map origin X in meters |
OverlayOffsetY | [Maps] | 46 | Overlay placement: the height in meters the map’s spawn stands at. Physics stops at 50 m, see the physics world |
OverlayOffsetZ | [Maps] | 0 | Overlay placement: map origin Z in meters |
ReplaceRoots | [Maps] | Map/Hyland Point | Replace placement: semicolon-separated scene paths whose geometry is hidden |
StaticBatching | [Maps] | false | Combine a loaded map’s meshes with Unity’s runtime static batching; applies to the next visit |
ColliderShape | [Maps] | Shared | Diagnostic; applies to the next visit. Shared: each solid node gets a concave collider on its render mesh. UnitScale: the concave collider sits on a child with the node’s world scale baked into its own mesh, so it is cooked at real size. Convex: a convex hull per node, which fills doorways and courtyards |
DiagnosticBoxInMapKey | [Maps] | F7 | Diagnostic: places a plain static 3 × 3 × 0.5 m cube wall 4 m ahead of you, inside the loaded map’s scene. Ignored while typing; None disables it |
DiagnosticBoxInGameKey | [Maps] | F8 | Diagnostic: the same wall, left in Schedule I’s own scene. Both walls are removed when the map unloads or the scene changes |
Radius | [Voxels] | 128 | Meters from the camera a map’s voxel volumes stream in (8–4096) |
MeshBudgetMb | [Voxels] | 512 | Megabytes of voxel meshes every volume together may hold; past it the farthest regions are dropped (16–65536) |
Workers | [Voxels] | 2 | Threads that mesh voxel regions (1–16), read when a map’s first volume streams in |
UnattendedCollisionRun | [Diagnostics] | false | Diagnostic: on the next launch, continue the latest save, visit UnattendedCollisionMap in the configured Placement, fire board-like rigidbodies (and a stripped copy of the game’s skateboard) at a map-scene wall, a game-scene wall and the nearest concave map mesh, log one [CollisionRun] line per pairing, and quit. It switches itself back off when it starts |
UnattendedCollisionMap | [Diagnostics] | io.mltn.maps.mltn-city:mltn-city.dh-map | Diagnostic: the barcode of the map the unattended run visits |
UnattendedTour | [Diagnostics] | false | Diagnostic: on the next launch, continue the latest save, visit each of UnattendedTourMaps, photograph it from behind the player and the skateboard dropped beside it, then from a ring of free-camera viewpoints around them (kept inside the physics box the visit resized), into BepInEx/DigitalHeaven/tour/<yyyyMMdd-HHmmss>/<map>-<n>.png, log [Tour] frame times and per-frame cost at each stop and over a ten-second fly-through, and quit. It switches itself back off when it starts; launch the game exe directly, not through steam:// |
UnattendedTourMaps | [Diagnostics] | voxel-playground,world-older | Diagnostic: comma-separated maps the tour visits, each a barcode, a display name or a barcode’s asset name (any case). A name that matches nothing logs the maps that are installed and is skipped |
UnattendedTourPlacement | [Diagnostics] | empty | Diagnostic: Overlay or Replace for the tour’s visits; empty uses Placement |
Example Configuration
Section titled “Example Configuration”[DigitalHeaven]
## Full barcode for the player avatar (e.g. io.example.avatar:avatar.dh-avatar)PlayerAvatar = io.example.my-avatar:avatar.dh-avatar
## Full barcode for NPC avatars. Leave empty to disable NPC replacement.NPCAvatar =
[Lobby]
## Maximum lobby size when hosting. Set to 4 for vanilla behavior.## Clients joining your lobby adapt automatically.# MaxPlayers = 4Development Setup
Section titled “Development Setup”The mod project lives at Platforms/DigitalHeaven.Mods.ScheduleOne/ and is included in the main DigitalHeaven.sln solution under the Platforms folder.
Local Configuration
Section titled “Local Configuration”Copy DigitalHeaven.Mods.ScheduleOne.csproj.user.example and remove the .example extension. This file configures your local game install path and is git-ignored:
<Project> <PropertyGroup> <!-- Set this to your Schedule I game install path --> <ScheduleOneDir>C:\Program Files (x86)\Steam\steamapps\common\Schedule I</ScheduleOneDir> </PropertyGroup></Project>By default, ScheduleOneDir is used to locate both BepInEx assemblies and the game’s managed assemblies. If your setup has these in different locations (e.g. when using an r2modman profile), you can override them individually:
| Property | Default | Description |
|---|---|---|
ScheduleOneDir | (see .csproj) | Base game path. Controls everything if the other properties are not set. |
ScheduleOneBepInExDir | $(ScheduleOneDir)\BepInEx | Root BepInEx directory containing core\ and plugins\ folders. |
ScheduleOneManagedDir | $(ScheduleOneDir)\Schedule I_Data\Managed | Directory containing the game’s managed assemblies (Unity engine, Assembly-CSharp, etc.). |
For example, with an r2modman profile where BepInEx and plugins live in the profile but managed DLLs are in the Steam install:
<Project> <PropertyGroup> <ScheduleOneDir>D:\SteamLibrary\steamapps\common\Schedule I</ScheduleOneDir> <ScheduleOneBepInExDir>C:\Users\you\AppData\Roaming\r2modmanPlus-local\ScheduleI\profiles\Default\BepInEx</ScheduleOneBepInExDir> </PropertyGroup></Project>Building
Section titled “Building”The project references BepInEx DLLs from $(ScheduleOneBepInExDir)\core\ and game assemblies from $(ScheduleOneManagedDir). After building, a post-build target automatically deploys the mod and its dependencies to $(ScheduleOneBepInExDir)\plugins\DigitalHeaven\.
The project targets netstandard2.1 and references both DigitalHeaven.Core and DigitalHeaven.Unity as project dependencies.
Mono facades
Section titled “Mono facades”Schedule I runs Unity 2022.3 Mono, whose mscorlib already defines Span<T>, ArrayPool<T> and ValueTask. The NuGet polyfills System.Memory, System.Buffers and System.Threading.Tasks.Extensions would define them again and break type identity in game (MissingMethodException in System.IO.Hashing, a Utf8JsonWriter error, a silent config parse failure that falls back to Documents\DigitalHeaven). The project sets DigitalHeavenMonoFacades, so the build and the deploy to plugins\DigitalHeaven\ ship small forwarding facades from Unity/DigitalHeaven.Unity.MonoFacades/ in place of those DLLs, and the netstandard2.1 Microsoft.Bcl.AsyncInterfaces, which is already a facade. After a deploy those four DLLs are a few kilobytes each (System.Memory.dll is about 4.5 KB, not 140 KB). Never copy the NuGet versions into the plugin folder by hand.