Appearance
The host surface
Jester means to ship one player build that never needs updating: the Unity API wrapped as host calls, mod load and unload, and the boot shell. Everything else is a mod, forever.
That promise holds exactly as far as the host's API surface reaches. Every capability a future mod needs and cannot reach is another host build the author has to ship and every player has to install. So this page answers three questions: what can a mod do today, what can it not, and what is the finite list of work between here and a surface worth freezing.
Everything counted here was counted by a command. The breakdown table below is generated out of the engine's own C# dispatch by tools/jester-surface.mjs, and the prose figures each have a row in this site's CLAIMS.tsv that re-derives them. The effort figures are the only numbers that are not measured, and they are marked as estimates.
This page was stale, and the fix was mechanical
It stood at "220 host calls" and "511 exported procs across 20 modules" while the surface roughly doubled underneath it. Nothing could catch that, because nothing read the prose. The counts are now derived; the narrative below is hand-written, and the numbers inside it are gated.
The surface as it exists
Host surface version 1.8.0, as of commit f559ceb (2026-09-10). The C# dispatch and the mod-side declarations agree exactly: there is no bridge case without a wrapper, and no wrapper without a case, and the two lists are compared by a gate rather than by hand. No mod declares an importc of its own, so there is no path by which a mod reaches a name the host does not answer.
Every count on this page is pinned to that commit and says so. The surface is under active development and moves several times a day, so a bare number here would be wrong within hours of being written, and re-deriving it on every change would only publish a figure that is stale by the time it is read. Pinned, the number is true permanently and a reader can check it against that commit. The tables below are regenerated by node tools/jester-surface.mjs <checkout> out of the dispatch itself rather than maintained by hand.
440 host calls, of which 282 live in 11 prefix-owned tables the dispatch falls through to, and 158 are inline case labels in the switch itself.
| family | calls | what it is |
|---|---|---|
infiniteless_desktop_ | 54 | file dialogs, the tray, one copy at a time, argv, dropped files, menus, global keys, and the host's own window rescue |
infiniteless_audio_ | 44 | WAV decode, clips, speakers, one-shots, 3D falloff, named volume buses |
infiniteless_window_ | 32 | frame, transparency, corners, on top, click-through, place, size, drag |
infiniteless_anim_ | 31 | skeletons and bone weights, clips built from parsed keyframes, playback, crossfade, layers, one joint |
infiniteless_network_ | 31 | transport, peers, worlds, part placement, message events |
infiniteless_import_ | 28 | finding a game the player owns, scanning it, extracting from it, reading back what it wrote |
infiniteless_http_ | 17 | prepared requests, allowlist policy, async polling |
infiniteless_text_ | 16 | clipboard, IME composition and its caret, key-repeat and double-click timings |
infiniteless_content_ | 15 | staging, hashing and committing a content set |
infiniteless_render_ | 9 | frame-rate cap, on-demand interval, idle-when-quiet, wake, frame count |
infiniteless_display_ | 5 | dots per inch, the scale factor, and whether it just changed |
| the core switch | 158 | broken out below |
| total | 440 |
The 158 in the core switch, by area:
| area | calls | what it is |
|---|---|---|
| collections | 17 | the catalog substrate mods build data on, and which catalogs exist at all |
| voice | 17 | microphone, devices, push-to-talk, VAD |
| model bytes | 12 | reading a file as bytes or text for a format-reader mod, and letting one go |
| screen UI | 11 | one panel stack, labels, buttons, hit testing |
| input | 10 | the whole keyboard, three mouse buttons, the wheel, look delta, typed text, screen size |
| meshes | 10 | vertex, uv, normal, triangle and quad building, with and without weights |
| Unity reflection | 10 | get, set and call over any public instance member, plus add/find a component by type name |
| transforms and hierarchy | 9 | position, rotation, scale, parent, active, forward, create, destroy |
| drawing | 7 | fill, text, image, measure, and the scissor the recording carries |
| services | 7 | one mod asking another a named question and getting text back |
| parts | 6 | spawn, and the provider protocol a format reader answers on |
| character | 5 | controller create, move, grounded, touched, velocity |
| materials and surfaces | 5 | a tint, and four texture slots fed from encoded image bytes |
| pointer | 5 | lock, show, position |
| the player's view | 5 | where the eye is, where it faces, and the crosshair ray |
| aiming | 4 | one ray, its hit, point and distance |
| modpacks | 4 | list, name, select, playing |
| rigidbodies | 3 | add with mass, freeze, set velocity |
| state | 2 | remember and save over scalars |
| the host itself | 2 | the surface version a mod can ask for, and the SDK stamp discovery judges a mod against |
| the rest | 7 | log, time, delta, camera create, primitives, the mod's own folder, one import read |
| total | 158 |
The gate that counts these reads the family list out of the bridge's own XHostCalls.Prefix references rather than carrying a list of its own, so a family nobody told it about is still counted the day it is added. That matters: the count was 205 for one day, was right when it was taken, and was wrong two commits later when a fifth family appeared and nothing re-counted.
Above the 440 the SDK adds 780 exported procs across 28 modules — vectors, colours, an event iterator, a catalog DSL, a settings model, a character controller, a UI library. Those cost nothing at the boundary; they are aowlmony written over the same 440 calls, and they are the part that can change without a host build.
What "no sandbox" means here
Jester is not a sandbox, and this page will not be read as claiming it is. What is true and checkable is narrower:
- The surface is finite and enumerable — 440 calls, every one of them listed, with a gate proving the host's dispatch table and the mod-side declarations are the same list.
- Gameplay is interpreted, so a mod cannot execute arbitrary machine code by construction.
- Discovery refuses a mod by name when this host cannot serve what its manifest asks for, rather than letting it reach a missing call and die inside a callback.
What is not true is that a mod is confined to what that list allows. Native code reached from the interpreter's own standard library does not pass through the host's folder checks — infiniteless.ldraw reads absolute paths through std/syncio today. A capability model over the surface is in progress; until it lands, read "no platform" as nobody takes a cut, not safe to run a stranger's code.
The escape hatch, and its exact shape
Ten of the calls are general reflection, and they change the honest answer to "what can a mod reach" far more than their count suggests: component_add and component_get add or find any UnityEngine.Component by assembly-qualified type name; unity_get and unity_set read and write any public instance property or field; unity_call invokes any public instance method — each with its handle and entity variants.
So a mod can add a Light and set its intensity, range, colour and type with no host change at all. That is real reach. Its walls are just as real:
- Scalars only. The boundary carries
long,double,bool,string, enums by name or ordinal,Coloras#rrggbbaa,System.Typeas a name, and object handles. Nothing else. A property returningVector3,Quaternion,Bounds,Matrix4x4,Ray, an array, or any Unity struct module throws rather than returning. - Instance methods only. Every Unity static is unreachable:
Physics.Raycast,Physics.OverlapSphere,Object.Instantiate,Resources.Load,AudioClip.Create,SceneManager.*,Time.*,Application.*,Mathf.*,NavMesh.CalculatePath. - No constructors. The only things a mod can bring into existence are a GameObject, a primitive, a camera, a component on an existing object, and a mesh. It cannot construct a
Mesh,Material,Texture2D,AnimationClip,AudioClip,RenderTextureorSprite.
Rules 2 and 3 are why "just add an Animator by reflection" never solved animation: Animator.Play("fire") is an instance method with a string argument and would dispatch fine, but the RuntimeAnimatorController it needs is an editor asset and cannot be constructed or loaded by any call in the surface. Animation was solved by a named family instead — see below.
The audit, by domain
Reachable = a plausible mod can do it today. Partly = it can do a useful subset and the missing part is named. Not = there is no path.
Transforms and hierarchy — reachable. Position, Euler rotation, scale, world-preserving parent, active, destroy, world position and forward. Missing and cheap: quaternion rotation, so a mod cannot compose rotations without gimbal risk; local versus world; right and up; lossyScale; sibling index; find-by-name. None of it blocks a mod; several are irritating.
Rendering — partly reachable. A mesh can be built from vertices, normals, UVs, triangles or quads — one submesh, one material, always lit, always with a collider attached. Materials get a tint and four texture slots fed from encoded image bytes. Cameras take any scalar property by reflection, which is adequate. Lighting works through component_add plus scalar properties — undocumented and undiscoverable, which is its own kind of gap. Not reachable: custom shaders, arbitrary shader properties, material property blocks, render queue, vertex colours, tangents, a second UV set, submeshes, declining the collider, updating a mesh in place, post-processing, render textures, command buffers.
Physics — partly reachable. One ray from a transform, along a chosen direction, against a layer mask, nearest hit, triggers ignored, own root skipped. Rigidbody add with mass, kinematic on and off, set linear velocity. A character controller with create, move, grounded, touched sides and velocity. Not reachable: all-hits enumeration, shape casts, overlap queries, the hit normal, forces, torque, impulses, drag, constraints, joints, collision and trigger callbacks, the layer collision matrix, gravity, manual stepping.
Animation — reachable, since surface 1.3.0. This used to be the largest hole and it is closed. The 31-call infiniteless_anim_ family adds a skeleton and per-vertex bone weights to the mesh builder, clips a mod builds out of keyframes it parsed itself, playback with speed, looping, crossfade and four layers, and read/write access to a single joint so procedural work — recoil, sway, a look-at — composes with a clip rather than fighting it. A model that weighted every vertex spawns as a SkinnedMeshRenderer with real joint transforms under it. Neither an Animator controller nor Playables: a clip built at run time is a legacy clip and a legacy Animation component plays it, because the dense clip Mecanim samples is baked by the editor alone. Still missing: root motion, Avatar and humanoid retargeting, blend trees, additive layers, blend shapes, clip events.
Audio — reachable, since surface 1.4.0. It used to be that an AudioSource could be added by reflection and played, and there was no way at all to obtain a clip, because every door to one is a static: a mod could hold the loudspeaker and never the record. The 44-call infiniteless_audio_ family is that record. A WAV is decoded out of the calling mod's own folder, or out of a named byte range of a container it ships or the player extracted; a clip can also be built sample by sample. Then one-shots the host reaps, looping speakers, 2D and positional playback, volume, pitch, seek, knowing when a sound finished, a rolloff the mod names, and named volume buses a settings screen can enumerate rather than being told about. Clips and speakers belong to the mod generation that made them and are reaped on unload. Still missing: compressed formats, streaming, effects and reverb, doppler, reading a signal back. Voice chat's 17 calls remain a separate system — they are comms, not game audio.
Text input — reachable, since surface 1.2.0. The 16 infiniteless_text_ calls are the clipboard, the input method and its caret, and the three timings a person's own settings decide — key repeat delay, key repeat rate, double-click time. Both the clipboard and the IME also answer whether they work here and why not, because a call that reaches the platform and quietly changes nothing is worse than no call. Everything else a text field needs is arithmetic and lives in mod code: a caret rule in a mod is a rebuild, and a caret host call is forever.
Input — partly reachable. The whole keyboard by name, held and pressed edges; three mouse buttons; position; look delta; typed text; screen size; pointer lock and visibility; and, since 1.5.0, the scroll wheel — in notches on either axis, with the available/problem pair beside it, because a wheel that reads zero for ever and a wheel nobody turned are the same number. Not reachable: mouse buttons 4 and 5, gamepads entirely, touch, and any rebindable action map.
UI and drawing — partly reachable. Three primitives, recorded and replayed when a camera renders so they also work headlessly: a filled rect, text in a box with three alignments, and an image with a source sub-rect — plus measurement and a small panel stack. Clipping, scrolling, nesting and drag-and-drop are not host features; the UI library implements them in mod code by cropping rectangles itself, which is exactly what the image call's source rect exists for. The one thing mod code could never recover was glyph-level clipping, and 1.5.0 is it: a clip pushed into the recording rather than a GL state call, because the recording is replayed after a camera renders and by then the frame is over. Each command remembers the clip standing over it and the replay cuts its own quads, so half a letter costs the same arithmetic as half a rectangle. A scrolling list shows a half-row, and a text field scrolls sideways instead of spilling. Still missing: line and polygon drawing, rotation, and any font other than the built-in one.
The window, and how often it is drawn — reachable on Windows. The 29 infiniteless_window_ calls give a mod its own window: leave full screen, drop the frame, per-pixel transparency through DWM, Windows 11 corner rounding, always on top, click-through, move, size, centre, title, minimise, maximise, restore, close, what the back buffer clears to, and a rectangle the mod nominates that drags the window — a frameless window has no title bar, so that is not a convenience. The 9 infiniteless_render_ calls decide how often any of it is drawn: a frame-rate cap, an on-demand interval, and an idle-when-quiet mode with a wake a mod calls when its content changed. Both are things Unity keeps behind statics, so neither was reachable through the reflection hatch at all. Windows player only: elsewhere every window call is a no-op and the available/problem pair says why; the render family works everywhere.
Being a desktop application — reachable on Windows. The 54 infiniteless_desktop_ calls are the rest of what an application is: native file dialogs that run beside the render loop rather than inside it; a tray icon with a tooltip, a context menu, click events, balloon notifications and minimise-to-tray; one copy at a time, with a second launch's arguments and working directory handed to the copy already running; the command line, the working directory and the program's path; files dropped on the window; a real menu bar or the same menu as a popup; and global keys that work when the window is not focused. It also carries the answer to a hazard the window family created: a mod that makes its window frameless, invisible, on top and click-through leaves a player with no way out, so the host itself — not any mod — watches for Ctrl+Alt+Shift+F12, restores a usable window, and then refuses further shaping until the mod reloads. Registering a file association is deliberately not here: reading the path the shell launched the program with is, but writing HKCU\Software\Classes is a persistent machine-wide side effect a mod cannot be trusted to undo.
Mod-to-mod services — reachable, since surface 1.6.0. See below.
Navigation — not reachable. No NavMesh baking, and an agent is unusable because its API is Vector3-shaped and the path query is static. Bot AI must implement its own pathfinding over geometry it generated — which for a mod that built the world itself is quite reasonable, and for a mod driving bots around an imported map is not.
Particles — not reachable. A particle system can be added but is configured entirely through struct-valued module properties, which the scalar boundary rejects. A mod can fake particles today with many small meshes.
Time — reachable. Delta, clock, and the render frame count. Missing: fixed delta, time scale, realtime since startup. Small and cheap.
Scenes — not reachable, and probably correct. A world here is built by mods at runtime; there is no evidence any mod wants scene loading.
Networking — reachable, and among the most complete domains in the surface. See networking.
Persistence — partly reachable. Four scalar types per mod, debounced to disk, profile-scoped, surviving a hot swap, with a reserved key that tells a mod whether its values came back. Only scalars — a grid inventory must serialise itself into a string, which works but has no size guidance and no binary path.
Content updating — reachable. The 15 infiniteless_content_ calls are the staged, manifest-checked disk verbs a self-updater needs and nothing more. A mod still has no general file handles: a "write under the mods root" call would let any mod write a mod, which is a worse hole than the one it fills, so this family is scoped rather than general.
Assets and resource loading — reachable, and well designed. The host owns no formats: a mod claims a file extension, is called back with the bytes, and answers with geometry plus up to four texture maps. The gaps here are the mesh gaps above — one material — and not loading gaps. See importers.
Services, and published pictures
Until surface 1.6.0 two mods shared exactly one channel, and a catalog is a noticeboard: append-only, read whenever the reader gets round to it. That is right for "here is what I contribute" and useless for "what is this worth right now" — so a voxel mod that broke a block had no way to hand the item to a HUD mod, and every consumer that needed something from a producer polled a catalog signature once a frame waiting for it to appear.
The seven infiniteless_service_ calls generalise a mechanism the host already had and had spent on exactly one problem. A mod registers a service name; the host reaches back into that mod through a named proc, hands it a request and takes an answer.
nim
# in the mod that knows
provideService("voxel.item")
proc serveRequest() =
if serviceName() == "voxel.item":
answerService(describe(serviceArgument()))
# in the mod that asks
if serves("voxel.item"):
let described = callService("voxel.item", "minecraft:stone")
if described.len == 0: log(serviceProblem())Text in, text out, and that is not a placeholder. Two mods that want structure encode it and agree the encoding between themselves. A typed call would mean the engine holding a schema, which would mean the engine being told about every service anybody ever invents — exactly the coupling the mod boundary exists to prevent. The host knows one fact about a service: it has a name, and some mod claims it.
serves() replaces the polling loop. It is the available/problem convention this surface uses everywhere, asked of one name: false until the mod that serves it has loaded and registered, true from then on.
What cannot happen: a mod cannot call its own service. A mod already answering cannot be re-entered, so A asking B asking A is refused at the second A rather than found out when something runs out of stack, and nesting is capped at eight regardless. A provider that falls over mid-answer, or unloads mid-answer, is caught: the caller gets an empty string and a sentence, rather than the frame ending for everyone who asked it anything. An empty answer is not by itself an error — a service may deliberately answer nothing — which is why serviceProblem() is carried beside every call.
1.6.0 opened three more doors that had been closed for the same reason, that the mod folder boundary was the only answer to every question about another mod:
published:<mod>/<name>names a picture another mod offered. It is a spelling, not a call, so it is understood everywhere adata:URI already is —useTexture,useBumps,modelBytes,hasImage,hasModelFile, surface textures anddrawImageat once. Publishing is a catalog claim, so it stays the owner's decision, and the file resolves inside the publisher's folder: no path from one mod ever names a file in another. It is also what stops a catalog going quadratic — a row that had to carry a picture as adata:URI can carry a twenty-character name instead.- Catalog enumeration — which catalogs exist at all, their kind and note, and who filled them. A browser over every catalog of parts could previously see one only if its provider had opted into some agreed registry.
- The player's view — where the eye is, where it faces, and the crosshair ray. Aiming starts from a handle and a handle belongs to one mod, so a mod that did not build the character could not ask. Read-only, deliberately.
Calls were added and none changed, so every mod asking for ^1.0.0 through ^1.5.0 keeps running; a mod that needs any of the above asks for ^1.6.0.
Applications
A player can now launch straight into one modpack rather than into the picker:
Jester.exe --app <id><id> is a modpack id. The launched modpack gets its own window title (the modpack's name — nobody's chat client is called Electron), its own taskbar identity, its own icon if its shell mod ships one, and its own save directory under Apps\<id>\. Content is never duplicated: mods and modpacks stay the shared library every launch reads. An application is a modpack plus a shortcut, and it weighs what its own content weighs — which is the whole point of the comparison with shipping a copy of a browser per app.
Nothing that has ever been saved moves, is copied, or is deleted. The consequence stated honestly: the first --app launch of a modpack you had previously played from the picker starts with empty state, because that state lives in the game's own save directory and nothing migrates silently. --shared-data is the documented way to go on using it.
A shortcut outlives the content it names, so a bad id is a working game that says what is missing — named in the window rather than only in a log, with the picker behind it — never a black window.
Left undone: no modpack in the tree ships an icon.ico yet, so the icon path is written and wired but has not been seen doing anything; a shortcut cannot carry the taskbar identity the runtime sets, so a shortcut pinned before its app has ever run may still group under Jester's own; and there is no installer.
Ranked gaps
Ranked by whether a mod being written right now is blocked. Effort figures are estimates, in engineer-days, and nothing still open below has been prototyped.
| # | gap | what it blocks | est. days |
|---|---|---|---|
| 1 | Done, 1.3.0. The infiniteless_anim_ family is the skeleton, the bone weights, the clips and the playback clock. | done | |
| 2 | Done, 1.5.0. Notches on either axis, with the available/problem pair beside it. | done | |
| 3 | Done, 1.5.0. A clip carried in the recording rather than a GL state call, so a glyph is cut exactly like a rectangle. | done | |
| 4 | Done, 1.4.0. Decode to a clip, one-shot and looping, 2D and positional, buses. | done | |
| 5 | Submeshes and several materials | multi-material imported models, and chunked worlds that want separate opaque, cutout and translucent passes | 2–3 |
| 6 | Declining the collider, and updating a mesh in place | world import at scale: every chunk pays for a collider whether it wants one or not, and a changed chunk must be rebuilt as a whole new object | 1–2 |
| 7 | Hit normal, multi-hit, shape casts, overlap queries | bot AI (line of sight, cover, proximity) and any weapon wanting a surface normal | 2–3 |
| 8 | Trigger and collision callbacks | pickup volumes, trigger zones, anything event-driven rather than polled | 2–3 |
| 9 | Quaternion rotation | anything composing rotations. Euler-only is a correctness hazard rather than a wall | 0.5 |
| 10 | Time scale, fixed delta | slow motion, deterministic ticks | 0.25 |
| 11 | Gamepad input | nothing today; a shooter eventually | 1–2 |
| 12 | Particles | muzzle flash, blood, sparks. Fakeable with meshes today | 3–4 |
| 13 | Navigation | bots on an imported map | 4–6, or never if pathfinding stays a mod |
| 14 | Custom shaders and material properties | nothing today. Worth exposing before the freeze, because after it nobody can add it | 2–3 |
| 15 | Binary or bulk state | a scale problem, not a wall | 1 |
| 16 | A capability model over the surface | running a mod you did not write. In progress; see above | not estimated |
Distance to a freezable host. The four items that dominated this list are done. What remains of items 5–10 is what the named mods actually need; items 11–15 are the "do it before the door closes" set. None of it is research; all of it is plumbing along a boundary that already exists and is already exactly consistent between its two sides. The real risk is not any single item — it is freezing before something like item 14, or item 16, has been thought about.
The host contract
For "it will always work with any mod" to be a fact rather than a hope, three things must be true. A mod must say what it needs. The host must check that at discovery, not fail somewhere inside a callback. And the SDK-stamp rule must not turn a single mod update into a broken tree.
Where the version lives
In mod.json, beside the dependency ranges it already carries, because that file is the mod's declaration of what it needs and it is read at discovery, before anything loads:
json
{
"id": "example.hello",
"version": "0.1.0",
"host": "^1.6.0",
"dependencies": [ { "id": "infiniteless.parts", "version": "^0.1.0" } ]
}Not in the built artifact. The artifact is the compiler's output and its identity is a content hash; the requirement is authorial intent, and belongs with the author's other declarations.
The host declares one semantic version for its call surface: patch for a bug fixed behind an unchanged signature, minor for calls added — old mods keep working, so ^1.0.0 accepts 1.6.0 — and major for a call removed or its meaning changed. That last is the number the freeze is about; after the freeze it should never move again. An absent or empty host means no constraint, so every mod written before the rule keeps loading.
Minor has moved six times and never broken anything: 1.1.0 added the window and render families, 1.2.0 the text, display and desktop families, 1.3.0 animation, 1.4.0 audio, 1.5.0 the scissor and the wheel, and 1.6.0 services, published pictures, catalog enumeration and the player's view. Calls added, none changed, every time.
A mod that asks for more than this host has fails at discovery, as a named failure saying which mod, what it asked for, and what this host is — the same channel that already reports a bad manifest or a missing artifact, and which the boot shell already renders. It does not load, so it cannot reach a call that is not there and abandon start() mid-procedure with nothing in the log. That is the entire point: a crash somewhere inside a callback becomes a sentence the player can act on.
This is implemented. The field, the check, and the failure message.
The SDK stamp, and why a frozen host makes it worse
Every mod compiles the shared SDK into its own artifact under one module id, and the interpreter keeps a single program table, so whichever mod loads first imposes its copy of the SDK on all the others. The current rule is a strict majority vote over a content hash of the SDK tree carried in each mod's build receipt. The minority is left unbuilt; with no majority, nothing is trusted.
A majority vote is the right answer for a development tree that is rebuilt whole. It is the wrong answer for a shipped world, for two reasons: the stamp is a content hash, so it changes when a doc comment changes — it can say "different", it can never say "incompatible"; and under auto-updating mods the majority is whatever most mods happened to update to last, so shipping one updated mod into a set of nine leaves the updated one silently not loading, while updating five of nine flips the majority and stops the other four. The failure moves around with the update schedule, which is exactly the thing a frozen host is promising will never happen.
The design is to replace the vote with a fixed reference: the host stamps into itself, at build time, the SDK hash it was built alongside, and publishes a small set of stamps it accepts. Then a mod is refused by name, against the host, not against its siblings, and the refusal is stable — it depends only on the mod and the host, so a mod that loads today loads tomorrow no matter which of its neighbours updated. That composes cleanly with the host field: host is the author's statement of what they need, the SDK stamp is the builder's record of what they compiled against, and the host checks both against constants it carries. Neither can be affected by another mod.
This half is designed and not built. It needs a build-time generator, and the majority vote is not hurting anyone in a tree that is rebuilt whole. The better long-term answer is for the SDK to gain its own semantic version, so that a hash stops being the compatibility question at all.

