# Level editor – importing object types from other levels ("All worlds") Goal (owner): import objects of **all** worlds into the current level, so a fresh map does not start from scratch. Packages: `format.stpcmerge` (STPC analysis + merge, no editor dependency) and `editor.tools.catalog` (`CatalogEditorPlugin`: catalogue, asset tab, drag & drop, bulk import, project integration). Tests: `StpcMergeTest`, `CrossImportTest` (need `-Dllamamod.cd`). ![All worlds](img/editor/xobj_all_worlds_tab.jpg) ## STPC layout (exact for all 33 level WADs) ``` u32 nModels, nModels × (0x8c header + data) loader FUN_0041f770, one call per model u32 nAnims, nAnims × animation set FUN_0041f8b0 [WFPC & 0x100: model block – used by no CD level] code region: u32 nUnits, nUnits × {u32 nWords, nWords × u32} ← new finding ``` The code region is a list of **units**, one per compiled strat source file (village: 53, lab 1: 65); the chain ends exactly at the chunk end in every level. A unit is self-contained: name strings, inline data, handler tables and code of one or more strats (e.g. `SinkingStatuePad` + `SinkingStatue`, `Spider` + `RopeForSpider`). The loader never parses units; MAP strat records and the bytecode address them by absolute STPC offsets. Other references to STPC: MAP zone field `stpcOffset` (always a model header: sky models) – nothing else (`DAT_006d9dbc` is used only by the zone loader, the strat record loader and opcode 0xb2). ## Strat VM (FUN_0054d180) u32 words, low 16 bits = opcode. Opcodes < 0x45: table 0x5796bc, the signed high 16 bits are the argument. Opcodes 0x45..0x1b1: table 0x578ff4, operands inline (0x1b2+ alias the short table and never occur). Operand counts were measured by **emulating all 435 handlers** of groove.exe (unicorn, fake strat struct, IP into a sentinel buffer, several operand/stack fillings) and checking the variable ones by hand (`StratOps`): | Op | Operands / meaning | |---|---| | 0x0c | a, slot: event handler slot; entry {type, param, rel} at pc+8+a·4, handler = pc+8+rel·4 (= the MAP "slots") | | 0x10/0x11 | call next+arg·4 (pushes the return address); 0x12–0x15 conditional, 0x16 jump, 0x2b jump+yield | | 0x17 | switch: table at next+arg·4+4: n, n × {case (0x44 short / 0x45 imm), rel} | | 0x02/0x03 | skip list: n, then n items (2 words for 0x45/0xea) | | 0x05/0x09 | push word / address at next+arg·4 (inline data) | | 0x45, 0xd7 | push immediate (0xd7 = **sound sample id**) | | **0xb2** | push STPC base + operand (≥ 0: absolute STPC offset of a model, animation set, unit code or data; < 0: DEFANIM.WAD animation); self-patches to 0x45 at run time | | 0xb3 | jump to popped address (return) | | 0xd4 0xea rel | strat name | | 0xe0 | spawn: pops code address, 5 inline type-field words | | 0xf0, 0xfa | 1 operand: **text index** (LGPC) | | 0x94/0x95 | 5/6 inline words; 0xa5 0xa6 0xb1 0xd0 0xd3 0xdb 0x115 0x11c 0x14b 0x14d 0x162 0x167 0x175: 1 word | | 0xb4, 0x8e, 0x8c | end/kill; with 0x16/0x2b/0xb3 the terminators | | 0xd5/0x99/0x131 | play / stop / is-playing sound (id from the stack; bit 0x80 = front-end sound) | | 0x5a, 0xde, 0x187, 0x1a9 | take a **TEXT texture index** from the stack (sprite/shadow at strat+0x128, particles, screen effect); the compiler pushes it with 0x45 right before | | 0x11a/0x11b | swap the alternative tile models of the strat's cell | **Disassembler** (`StratCode`): recursive descent from MAP entries, name sites, 0xb2 code targets, switch/handler tables; then code after terminators is decoded tentatively and kept if clean (unplaced spawn stubs). Result on all levels: no decode errors, ~92 % of the code region covered (rest: strings, inline data), and **no word 0xb2 outside a decoded instruction** – the relocation set is complete. **Level-dependent operands** were confirmed by diffing the same unit across levels (units equal modulo operands): every differing word is a 0xb2 operand, a 0xd7 sound id, a 0xf0/0xfa text index or a 0x45 texture index before a texture op – nothing else. ## Merge (`StpcMerge`, `TextMerge`, `SoundMerge`, `DialogMerge`) New STPC = `[target models][imported models][target anims][imported anims][target units][imported units]`. Models must stay in the model block (the loader turns their offsets and face texture indices into pointers), so appending is only possible at the end of each block: **target model offsets stay; target animation offsets move by the imported model bytes, target code offsets by models + anims.** Every absolute reference of the target is relocated with that piecewise map: 0xb2 operands (from the disassembly) and MAP strat codes (zones point at models and stay). Per import request (source level + MAP entry): 1. Closure: the entry's unit, then transitively every unit, model and animation set reached by its 0xb2 operands (sub-strats: shards, projectiles, effects, heads …), plus sound ids, texture indices (code constants + model faces) and text indices of the copied units. 2. Units identical to a target unit (bytes with the level-dependent operands masked) are **not copied** but mapped onto the target's unit (Coin, EnemyCoin, Sparkles …); models identical after texture remapping and identical animation sets are shared too. 3. Textures: needed entries (+ flip-book frames of TEXT animations). Pages identical to a target page are reused; mostly used pages are copied whole; otherwise the used rectangles (+1 px border) are packed into new 256×256 pages per page class. Entries identical to a target entry are reused. 4. Sounds: SMPC samples appended (identical ones reused); 0xd7 operands remapped. 5. Texts: LGPC groups (language strings + speech slot) appended, 0xf0/0xfa operands remapped; a speech slot `#hex` is an SRPC index – SRPC entries (offset/size into the global `Music/.CVS`) are copied and the slot rewritten (`#` + upper-case hex, FUN_0040d890 reads only upper case). 6. Write, relocate imported 0xb2 operands (source → new offsets), then **verify**: disassemble the merged STPC again; the set of 0xb2 sites and their values must equal the expected relocation exactly and every value must hit a model header, animation set or unit. Deterministic: same inputs → same bytes (test). All 925 (level, type) entries of the CD merge into the village and verify (test `everyPlacedTypeMergesIntoTheVillage`). ## Engine limits | Limit | Value | Source | |---|---|---| | level memory | 16 MB block for STPC, TEXT (compressed pages + 20 B/entry), TRAK, MAP, LGPC, strat pool | FUN_0041eea0 / FUN_0041ef00 (returns 0 when full) | | sound samples | 128 (id is a signed char, bit 0x80 = front end) | FUN_00548760 | | texture pages | 127 (signed byte page index) | TEXT entry | | texture entries | 65535 (u16 face field) | | | strat pool | poolExtra + 30 slots | MAP | The editor estimates level memory from the scene (`CrossImport.limits`); originals use ≤ 7 MB. The import preview and Build check report it (errors above the hard limits, warnings near them). ## Editor integration * **Project file**: `level.json` key `importedTypes`: `[{"types":[{"level":"t1l2m001","type":"Spider","code":…}]}, …]` (batches in order). No game data. `ProjectIO` load hook (`ProjectIO.addLoadHook`, registered by `CrossImport`) merges every batch into **both** the reference scene and the current scene before the stored changes are applied, so the diff still only contains the user's edits; instances of imported types are ordinary added strats. Builds (editor, Play, `--editor-build`, LlamaMod level mods) use the merged scene → deterministic. * At run time an import merges into reference + scene (strat codes, STPC/SMPC/LGPC/SRPC chunks, new texture pages/entries/anims inserted after the reference ones – user textures behind them are renumbered, catalog with remapped codes + the imported variants). Code offsets move, so the undo history is cleared and replaced by a permanent "Import …" entry. Imports cannot be removed. * **Catalogue** (`ObjectCatalog`): every placed type of every level (906 types, 33 levels, ~3 s): name, level/world/chapter, refined category (enemies, collectibles, mechanisms, characters, animals, effects …), instance count, parameters, dependency counts/size, thumbnail (software rasteriser, posed skinned models), importable + reasons/notes. Not importable: player starts, cameras and cut scenes (126); notes: texts copied, tile swaps, waypoint chains. Cached in `/editor/catalog/` (catalog.json + thumbs/), keyed by the CD's WAD sizes. * **UI**: Assets → *All worlds* (search, world/category filters, "Importable only", ✓ for imported types, tooltips with dependencies). Drag into the viewport = import (background) + place at the drop point; double-click = view centre; context menu: import only, import all of that level. *Import world…* (Level menu, Ctrl+Shift+I, tab button): all types of a world or level with checkboxes, sizes, and *Preview* (exact merge result vs. limits). Inspector: "Imported object – from …". Level check: limits. Imported types then also appear in Assets → Objects like the base's own. ![drag & drop](img/editor/xobj_drag_drop_inspector.jpg) ![import world](img/editor/xobj_import_world_preview.jpg) ## Verified in game (apptest, display :6, 2026-11-07) * **New level on the village base** (flat floor, chapter 5): Spider (Jungle at Night 1) drops on its rope when the llama comes near (`xobj_game_spider.jpg`); Grapes (City 2) render and are collected (fruit counter); CitySwingingBlade (City 2) and MerryGoRound (Mountain 1) render; Bucky (Jungle 1, dragged in the editor, with texts and sounds) runs around and throws nuts (`xobj_game_bucky.jpg`). Plus the whole of Jungle at Night via *Import world…* (28 types, +1.2 MB, 44/127 pages, 98/128 sounds): builds and loads, no crash. * **Edited original village** (t1l1m001 + Spider, 2 Grapes, JumpingBeetleBlue (jungle), CitySwingingBlade): intro dialogue still shows the village's own texts, the blade swings, beetle and grapes are there (`xobj_game_village_*.jpg`), no crash. * Sounds are verified only structurally (merged bank, remapped ids; the test sessions are muted). ## Known limits * Objects that belong to level geometry (tile swaps, doors in walls, river/roller-coaster specials) import fine but need matching geometry; waypoint chains are copied relative to the object. * Parameters are those of the source's first instance (edit them in the inspector). * A type whose code is identical to the target's (e.g. Coin) is shared, i.e. it keeps the target's model/textures. * Level-memory figure is an estimate (sum of the loaded chunks), not measured at run time. * Imports cannot be undone or removed from a project (re-create the level or edit level.json).