154 lines
11 KiB
Markdown
154 lines
11 KiB
Markdown
# 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`).
|
||
|
||

|
||
|
||
## 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/<lang>.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
|
||
`<data>/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.
|
||
|
||

|
||

|
||
|
||
## 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).
|