Files
llamamod/notes/editor-api.md
T

161 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 core – API (`editor.model`, `editor.model.cmd`, `editor.io`)
Status: phase 1 done (core, no UI): `src/main/java/com/eliasmueller/llamamod/editor/{model,model/cmd,io}`,
LlamaMod integration in `mods/levels` (`notes/mod-levels.md`). Tests: `src/test/java/.../editor/io`.
Project file format: `notes/editor-format.md`. Design brief: `notes/editor-design.md`.
## Quick start
```java
byte[] wad = Files.readAllBytes(cd.resolve("wads/t1l1m001.wad"));
Scene s = WadImporter.importWad(wad, "t1l1m001", rel -> read(cd.resolve(rel))); // original level
History h = new History(s); // undo/redo
s.addListener(e -> viewport.repaint()); // change events
h.execute(Edits.translate(List.of(strat), 1.0, 0, 0, null)); // move 1 cell east
h.undo();
byte[] out = WadBuilder.build(s); // Scene → WAD (byte-identical if unedited)
Bases bases = Bases.fromCd(cd); // original CD folder
Project p = ProjectIO.load(path, bases); // .llamalevel (p.scene(), p.warnings())
ProjectIO.save(p, path, bases); // stores only the changes
Project n = Project.create(bases, "new", "t1l1m001"); // empty level on the village base
Project e = Project.create(bases, "edit", "t1l3m001"); // edit an original level
```
## Coordinate spaces (important for rendering)
| Space | Used by | Axes |
|---|---|---|
| **export space** | `TileModel` vertices (local), `Tile.x/y/z` (cells, z ≤ 0), `Light` x/y/z | right-handed, y up; world vertex = `tile.transform().point(vx, vy, vz)` |
| **game space**, 20.12 fixed point (4096 = 1 cell) | `Strat`, `PathNode`, `EnvZone` target, collision records | x east, y up, z = grid row (positive) = −export z |
| **game space, cells (double)** | `Brush` parameters, `MeshData` | same, unscaled |
Angles: ints in 1/4096 turns (`PropType.ANGLE`). Strat yaw 0 = facing +z (game), 1024 = facing +x.
Tile transform (export space): `X = x + vx·cos a − vz·sin a; Y = y + vy; Z = z + vx·sin a + vz·cos a`,
a = rot/4096·2π (`format.Geometry.TileXform`). Grid cell of a tile = (floor(x), floor(−z)).
Collision records (`TileModel.collision0..2`, 32 bytes) are stored in game space; decode with
`TileModel.collisionPolygon(g, k)` (→ `format.Collision.Decoded`, already converted to export
space) and `Collision.polygon(decoded)` for the corners (local to the tile; apply the tile transform).
Placed in the world: `tile.worldCollisionPolygons()` / `model.collisionPolygons(xf)` (corners, normal,
material; polygons with fewer than 3 corners skipped).
## Scene (`editor.model.Scene`)
Singletons: `settings()` (`LevelSettings`), `regions()` (`Regions`). Ordered lists (`NodeList<T>`,
read-only `List` with O(1) `indexOf`; order = WAD order):
| Accessor | Node | WAD source |
|---|---|---|
| `tileModels()` | `TileModel` | TRAK models (vertices, faces, planes, 3 collision groups, bbox header) |
| `tiles()` | `Tile` | MAP tiles: x, y, z, rot, `model` (ref), swap (alt model, door/bridge), vertex colours |
| `strats()` | `Strat` | MAP objects: `type` (name), `code`, position/rotation (fixed), `params`, type fields, `waypoint` (ref), flags |
| `pathNodes()` | `PathNode` | MAP waypoints/camera/respawn nodes; `next` (ref) chains |
| `zones()` | `EnvZone` | MAP 90-byte env/teleport/respawn zones incl. fog, colours, music |
| `lights()` | `Light` | LGHT (directional / point) |
| `pages()` | `TexturePage` | TEXT pages (256×256 ABGR1555, `argb()` for images) |
| `textures()` | `TextureEntry` | TEXT texture table (faces use the **index** in this list) |
| `textureAnims()` | `TextureAnim` | TEXT texture animations |
| `brushes()` | `Brush` | editor-only primitives → tiles at build time |
| `meshAssets()` | `MeshAsset` | imported OBJ/glTF (`mesh()` = parsed `MeshData`) |
| `chunks()` | `RawChunk` | WAD chunk order; unmanaged chunks (STPC, sound, …) verbatim |
Other: `node(id)`, `listOf(kind)`, `catalog()` (`StratCatalog`: placeable object types of the base
level, variants with type fields, default params and relative waypoint chains), `assets()`
(project files), `chain(pathNode)`, `playerStart()`, `referencesTo(node)`, `create(kind)` /
`createBrush(kind)` (detached node with a fresh id), `add/remove/set` (with events, no undo).
**Stable ids**: imported nodes `"<kind>:<index in the WAD>"` (`tile:12`, `strat:3`, `wp:40`, `zone:0`,
`model:7`, `tex:120`, `page:2`, `light:0`, `anim:1`, `chunk:5`), new nodes `"<kind>:n<N>"`, singletons
`settings:0`, `regions:0`. Ids never change, also not when other nodes are deleted.
## Nodes and properties
Every node: `id()`, `kind()` (`NodeKind`), `label()`, `props()` → `List<Prop>`. A `Prop` has
`name()`, `label()`, `group()` (inspector section), `type()` (`PropType`), `readOnly()`,
`nullable()`, `refKind()`, `choices()`, `get(node)`. Value classes by `PropType`:
INT/FIXED/ANGLE/FLAGS/COLOR → `Integer`, DOUBLE → `Double`, BOOL, STRING/CHOICE → `String`,
REF → `Node`, INT_ARRAY `int[]`, DOUBLE_ARRAY `double[]`, BYTES `byte[]`. Arrays are shared: never
mutate a returned array, set a new one. Typed getters exist on every node class (`tile.x()`,
`strat.gx()` (cells), `model.vertices()` …).
Key render data:
* `TileModel.vertices()` 6 doubles/vertex (x y z nx ny nz), `faces()` 6 ints/face (flags, v0, v1,
v2, tex, pad), `planes()` 4/face; helpers `vertexCount()`, `faceCount()`, `faceFlags(f)`,
`faceVertex(f,k)`, `faceTexture(f)`, `collisionCount(g)`, `collisionMaterial(g,k)`.
* UVs: `format.Geometry.faceUvs(flags, Geometry.texUv(entry.toFormat(), page.width(), page.height()))`;
flat colours: `TextureEntry.isFlat()`, `flatRgb()`. Face flags: 0x400 double-sided, 0x4000
ignore vertex colours, 0x8 batch start.
* `Tile.colors()`: frames × nv RGBA words (0x80 = 1.0, engine ×2), first frame = `colors[0..nv)`.
* `Regions.visibleFrom(camCellX, camCellZ, cellX, cellZ, width)` = engine visibility rule.
* Collision materials: 0 normal, 16 exit floor (LevelDoor), 17/18 selective, 30 no floor/death.
* `StratCatalog.category(type)` → player, exit, checkpoint, camera, trigger, collectible, force, other.
## Commands (`editor.model.cmd`)
`History(scene)`: `execute(cmd)`, `undo()`, `redo()`, `canUndo/Redo`, `undoLabel/redoLabel`,
`begin(label)`/`end()` (group into one step), `markSaved()`/`isDirty()`, `addListener(Runnable)`.
Commands: `SetProp.of(node, "prop", value)` / `SetProp.of(node, prop, value, mergeKey)`,
`AddNode(node[, index])`, `RemoveNode(scene, node)` (clears optional references, refuses if a
tile still uses a model), `Batch(label, cmds[, mergeKey])`. **Drags**: give every step of one
gesture the same `mergeKey` object → consecutive steps merge into one undo entry.
`Edits` helpers: `translate(nodes, dx, dy, dz, mergeKey)` (cells, game space; strats, waypoints,
tiles, brushes, zone targets, point lights), `rotateTo(node, degrees, mergeKey)`,
`placeStrat(scene, type, variant, x, y, z, yawDeg)` (copies type fields/params/waypoint chain from
the catalog), `delete(scene, nodes)` (also removes waypoint chains used only by deleted strats),
`addBrush(scene, kind, params)`, `set(label, Map<Node, Map<prop, value>>, mergeKey)`.
Events (`SceneListener`, `SceneEvent`): `ADDED(node, index)`, `REMOVED(node, index)`,
`CHANGED(node, prop, oldValue)`, `RELOADED` (bulk change, rebuild everything). Fired synchronously
on the changing thread (use the EDT).
## Brushes (`Brush`, `BrushKind`)
Kinds FLOOR, BOX, PLATFORM, PILLAR, RAMP, STAIRS, FENCE, TREE, SEA, KILL_PLANE, QUAD, COLLIDER, MESH
– the `levelbuild.LevelBuilder` primitives; parameters = props named as in `BrushKind.params()`
(`Param(name, label, type, default, role, choices)`; role X/Y/Z/XZ_LIST/XYZ_LIST/XZ_RECT marks
coordinates). `brush.bounds()` gives an approximate box. Texture parameters: village names
(`LevelBuilder.TEX`, resolved per base by `BaseTextures`), `"#index"`, flat colour names (`LevelBuilder.FLAT`) or `"rgbRRGGBB"`.
For a preview of the generated geometry: `BrushCompiler.compile(scene, warnings)` → generated
`TileModel`s/`Tile`s/textures (same as the build).
## IO (`editor.io`)
| Class | Purpose |
|---|---|
| `WadImporter.importWad(bytes, base, cd)` | original WAD → Scene (cd: loader for other CD files, may be null) |
| `WadBuilder.build(scene)` / `buildWithReport(scene)` | Scene → WAD (+ `Stats`, warnings); validates first, throws `IllegalStateException` with all errors |
| `Validator.validate(scene, warnings)` | errors (unbuildable) + warnings, for a problems panel |
| `BrushCompiler.compile(scene, warnings)` | brushes/meshes → generated `TileModel`s/`Tile`s/pages/entries (ids "gen:…", not in the scene) – usable as a preview of the build |
| `Project` | `scene()`, `thumbnail()`/`setThumbnail(png)`, `warnings()`; `Project.create(bases, "edit"/"new", base)` |
| `ProjectIO` | `load/read(bytes)`, `save(p, path, bases)`/`write`, `info(bytes)` (header only), `sha256` |
| `Templates` | `original(bases, base)`, `newLevel(bases, base)` (reference), `clearBaseLeftovers(scene)`, `DEFAULT_BASE` |
| `Bases` | original files: `Bases.fromCd(dir)` (case-insensitive), `wad(base)`, `file(rel)` |
| `MeshImport.importFile(scene, path)` | OBJ(+MTL, map_Kd), glTF (.gltf + .bin/data URIs, .glb) → detached `MeshAsset` (add with `AddNode`); files copied into `scene.assets()`; `load(scene, asset)` re-parses |
| `TextureImport.importImages(scene, images, size)` | images → `Result(command, entries, pages)`: execute the command, then use `"#" + entries[i]` as a texture |
| `Images` | page ↔ PNG/`BufferedImage` (`pageImage`, `pagePng`, `toAbgr1555`) |
Meshes at build time: transformed (scale, yaw, position), then every triangle goes through
`LevelBuilder.quad` (split into 1×1 cells / 10-unit height bands, collision polygon per triangle
with the brush's material, lit vertex colours). The engine has no free UVs – a triangle always
shows one half of a texture rectangle – so each triangle gets the rectangle around its UVs
(rounded to `uvSteps` per texture) on a copy of its material texture (flipped in U and/or V when
that fits the corner order better). Rectangular quads map exactly; arbitrary UV layouts are
approximated; tiling UVs (> 1) are clamped. Material textures are packed into new 256×256 pages
at `texSize` (power of two). Untextured materials use flat colours (Kd / baseColorFactor);
vertex colours tint the lighting.
## Limits / unknown (phase 1)
* Strat code (STPC) and object models are kept verbatim: only types present in the base level are
placeable (`StratCatalog`), plus RubyIdol borrowed from t1l1m002 for the village (as levelbuild).
* Meaning of many raw fields is unknown (`unk*` props of zones/waypoints, texture entry flags,
collision groups B/C, region value) – they are editable but not interpreted.
* Tile swaps (WFPC 0x10000) can only exist in bases with that flag.
* Texture names are the village's levelbuild names; on other bases they resolve to the same picture
(`levelbuild.BaseTextures`, pixel fingerprint); names the base lacks are not offered (GeoWidgets) and a
brush using one is skipped with a warning (the editor has no CD access to copy them over).
* Lights: types other than 1/2/4 carry no data and are ignored by the engine.
* Removing original tiles shifts tile indices used by strat scripts (see editor-format.md).
* The engine limits: ≤ 8000 vertices per tile model, collision within ±8 of the tile origin
(handled by the brush compiler), texture pages ≤ 127.