Building a slider workbench and an install gate for models
A hashed record of 19 pipeline steps that the installer checks, a Godot tool window that drives headless Blender, and test runs moved off the player's saves.
A generated model can’t be installed in Blobber now unless a record beside it shows every step of the pipeline was run on those exact bytes. A tool window puts every number of the five model generators on a slider with a live preview. Both were built on 9 October. On the 10th, test runs stopped writing where my saves live. The work is done by AI coding agents that I direct.
How do I know a model went through every step?
The installer refuses a model whose record is incomplete, failed, or made for other bytes.
Until now the steps lived in several scripts and two skills. A worker could skip one and nothing noticed. Twelve variants of the old orc had been installed while failing a check.
The steps are one numbered list of 19 in pipeline.md, the same for every class of model.
Steps 1 to 15 are needed to install:
- spec: built from a spec on its class’s kit, with a brief
- build
- gaps
- clips: the clip check for anything held or worn
- carve: unseen faces removed, protected collapse
- theme_state
- file_pass
- validate
- triangles: within the class’s target and cap
- class_fields: sockets and the manifest’s fields
- lod_collision
- twice: built twice to the same bytes
- sheets: at 2, 5 and 12 m in both lights, read, and by whom
- compare: against what it replaces
- godot_import
Steps 16 to 18 are needed to be in the game: a test, a film, credits. Step 19 is my approval with its date and words.
The record is <stem>.pipeline.json. It names the .glb and every texture it references
with a SHA-256 each, one hash of them all called the subject, and a hash of the manifest
with its date and generator hash left out. Every step carries the subject it was run on,
the tool with a hash of the tool, its numbers and a date. A changed file, a changed
manifest, or a step run on another subject is refused as stale. A rebuild to the same bytes
keeps the steps a person did, and other bytes drop them.
The first design put the record inside the model’s manifest. Every build rewrites the manifest, so a step a person did would be lost. The game reads it. And it is one of the things the record has to vouch for. The record is a file of its own.
The build writes steps 1 to 11 itself (pipeline.seal). Steps 12 to 19 are set by
commands: pipeline.py twice, read, compare, godot, mark, approve. The gate is
pipeline.gate() at the top of the one installer, 18 added lines. It prints
REFUSED <model>: step N ... and the build exits 1.
The gate doesn’t trust the record’s numbers. A model of 12,000 triangles whose record said it passed a 10,000 cap is still refused, because the gate counts triangles from the file.
The first version let a model stand with steps “owed by name”. I ruled that out the same day as a way round the gate. Now nothing is owed, and a step “does not apply” only by a table in code, by step and class, each with its reason.
Removing that excuse found something. Weapons had been excused the carve step, on the ground that closed solids of 84 to 312 triangles have no buried ends. Run for real, the step took hidden faces off every one. The longsword went from 132 triangles to 114 and the void blade from 232 to 158. All twelve were rebuilt and reinstalled through the gate.
A third pass closed two gaps. A plain body with armour hard-coded onto it would have passed as an unthemed root. A family now declares its root part by part in a plain table in its generator, which the gate reads without running it. And a family declares what each model takes over from, or “replaces nothing” with its reason.
Of 71 installed models, 13 are under the record: the new orc and twelve weapons, all trials. The 58 older models are allowed by name, on a list meant to shrink to nothing. Two blind spots are written down: geometry worked into a declared part without changing its count, and a successor that shares no word of its name with what it replaces.
Building a slider workbench on the generators
workbench.bat opens a tool window on the five generators: rocks, flora, heads, bodies and
the orc family. Every number appears as a slider, and Blender builds the preview again when
a slider comes to rest.

I wanted to load a generator, see its variables as sliders, save the models I like, and draw N variants inside ranges from any starting point.
The window is a Godot tool scene in GDScript, because the game’s light is where a model is judged. Headless Blender does every build.
A class is one file. tools/blender/schemas/<class>.py holds SCHEMA, a list of knobs
made with num, whole, choice, switch and seed. A knob has a key that is a path
into the kit’s spec ("size.0", "eyes.open.1"), a least, most, step and default, a group
and a line of help. A new class needs no workbench code.
A model is JSON: {format, generator, schema, name, note, date, base, values}. base is
the study member it started from. Whatever is not a knob (colours, a head’s tusks, a hide)
stays the base’s, because those are functions in the generators and a function can’t be
saved.
tools/blender/workbench_build.py serve <folder> keeps Blender running on a folder of
jobs, so a draft doesn’t pay Blender’s start. There is one live lane where the newest job
wins, for a slider being dragged, and batch lanes taken in turn, for a grid of variants.
A draft is the kit’s own build stopped before the bake, the file pass, the levels of detail
and the clips. An error from a kit is text in the result, never a dead service. By the
worker’s note, a rock drafts in 0.3 to 1.4 s, a head in 0.8 to 1.1 s and the orc family in
2.1 to 2.4 s.
Builds are cached under a key made of the generator, the schema’s version, the base, the values and a hash of the kit’s files. The same key is computed in GDScript and in Python, and a test holds them equal.
Each knob can be locked or given a range. Four presets (minor, moderate, major, extreme)
set a range either side of every unlocked knob’s value. They started at 4, 10, 22 and 45%
of the knob’s whole range. After trying the window I set them to 5, 15, 35 and 65%, and
they are now boxes in the window, kept in presets.json. No preset opens a choice. The
draw is seeded and bunched toward the value by a triangle distribution.
“Want this” writes a pick: the spec, my note, the draft’s numbers and a picture. A pick is a choice of shape. It skips no step of the pipeline.
On the morning of the 10th, ranges learned to depend on a choice. An arch only builds near
one size, and it had been offered a boulder’s range. A knob may now carry
within = {choice: {option: (least, most)}}, and the slider travels over that row. A row
is where a piece builds and not where it looks right: a litter stone can still be dragged
into a 10 m needle. How a rock is turned became three angle knobs as well.
It’s shape only: no colours, textures, clips or weapons. The slider ranges are a worker’s proposals, from 1,724 builds with each knob at its least and most, 6 of which fail.
Keeping test runs out of the player’s saves
Every run the tools start (a test, a film, a script check, an import) now has its engine user folder inside the workspace. A film had overwritten three of my save slots the day before.
Godot has no command-line option and no project setting for the user folder.
XDG_DATA_HOME is ignored on Windows. Godot 4.7 on Windows derives the user folder, the
editor settings and the editor cache from the APPDATA and LOCALAPPDATA environment
variables. tools/gd.sh is the only way the tools start Godot, and it exports both as
folders under <workspace>/Tmp/Blobber/godot_home before starting the engine. user://
lands inside that home. The launchers I play with don’t go through gd.sh and set
neither, so saves and settings are where they were.
Two things broke. A new home has no editor settings, and the first import printed a script
error from the terrain addon, which reads a setting that isn’t there yet. The launcher now
primes a new home by opening the editor once on an empty project. And Python keeps its user
site-packages under APPDATA, so tests that start Python lost their packages until
PYTHONUSERBASE was pointed back.
A guard, tools/user_folder.py, records names, sizes and modification times under the
three real folders before the first test and after the last. Any entry added, changed or
removed fails the run as a test named user_folder. Its list of allowed exceptions is
empty, and it tests itself on a stand-in folder at the start of every run.
Both whole-suite runs passed all 122 tests and failed the guard, on 11 and then 20 entries written by runs from other worktrees that still had the old launcher. A clean whole-suite run is still owed.