shisaku ~/devlog
$ auth github

Author sign in

Sign in with GitHub to comment on devlog entries.

Continue with GitHub
← all posts

Devlog #12 — Building the Tool That Builds the Scenes

Turning the choreography engine into a *\*contract\**, then standing up a separate app on top of it — the unglamorous plumbing that decides whether non-programmers ever get to direct this game.

Devlog #12 — Building the Tool That Builds the Scenes

Date: July 1, 2026

Focus: Turning the choreography engine into a contract, then standing up a

separate app on top of it — the unglamorous plumbing that decides whether

non-programmers ever get to direct this game.


There is a certain kind of feature that produces no screenshot. You work for a

day, everything compiles, the tests are green, and if you show it to someone they

say “so what does it do.” The honest answer is "it does nothing you can see, it

just makes six future things possible." This devlog is about one of those.

The choreography system already worked. It could stage the intro, walk companions

in from off-screen, fire camera nudges, run gestures — all from a TOML file. The

problem is that a TOML file is a programmer’s idea of “accessible.” It is

accessible the way a car engine is accessible when you remove the hood: technically

everything is right there.

The actual goal has always been a Windows app where a writer who has never seen a

curly brace can direct scenes like a film. So this stretch was about the boring,

load-bearing question underneath that goal: what does such an app actually need

from the engine, and can it get it without dragging the whole game along?


1. A Format You Can Only Read Is Half a Format

The first embarrassing discovery: the choreography structs could be read but not

written. They derived Deserialize and not Serialize. The engine could load a

scene; it could not save one back. For a game that is fine — the game never edits

its own scripts. For an editor it is fatal, because “load, change one thing, save”

is the entire job.


*// before*

#[derive(Debug, Clone, Deserialize)]

pub struct SequenceDef { */* ... */* }



*// after*

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct SequenceDef { */* ... */* }

That is not a clever change. It is a change I should not have needed to make,

which is exactly the sort of thing you only notice when you stop asking "does this

run" and start asking "could someone else’s program hand this back to me

unchanged." The serde tags were already symmetric, so once the derive was there,

TOML → model → JSON → model → TOML round-tripped losslessly. I wrote a test that

does precisely that loop over the stock scene, because the day it stops being

lossless is the day the editor starts quietly corrupting people’s work.


2. The Editor Speaks JSON, The Writer Speaks Neither

The person using the tool will never see JSON. That was a firm requirement, and a

good one, because the moment a “content tool” leaks its serialization format the

non-technical user is gone. But internally, JSON is the right currency: editors,

schemas, and web UIs all love it, and TOML tooling is thinner.

So the arrangement became: the game keeps reading TOML, the app speaks JSON, and a

small CLI bridges the two. The writer sees a timeline and some actors on a stage.

JSON is the pipe under the sink — necessary, and nobody should have to look at it.


choreo validate <file>        # deep checks, human errors

choreo convert  <in> <out>    # toml <-> json, lossless

choreo schema   [--out path]  # emit the JSON Schema

choreo preview  <file> <seq>  # headless scene timeline

The nicer part is that the schema is generated from the Rust types, not written

by hand. If someone adds a new beat to the engine next month, the schema updates

itself and a test fails if the checked-in copy went stale. A hand-written schema

would drift the first afternoon nobody was watching. This one cannot.


3. The Question That Decided Everything

Then came the fork that mattered: what technology is the app built in? Rust with an

in-process UI? A web app in a native shell? Something else entirely?

I nearly answered it the usual way — by preference and vibes. Instead I noticed

that the whole decision hinged on one unknown: live preview. If the only way to

show a writer what their scene looks like is to run the actual game engine, then

the app is chained to the engine’s language and framework forever. If a scene can

be previewed without the game, every option stays open.

So rather than argue about frameworks, I built the thing that answers the question.


4. The Preview That Doesn’t Need The Game

The keystone turned out to be a small pure module that computes, for a given

sequence at a given time, where every actor is, which way it faces, whether it is

visible, and what the camera is doing — with no Macroquad, no game loop, nothing

but arithmetic.

The trick to keeping it honest is that it does not reimplement the engine. It

drives the real ChoreographyEngine::tick, takes the same beat-intents the game

takes, and applies them to an abstract world of points instead of sprites. Same

timing, same triggers, same step-and-beat semantics — just a different thing on the

receiving end.


$ choreo preview Assets/Data/choreography.toml eve_walk_in_demo

...

eve   (2798,1080) -> (2070,1080)   # walks in from off-screen east, stops at her mark

That one line of output is the whole argument. A scene can be evaluated headlessly,

faithfully, in pure Rust, and emitted as JSON any program can draw. Which means the

editor never has to embed the game. Which means the framework choice is now about

ergonomics, not capability.

It also caught a real bug in itself immediately: the first version ran the preview

in intro mode, so the intro’s own “hide the companions” sequence kept firing and

fighting the walk-in I was trying to watch. The actor kept vanishing. The fix was to

preview each sequence in an isolated neutral mode, which is obvious in hindsight and

was not obvious at 1 a.m.


5. Naming Is Half the Work, Emotionally

With preview proven stack-independent, the app choice fell to whatever gives a

non-technical writer the best node-and-timeline editor — which is the web ecosystem,

wrapped in a small native shell, kept in its own repository so none of that weight

touches the game.

It needed a name. The tool composes recurring, deliberately-arranged themes and

weaves them through the work, which is the literal definition of a leitmotif. So

Leitmotif it is. It lives as a submodule, has its own version, and talks to the

game only through three things it will never be allowed to forget: the choreo

CLI, the generated schema, and the preview JSON. No source coupling. If the game

crate never learns the app exists, that is the correct amount.

The A0 scaffold does exactly one honest thing so far: it opens a window with three

buttons that reach across to the choreo binary and come back with real answers.

That is not a screenshot anyone will frame. But it is the first moment the tool and

the engine spoke to each other through a seam that was designed rather than

improvised, and that seam is the whole point.


6. The Part I Would Pretend Was Planned

I would like to claim I saw this sequence in advance — contract, then schema, then

preview, then app. I did not. What actually happened is that each step refused to

let me skip it: you cannot schema-drive a format that cannot serialize, you cannot

choose a UI stack until you know whether preview needs the engine, and you cannot

name a thing until you know what it is.

If I reduce it to one sentence: the editor became possible not when I built a UI,

but when I made the scene file into something a second program could hold, hand

back, and preview without asking the game for permission.

The scenes have not changed. What changed is who is now allowed to write them.

$ comments

Reader notes

0 notes

No notes yet.

If you're reading this as a developer: this devlog is built in the open.

If you're reading this as a modder: the direction is source-visible, inspectable systems.