Skip to content

apps/worker ​

The generator worker (Technical specs §9.3): a level spec in, a level out. It checks the spec against @foi/catalogue's schema, runs the generator in headless Blender (blender -b --factory-startup --python assets/blender/levels/generate.py -- --spec <file> --out <dir>) with a time limit, and turns what Blender writes (the level's own kit, its recipe, its places) into level data with the level pipeline (@foi/tools's exportRecipe: the scene level:recipe would write, read back as level:export reads it, all in memory), in the canonical form of a level file, then renders the level's preview: a second headless Blender run of assets/blender/levels/preview.py over the level data (an orthographic camera straight above the whole ground, as the minimap shows it; Cycles on the CPU, so no GPU or display is needed; a 384 px PNG). A preview that fails leaves the level without one (stats.previewError says why); it never fails the generation. The platform API builds its spec drafts with it, over HTTP or as a library in its own process (see the Platform guide).

FileWhat
generate.tsgenerateLevel(spec, options): the spec checked, written to a temporary folder, Blender run, its refusals (generate: <path>: <message> on exit code 1) answered as issues, its output read into { level: { name, data }, places, stats }. A region is built in this process (@foi/tools' writeRegion), Blender only for its towns and its preview; its places are those its places file holds, its towns' merged in. A city is solved in this process first (@foi/catalogue's buildCity: its issues without Blender) and its plan handed to Blender beside the spec (--plan), with a time limit of 6 minutes. Its result says what generated the level (generator: the release, and where Blender built towns townScriptsHash of assets/blender/levels/*.py and Blender's version from its first line), for the build's manifest.
audit.tsauditGenerated(level, places): every generated level audited and repaired (@foi/world's repairLevel with the kit library's facts) before its preview and its cache, a prop standing on a named place kept; what was left out and the faults left go in the stats (stats.audit), which the build turns into warnings.
fuzz.tsnpm run level:fuzz: specs drawn from seeds (@foi/catalogue/testing) built through the whole pipeline and held to a bar (the generator never throws, a refusal names paths the spec has, a level has no fault left after its repair and lost at most 0.5 % of its pieces to it, one in ten built twice the same); a failure is a line with its seed and a file holding the spec (--replay <file>); fuzz-corpus/ keeps those that ever failed (--corpus, replayed on every release).
blender.tsfindBlender (FOI_BLENDER, else blender on the PATH, else the installers' folders: the newest Blender Foundation\Blender x.y on Windows) and runProcess, the process runner with a time limit (the tests inject a fake one).
preview.tsrenderPreview(level, options): level data written to a temporary folder, preview.py run in headless Blender with a time limit, the PNG's bytes; bundledPreview(name) reads a bundled level's committed picture from previews/ (the API's builds store it; @foi/worker/preview loads nothing of the level pipeline).
render-previews.tsnpm run level:previews [-- <level>…]: renders the bundled levels' previews into apps/worker/previews/<level>.png and removes those of levels that are gone.
server.tsThe HTTP endpoints: GET /health, POST /generate {spec} → 200 { level, places, stats, preview? } (the preview a base64 PNG), 422 { issues }, 500 { error }; one generation at a time, the others queued.
index.tsThe process: finds Blender, serves the endpoints.
lib.tsThe library entry @foi/worker (generateLevel); @foi/worker/blender and @foi/worker/preview are the light ones that find Blender and read the committed previews.

The root .env.example lists them with their comments (the stack's worker has them fixed in deploy/compose.yaml).

VariableDefaultWhat
FOI_WORKER_PORT8090The port.
FOI_WORKER_TIMEOUT_S360 / 120The time limit of one generation, in seconds (unset: a city's 360, every other template's 120).
FOI_BLENDERfound (see above)Blender's executable.

Run it with npm run -w apps/worker dev; the API sends it its builds when FOI_WORKER_URL=http://localhost:8090. It only ever starts its own Blender, in the background; never the interactive one the Blender MCP server drives. generate.test.ts runs generateLevel with a fake Blender that writes the committed output of test-generated-town, and the level must be the committed packages/world/levels/test-generated-town.json (what CI runs, without Blender); preview.test.ts runs renderPreview with a fake Blender and checks the committed previews (PNGs of bundled levels, one for every main-* level); preview.integration.test.ts renders a bundled level in the real Blender (skipped without it). generate.integration.test.ts runs the real Blender on the committed spec and generate.py's checks on the catalogue's spec cases, and skips itself where Blender is not installed. fuzz.integration.test.ts builds twelve random regions (no towns: no Blender) on every pull request; the nightly Level fuzz workflow builds thirty grid towns, ten regions with towns and twelve cities (--template city: drawn until the solver accepts one, one in three 1000 m, the largest a spec may draw) in the worker's image.