Files
llamamod/notes/editor-cross-import.md
T

154 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<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.
![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).