Birdoggydog's Builds

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:

  1. spec: built from a spec on its class’s kit, with a brief
  2. build
  3. gaps
  4. clips: the clip check for anything held or worn
  5. carve: unseen faces removed, protected collapse
  6. theme_state
  7. file_pass
  8. validate
  9. triangles: within the class’s target and cap
  10. class_fields: sockets and the manifest’s fields
  11. lod_collision
  12. twice: built twice to the same bytes
  13. sheets: at 2, 5 and 12 m in both lights, read, and by whom
  14. compare: against what it replaces
  15. 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.

Eight captioned frames of a dark tool window. A list of models on the left, a 3D preview in the middle of a body, a boulder, an arch, a tree and an orc’s head, columns of sliders on the right, and a grid of small variant thumbnails below the preview.
Frames from the workbench's film: a body, rocks, a tree and a head, each with its own knobs.

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.