# B14: spawn-tuning measurement `b14.py` automates SPAWN-TUNING.md §B14. It compares the upstream spawn defaults with the shipped partial files (`d80f85b`, `8e8483d`: `alexsmobs-common.toml`, `fowlplay.json5`, `crittersandcompanions-common.json5`). Each comparison stands still for 10 minutes at a river, a jungle and a warm ocean, and counts entities within 128 blocks. It reuses `../bench.py` as a module: `launch`, `kill_client`, `LogTail`, `monitor`, the `Journal` / `restore_pending` crash-safe restore, `apply_pins`, `apply_dh`, and `zip_world_root`. It needs nothing beyond the Python 3 standard library. ## Loop ```sh cd analysis/client-perf/harness python3 b14/b14.py run --config defaults --dry-run # world, config plan, datapack python3 b14/b14.py run --config defaults # ~32 min, all three biomes python3 b14/b14.py run --config shipped python3 b14/b14.py summary # SPAWN-TUNING §B14 verdict ``` - `--biome river,jungle,warm_ocean` (default `all`) runs the sites in that order in one launch. - `--minutes 10` (default) sets the stay at each site. - Repeating runs (for example two of each config, interleaved defaults → shipped → defaults → shipped) gives medians. The summary takes the median across runs. - Results go to `~/onion-bench/results/b14.jsonl`. Each count line is a `b14_count` row, and each launch is a `b14_session` row (outcome, config check, git/mods hash, sites, archived log at `results/logs/session-.log`). - `b14.py restore` (or `bench.py restore`, which uses the same journal) undoes a hard-killed run. - `b14.py parse LOG` prints the count rows found in any archived log. The script refuses to start in these cases: - a restore journal is pending; - a bench client is running; - any `matrix.py run`, `bench.py session` or other `b14.py run` process exists. It shares the OnionBench instance with the matrix, so **never run it while the matrix runs.** ## Design ### Throwaway world `saves/onionspawn`, one per run - It contains only the bench world's `level.dat` (seed `onionbench`, gamerules, datapack list), the `onion-bench` datapack (it has no load or tick tags) and `serverconfig/`. - It has no `region/`, `entities/` or `data/` (no forceloads, no DH database). Every chunk around a site is therefore generated during the run, under the config being measured. That covers both kinds of spawning: the initial creatures placed at world generation and natural spawning. - The seed is the same, so the terrain is identical between runs. The world is journaled as a `tree` entry and deleted on restore. The bench world `saves/onionbench` is never touched. ### Sites are pre-located offline (`b14.py locate`) `/locate biome` can't be used from a datapack. Functions run with suppressed output, and `execute store` only captures the distance, not the coordinates. So `locate` parses the overworld region files in `bench-world-v1.zip` with a small NBT reader (`nbt.py`): - It reads each chunk's biome palette at the surface, using the `MOTION_BLOCKING_NO_LEAVES` heightmap in full chunks and `WORLD_SURFACE_WG` in proto chunks. - For each target biome it picks the chunk whose 5×5 neighbourhood has the largest share of that biome. - Sites must be at least 192 blocks from the world spawn and the saved player position (spawn chunks and the first-load area stay outside the 128-block count sphere), and at least 320 blocks from each other (the count spheres don't overlap). Biomes depend only on the seed, so the coordinates are valid for any world with this seed and mod set. They are committed in `sites.json` (from zip sha256 `144caf5f…`, seed −306676217): | biome | x y z | biome at site | share in 5×5 chunks | |---|---|---|---| | river | 904 63 88 | minecraft:river | 0.37 | | jungle | −8 107 −201 | minecraft:jungle | 0.52 | | warm_ocean | −200 63 56 | minecraft:warm_ocean | 0.75 | Every count line carries `biome_ok`, an in-game `execute if biome` check at the player's feet (`minecraft:river`, `#minecraft:is_jungle`, `minecraft:warm_ocean`). If the regenerated terrain doesn't match the offline lookup, the summary marks that biome invalid instead of silently measuring the wrong place. ### Config variants (journaled, bench instance only) - `defaults` deletes the three files. Each mod regenerates its upstream defaults on load (the same as "these three config files removed" in SPAWN-TUNING). They aren't in `config/defaultoptions/extra/`, so Default Options can't put them back. - `shipped` writes `git show :config/` (default `HEAD`), meaning the committed partials, not the working tree's boot re-saves. - After the run, and before the restore, the files the mods re-saved are read back and checked against the expected values. A mismatch fails the run (`config_check.ok=false`), and the summary ignores that run. The expected upstream defaults were verified from the jars: - Alex's Mobs: the `# Default:` comments in its re-saved TOML: gazelle 40, capuchin 28, elephant 30, straddler 70, rocky roller 60, underminer 50, blobfish 30, mudskipper 28, caiman 29. - Fowl Play: `FPConfig.` bytecode: duck 4/max 6, goose 4/4, gull 5/8, sparrow 20/6. - Critters and Companions: `CACCommonConfig$SpawningConfig.`: sea bunny warm ocean 32 (max 4), other oceans 16; leaf insect jungle 14, forest 6; dragonfly lush 10. ### Datapack (`saves/onionspawn/datapacks/b14-autoplan`, pack_format 48) - **Start-up:** a load tag polls once a second until a player exists. Then it pins the rules that matter for spawning: - `gamemode creative` (it counts for spawning and despawning, where spectators don't, and the player can't die); - `doMobSpawning true`; - the daylight, weather, patrol, trader, insomnia and warden rules; - time 6000 and clear weather. - **Per site:** 1. Teleport above the site. 2. 5 s later: clear a 3×3×3 pocket, put a barrier at y−1 (a platform over water), and teleport onto it. 3. Count at t=0 (+10 s), then every 60 s up to t=10. Each count re-asserts the barrier and the teleport, so the player never drifts. - **Each count** runs `execute store result score # b14 if entity @e[type=,distance=..128]` as the player, for the 22 tuned species and 10 vanilla spot-check species. It also stores: - `total` = `@e[type=!player,distance=..128]`, as SPAWN-TUNING defines it; - `nonitem`, which also excludes items and XP orbs; - `tuned`, the sum of the tuned species. - **Output:** a `tellraw` prints one line, which the client logs as `[System] [CHAT]`: `B14 t= biome_ok=1 total=N nonitem=N tuned=N alexsmobs:gazelle=N ...`. - **Errors:** `Failed to load function b14:` in the log (an unknown entity id makes the whole function unloadable) aborts the run as `datapack_error`. ### Entity ids All are checked against the jars in the bench instance. Each has an `entity..` lang key, and is either in the mod's registry class strings (`CACEntities`) or has a `loot_table/entities/.json`. - **Alex's Mobs** (`alexsmobs-1.22.17`): `gazelle`, `capuchin_monkey`, `elephant`, `straddler`, `rocky_roller`, `underminer`, `blobfish`, `mudskipper`, `caiman`. - **Fowl Play** (`fowlplay-1.2.3`): `duck`, `goose`, `gull`, `sparrow`. - **Critters and Companions** (`crittersandcompanions 2.7.0`): `leaf_insect`, `sea_bunny`, `dragonfly`, `ladybug`, `stag_beetle`, `roly_poly`, `snail`, `stick_bug`, `weevil`. ### Verdict (`summary`, SPAWN-TUNING §B14) - **Per biome**, using the final count (t = minutes): - **keep** if the median `total` drops under shipped *and* no tuned species that appeared at any minute under defaults is absent under shipped; - **back off** (listing the species) if one disappears; - **no drop** otherwise. - **Warnings:** `rare_warn` flags species whose shipped maximum is under 25 % of the defaults maximum (when defaults saw at least 4). `vanilla_gone` flags cows or sheep that vanished. - **Overall:** `keep` only if all three biomes keep. - **Excluded runs:** runs with a failed config check or a non-ok outcome are ignored. A biome with `biome_ok != 1` is `invalid`. ## Manual prep and limits - **Prep:** nothing beyond what's committed. `sites.json` is already generated. Re-run `b14.py locate --out b14/sites.json` only if the seed, the source zip or the biome-affecting mods (Terralith, Tectonic) change. - **Thin biome coverage:** the source world has only about 2300 chunks with final biomes, so the river and jungle patches are small (37 % and 52 % of the 5×5 neighbourhood). The 128-block count sphere also covers neighbouring biomes. This is the same for both configs, but it lowers the absolute counts. - **Better sites (optional):** on `~/onion-bench/server-bench` (seed `onionbench`), run `/locate biome minecraft:river` (and jungle, warm_ocean), check the share around each result, and put the coordinates into `sites.json` by hand. The `y` value is the first free block above the surface or the water. - **Jungle site from a proto chunk:** its `y` is the height before trees are placed. The 3×3×3 air pocket keeps a tree from burying the player, and `biome_ok` still guards the biome. - **Hostile mobs in `total`:** difficulty stays as in `level.dat` (normal), so hostiles from caves within 128 blocks count toward `total`. `tuned` and `nonitem` are recorded separately for reading the result. - **Machine:** this is proxy-machine data (singleplayer, simulation distance 8 pinned). Spawning is server logic, so the GPU doesn't matter, but CPU load does affect chunk generation speed.