Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
ac360bf
feat: preview a material on the context mesh slot in the Material Editor
raighen Oct 1, 2026
cbb2140
feat: turn a material made in Blender into a game material on push
raighen Oct 1, 2026
6ae4d23
fix: stop the Material Editor throwing while its XAML loads
raighen Oct 1, 2026
1a67f78
fix: live-test the Blender material push and close what it found
raighen Oct 1, 2026
d563d98
fix: store large textures the way the game does, and stop Build losin…
raighen Oct 2, 2026
e214dbb
fix: give a created material the two fields the stock ones carry
raighen Oct 2, 2026
6dc713b
fix: rename a probe local that clashed with one further down
raighen Oct 2, 2026
326d7e1
feat: carry a Blender material's normal and specular maps into the game
raighen Oct 2, 2026
ecf8219
style: dotnet format the preview viewport's mesh builder
raighen Oct 2, 2026
32fde0e
feat(mcp): tools that drive the running map editor
raighen Oct 2, 2026
719bdfc
feat(mcp): blender_push, and scene_select with no names clears the se…
raighen Oct 2, 2026
aded881
fix(bridge): refuse a pushed mesh over 65535 vertices instead of writ…
raighen Oct 2, 2026
8ef1cad
feat: bring a light into a district that has none — actor import acro…
raighen Oct 2, 2026
553670a
Keep a light's inverse matrix and clip box in step with where it stands
raighen Oct 2, 2026
c6c56f8
Show and edit a mesh's draw distance in the property panel
raighen Oct 2, 2026
e69f1ad
fix: touch a light's row only when the light has moved
raighen Oct 2, 2026
382eccc
docs: the editor tools, the Blender material push and their limits
raighen Oct 2, 2026
306aa3d
feat: carry a Blender material's alpha into the game - cut-out and tr…
raighen Oct 2, 2026
feb66ba
feat(viewport): draw alpha-blended materials blended
raighen Oct 2, 2026
86637d7
feat: mirror a district's edits into its winter archive
raighen Oct 2, 2026
d159df1
feat: carry objects from one archive into another
raighen Oct 2, 2026
1f98071
feat: a Props tab to drag stock objects into a district
raighen Oct 2, 2026
4282d4b
feat: simplified hulls for imported scenery, and a props column
raighen Oct 2, 2026
9229e38
feat: an imported object's collision moves and is deleted with it
raighen Oct 2, 2026
745476e
feat: a duplicate of an imported object gets copies of its collision
raighen Oct 3, 2026
ea85311
feat: save drops geometry nothing draws; a re-import shares geometry
raighen Oct 3, 2026
fac7b32
feat(mcp): drive the resource editor; read and set car tuning
raighen Oct 3, 2026
b65132d
feat(cars): clone a car under a new name; a Build no longer drops XML
raighen Oct 3, 2026
5c637e1
feat(mcp): archive_build packs one archive's working copy
raighen Oct 3, 2026
549a223
fix: a rebuilt name table keeps the order it was loaded in
raighen Oct 3, 2026
04c0443
feat(cars): a clone gets buffers and a title of its own
raighen Oct 3, 2026
e99a0d2
Packer keeps shipped memory requirements; export a car as an M2O reso…
raighen Oct 3, 2026
ed6863f
feat(cars): build a car under another car's name
raighen Oct 3, 2026
34bf765
feat(cars): the M2O car export follows the map export's layout; docs
raighen Oct 3, 2026
8cdb1b5
fix(viewport): a mesh without normals is lit instead of drawn black
raighen Oct 3, 2026
1e012f5
fix: an imported object brings textures its own archive does not hold
raighen Oct 3, 2026
a4d1eaa
fix(viewport): crash props are drawn with their textures, not black
raighen Oct 3, 2026
c6fc323
fix: an imported object brings textures its own archive does not hold
raighen Oct 3, 2026
141f47e
feat: hide triangles of a stock mesh without rebuilding it
raighen Oct 3, 2026
a7f2a4e
feat(mcp): scene_find finds a collision placement by its hull
raighen Oct 3, 2026
4771cbb
perf(bridge): read a level's index width without decoding the mesh
raighen Oct 5, 2026
f12155b
fix(mcp): editor tools say no when the editor does, and find crash co…
raighen Oct 5, 2026
bc9d5b1
Merge branch 'pr/editor-tools' into pr/alpha-winter
raighen Oct 5, 2026
3696630
fix(mirror): the winter mirror decides against the pair as shipped, n…
raighen Oct 5, 2026
4b7e474
Merge branch 'pr/blender-material-push' into pr/editor-tools
raighen Oct 5, 2026
838c69a
fix(bridge): a material push that can be undone, refused cleanly and …
raighen Oct 5, 2026
f1c6508
Merge branch 'pr/editor-tools' into pr/alpha-winter
raighen Oct 5, 2026
a3b5f73
fix: the material preview blends what the scene blends; an alpha mask…
raighen Oct 5, 2026
f9ee859
Merge branch 'pr/alpha-winter' into pr/object-transplant
raighen Oct 5, 2026
79cd70f
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
b955fa6
test: the scripted editor answers the resource status the tools now a…
raighen Oct 5, 2026
33a95c5
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
c4b33d4
fix(cars): a clone or a substitution that fails takes itself back; re…
raighen Oct 5, 2026
888c83e
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
56f5f8d
fix(import): a failed import leaves nothing behind; redo gets its sha…
raighen Oct 5, 2026
f36a23f
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
d9a8055
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
e958337
test(import): the carried-again check asks for the textures, not for …
raighen Oct 5, 2026
d8ff05b
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
f1dbae0
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
45236da
fix(import): the lesser findings of the self-review - carry, links, n…
raighen Oct 5, 2026
64b4e9b
fix(cars): the lesser findings of the self-review - target, notices, …
raighen Oct 5, 2026
b447370
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
91722f3
fix(import): the texture index keeps up with the mirror
raighen Oct 5, 2026
1e49f44
fix(import): review of #6 - links by place, blocks shared only when a…
raighen Oct 5, 2026
9dfe25f
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
1fc6c8c
fix(cars): review of #7 - the scratch clone takes itself back, manife…
raighen Oct 5, 2026
82db24d
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
da89b7d
fix(import): review of #9 - the carry tries every copy the index knows
raighen Oct 5, 2026
d91030d
fix(props): the picture cache of an earlier build is left alone
raighen Oct 5, 2026
b75e1aa
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
da11e6e
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
8debb06
Merge branch 'pr/blender-material-push' into pr/editor-tools
raighen Oct 5, 2026
4034b0f
fix(bridge): what a second look at 838c69a found
raighen Oct 5, 2026
0ceb4ed
Merge branch 'pr/editor-tools' into pr/alpha-winter
raighen Oct 5, 2026
01973ba
fix(mcp): a layer is not switched off over unsaved edits; moves and i…
raighen Oct 5, 2026
7750e77
fix(mirror): a second look at 3696630 and a3b5f73
raighen Oct 5, 2026
1787070
Merge branch 'pr/alpha-winter' into pr/object-transplant
raighen Oct 5, 2026
d880b76
fix(import): a third pass over the collision links, the sweep and the…
raighen Oct 5, 2026
dc0844f
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
c236fc5
fix(cars): what a failed pack leaves, and a second look at the checks…
raighen Oct 5, 2026
7e2b3d7
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
902583d
fix(bridge): the pre-pass refuses a material it cannot make; no empty…
raighen Oct 5, 2026
dca8b0a
Merge branch 'pr/blender-material-push' into pr/editor-tools
raighen Oct 5, 2026
e4a17f8
fix(mcp): the layer guard is about the crash layer only
raighen Oct 5, 2026
6478045
Merge branch 'pr/editor-tools' into pr/alpha-winter
raighen Oct 5, 2026
f11a72d
Merge branch 'pr/alpha-winter' into pr/object-transplant
raighen Oct 5, 2026
b2f3b79
fix(mirror): wearers that disagree, names that differ in case, an ent…
raighen Oct 5, 2026
c5ec22a
fix(import): the collision links once more - typed and pushed moves o…
raighen Oct 5, 2026
54e6c77
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
4006700
fix(cars): the export's check of an existing list asks each car for i…
raighen Oct 5, 2026
704e784
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
1dec8c7
Merge branch 'pr/alpha-winter' into pr/object-transplant
raighen Oct 5, 2026
12d8193
fix(mirror): the same bytes are not written again for an entry that c…
raighen Oct 5, 2026
c576dfe
fix(import): a rename moves the collision record by whose it is, not …
raighen Oct 5, 2026
c323dd5
Merge branch 'pr/object-transplant' into pr/cars
raighen Oct 5, 2026
d5d8b24
Merge branch 'pr/cars' into pr/import-textures
raighen Oct 5, 2026
b0a8984
feat(bridge): a skinned mesh can gain geometry; two levels of one fra…
raighen Oct 6, 2026
23fba4d
fix(bridge): a person's mesh is not turned twice in Blender
raighen Oct 6, 2026
8026257
feat(browser): pictures of archives in the content browser
raighen Oct 6, 2026
c872409
feat(mcp): collision_unused_hulls and crash_placements
raighen Oct 6, 2026
8b1536f
Merge branch 'feat/object-transplant' into release/raighen
raighen Oct 6, 2026
49ef29e
docs: the README says what this release line is and names what it adds
raighen Oct 6, 2026
4a31c23
feat(mcp): load zones (zones_at, zones_map, zone_move_face) and mesh_…
raighen Oct 7, 2026
1b2d232
feat(ui): a Tools menu over the jobs only the MCP server could do
raighen Oct 7, 2026
fb36350
feat(ui): the Build window lists what goes into the game
raighen Oct 7, 2026
a6b57ad
docs: the README covers the Tools menu, the load-zone tools and the B…
raighen Oct 7, 2026
ca0a785
feat(viewport): loading zones are picked by a click and edited with t…
raighen Oct 7, 2026
1d92f66
fix(build): a deleted resource is listed, and a failed comparison end…
raighen Oct 7, 2026
0fea705
fix: hide triangles, slot re-pointing and the car tools after a review
raighen Oct 7, 2026
07b777e
docs: the README covers zone editing in the viewport
raighen Oct 7, 2026
10df3f5
docs: the README of the proposed branch drops the fork's own note
raighen Oct 7, 2026
8f4c914
merge: main, after #3 and #8 went in as single commits
raighen Oct 7, 2026
a44c40c
merge: main, after #3 and #8 went in as single commits
raighen Oct 7, 2026
73b49f8
merge: main, after #3 and #8 went in as single commits
raighen Oct 7, 2026
72f55ba
merge: main, after #3 and #8 went in as single commits
raighen Oct 7, 2026
312f620
merge: the stack below, now on main
raighen Oct 7, 2026
027de7d
merge: main, after #6 went in as a single commit
raighen Oct 7, 2026
c64319d
merge: main, after #6 went in as a single commit
raighen Oct 7, 2026
04fcdae
merge: main, after #6 went in as a single commit
raighen Oct 7, 2026
001e550
merge: main, after #7 and #9 went in as single commits
raighen Oct 7, 2026
f1a4b39
fix: zone writes, the Build list and slot re-pointing after a second …
raighen Oct 7, 2026
ebfe45e
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 7, 2026
d0234e1
test: the hide-triangles probe names the frames that share a material…
raighen Oct 7, 2026
01aad5f
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 7, 2026
61bc4a1
fix(viewport): a resize while a district loads no longer ends the pro…
raighen Oct 8, 2026
ed1d964
feat(zones): a new loading zone can be made, and taken out again
raighen Oct 8, 2026
f737a2f
feat(viewport): a button that makes a loading zone, and a zone layer …
raighen Oct 8, 2026
9f40060
docs: the README covers making a loading zone and the new look of the…
raighen Oct 8, 2026
0c99606
fix(zones): what decides loading is the zone's name; review of zone c…
raighen Oct 8, 2026
276a81c
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 8, 2026
77389d7
Merge remote-tracking branch 'origin/main' into pr/tools-and-zones
raighen Oct 8, 2026
d8bbcf9
feat(tools): Hide triangles picks them by a click, and cuts a mesh th…
raighen Oct 8, 2026
8318111
feat(zones): added zones are marked, zone_create is an undo step, zon…
raighen Oct 8, 2026
40b5d7e
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 8, 2026
d5b5dab
fix(tools): a click that misses the mesh says why; tidy the probe runner
raighen Oct 8, 2026
21ba90d
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 8, 2026
01f0594
fix(tools): hiding triangles on shared and instanced meshes, after a …
raighen Oct 8, 2026
5586563
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 8, 2026
3ce3f83
style: the coarser-level test of FindPicked as a helper of its own
raighen Oct 8, 2026
984f2f0
Merge branch 'release/raighen' into pr/tools-and-zones
raighen Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ The last path is remembered. A green download arrow appears beside the gear when
out; the same progress bar shows the download, and the toolkit restarts to install it.
*(A "Resource Editor" tile exists but is a disabled stub.)*

The library browser shows a picture of each character, wardrobe piece and car, drawn from its
archive: people head and shoulders, a car on its wheels. Pictures are kept on disk and only the
tiles on screen are read.

### Viewport and streaming

- Area catalog of open-world districts and interiors, from `cityareas.bin`.
Expand Down Expand Up @@ -200,6 +204,9 @@ with it: *Blended* becomes a translucent surface (glass), anything else a cut-ou
grille). The material is written to the library on Save and its textures are packed into the
archive on Build.

A skinned mesh - a car body - can gain vertices: its remap pools are rebuilt from the pushed skin.
Two levels of detail of one object can go in one push. A person opens standing on their rig.

Limits worth knowing: untouched geometry round-trips bit-exactly (that is how a real reshape is
told apart from an untouched one); a topology rebuild leaves lower LODs and collision with the old
shape; collision placements refuse scale and mirror pushes (resize the hull with the toolkit's own
Expand All @@ -209,9 +216,15 @@ split it into several objects.

### Saving, building, backups

**Ctrl+S** writes edited FrameResources back to the extracted mirror. **Build** repacks every
edited archive into its `.sds`, creating a timestamped versioned backup first; archives are packed
independently, so one failure (the game holding a file open, say) does not block the rest.
**Ctrl+S** writes edited FrameResources back to the extracted mirror. **Build** opens a window
first: every archive edited in the session, ticked, and under each the files of its working copy that
differ from the archive the game has now. A Build packs a whole working copy, so that list is
everything that is about to change in the game - including a file changed in that folder in an
earlier session and forgotten, which is marked and counted out loud. Untick an archive to leave it
for a later Build; **Add archive…** packs one whose working copy was changed by hand (a script, a
table). Each ticked archive is then repacked into its `.sds` with a timestamped versioned backup
first; archives are packed independently, so one failure (the game holding a file open, say) does
not block the rest.
**Restore Backup** rolls a single archive back to an earlier version - replacing both the live
`.sds` and its extracted mirror - from the File menu, the tree's context menu or the viewport's.
*(Material-library edits are not covered by the backup flow.)*
Expand All @@ -233,14 +246,58 @@ beside the working copy in `illusion_memory.json`, and stated again on every Bui
resource changed size. A repacked stock car states exactly what the original did
(`--probe-car-clone`).

### Tools menu

What the MCP server can do to a scene, the menus can too: each item under **Tools** opens a window
over the same job the matching tool runs, so a result does not depend on who asked for it.

- **Hide triangles…** (map and resource editor) - cuts an opening in the selected mesh without
rebuilding it: a doorway in a stock wall, a pane of glass. With the window open the triangles are
picked by clicking them on the mesh in the viewport (a click on a marked one takes it back), or -
for many at once - by a box that holds them. What would be hidden is marked on the mesh, the count
says what goes on each level of detail, and Hide is one undo step.
- **Mirror to winter…** (map editor) - carries the loaded district's edits into its winter archive.
The item is greyed out, and says why, when the district has no winter variant or the winter one is
what is loaded.
- **Loading zones…** (map editor) - lists the zones of `city_univers` that hold a point (the
camera's, or one typed in), says which districts a player who appears there gets, and moves one
face of a zone. A district streams in where a zone named with two words after its number
(`AREA341_GREENFIELD_KINGSTONE`) holds the player; one named with a single word
(`AREA0019_GREENFIELD`) does not load it by itself. A moved face is written at once, redrawn in the Loading
zones layer, queued for Build, and is one step of the editor's Undo.
The zones are also edited in the viewport, with no window: with the **Loading zones** layer on, a
click picks the zone that spot is in (the same spot again takes the next one there) and the tool
shelf works on it - **Move** puts three arrows at the zone's centre and moves it whole, **Scale**
puts an arrow at the centre of each face and pulls that face alone. A side sliced off by a slanted
plane has no arrow. Letting go writes `city_univers` and queues it for Build; the right button or
Esc drops a drag, and Undo / Redo take back exactly what the drag changed. With **Whole map** on the
editor holds `city_univers` itself; a zone write is carried into that scene too, so the editor's own
save does not put the zone back.
The layer draws each zone as the edges of its box with a faint fill, hidden by the scene in front of
it and fading with distance (not in a parallel view); a zone with one word in its name - which does
not load its district by itself - is drawn fainter, the picked zone carries its name and
districts over it, and a zone that was added to the game's own has a second, near-white frame
round it (and "added" in its label and in the Loading zones list).
**New loading zone** (the button at the foot of the tool shelf, with the layer on) makes a zone
where the view looks: a name and the one or two districts it keeps loaded are asked, the box appears
picked, and Move and Scale put it in place. The name wants two words after its number
(`AREA900_DOCK_SOUTH`): that is what makes the game load the districts for a player who appears
inside, and the flyout says so when a name has one. It is written at once - the scene, its name
table and `cityareas.bin` - and Undo takes it out again. Not with Whole map on.
- **Stream map…** - find and replace across the text of a `StreamMapa.bin` (archive paths, instance,
line and group names), with every string it would change listed before anything is written and a
backup kept beside the file.
- **Clone car…**, **Replace a car…**, **Export car for multiplayer…** (resource editor) - the three
car jobs described under the MCP server below, starting from the car on the stage.

### MCP server

An MCP endpoint runs for the lifetime of the application at `http://127.0.0.1:2010/mcp` - loopback
only, no authorization - with its live status in the launcher's status bar. Point a client at it
with `claude mcp add --transport http illusion http://127.0.0.1:2010/mcp`; change the port with
`McpPort` in settings.

It serves 76 tools. The file tools all read through the same format layer the editor uses, so what
It serves 86 tools. The file tools all read through the same format layer the editor uses, so what
a model is told about a file is what the toolkit itself sees; the editor tools drive the running
map editor itself.

Expand All @@ -254,11 +311,12 @@ map editor itself.
| **Tables** | `list_tables`, `dump_rows`, `lookup_by_row` |
| **Stream map** | `parse_stream_map`, `edit_stream_map` |
| **Effects** | `parse_effects_file`, `parse_effects_from_bytes` |
| **Utility** | `hash_fnv32`, `hash_fnv64`, `hash_batch`, `convert_number`, `detect_file_format`, `detect_format_from_bytes`, `list_game_files`, `get_configured_games` |
| **Utility** | `hash_fnv32`, `hash_fnv64`, `hash_batch`, `convert_number`, `detect_file_format`, `detect_format_from_bytes`, `list_game_files`, `get_configured_games`, `ping` |
| **Editor** | `editor_status`, `editor_list_areas`, `editor_open_area`, `editor_save`, `editor_build`, `editor_mirror_winter`, `editor_undo`, `editor_redo`, `editor_notices` |
| **Scene** | `scene_find`, `scene_select`, `scene_delete_selected`, `scene_duplicate_selected`, `object_move`, `object_properties`, `object_set_property`, `actor_import`, `object_import` |
| **Scene** | `scene_find`, `scene_select`, `scene_delete_selected`, `scene_duplicate_selected`, `object_move`, `object_properties`, `object_set_property`, `actor_import`, `object_import`, `mesh_hide_triangles`, `mesh_materials`, `collision_unused_hulls`, `crash_placements` |
| **Resource editor** | `editor_target`, `resource_list`, `resource_open`, `resource_status`, `car_tuning`, `car_tuning_set` |
| **Cars** | `car_clone`, `car_substitute`, `car_export_m2o`, `archive_build` |
| **Loading zones** | `zones_at`, `zones_map`, `zone_move_face`, `zone_create`, `zone_delete` |
| **Blender session** | `blender_open`, `blender_push`, `blender_end` |
| **Viewport** | `camera_get`, `camera_set`, `camera_look_at`, `camera_frame_selection`, `view_set`, `viewport_screenshot` |

Expand All @@ -271,6 +329,27 @@ and nothing reaches the game before `editor_build` (which keeps the usual timest
a car, opened with `resource_open`); `car_tuning` and `car_tuning_set` read and edit that car's
entity-data tables the way the Tuning tab does.

`mesh_hide_triangles` cuts an opening into a stock mesh without rebuilding it: the triangles inside
a world-space box are hidden on every level of detail and no vertex is touched, so a stock facade
keeps the channels Blender never sees. It reports by default and hides with `apply`.
`mesh_materials` lists a mesh's material slots and re-points one at another material - no geometry
and no UV is touched. `collision_unused_hulls` counts, and with `apply` removes, the hulls no placement references.
`crash_placements` lists the crash-layer props (trees, lamps, bins) standing in a box and can
delete them, the twin season included.

`zones_at` lists the load zones of `city_univers` that hold a point and the districts they name,
`zones_map` draws where a district is asked for as a text plan, and `zone_move_face` moves one face
of a zone (its plane and its box together; reports by default, writes with `apply`). Measured in
the game: a district streams in where a zone named with two words after its number holds the player
- one named with a single word does not load it by itself - and free ride reads the base game's
`city_univers`, not the copy a DLC ships.
`zone_create` adds a new zone: a box between two corners, made like an existing zone, with a line in
`cityareas.bin` for the districts it keeps loaded. Measured in the game, with new zones over one
spot: a player who appears inside a zone named `AREA901_FOO_BAR` gets the district - with one
district in the table or two - and inside one named `AREA902_FOOXBAR` he does not. With the map
editor open the new zone is a step of its history; `zone_delete` takes an added zone out again (a
zone the game ships with is refused). Zones are made in the base game's `city_univers` only.

**Cars.** `car_clone` makes a new car out of an existing one for single player: a copy of its
archive (and the winter `_z` twin) with the root frame, name table, prefab entry, entity data and
geometry buffers filed under the new model name, registered in the vehicle, paint, cover-point and
Expand Down
23 changes: 23 additions & 0 deletions src/Illusion.Assets/Adapters/FrameNodeAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,29 @@ public bool SetSlotMaterial(int slotIndex, ulong hash)
return true;
}

public IReadOnlyList<ulong[]> GetSlotTable()
{
if (Lod0Structs() == null || _frame is not FrameObjectSingleMesh mesh) return [];
return [.. mesh.Material.Materials.Select(level => (level ?? []).Select(slot => slot.MaterialHash).ToArray())];
}

public bool SetSlotTable(IReadOnlyList<ulong[]> table)
{
ArgumentNullException.ThrowIfNull(table);
if (Lod0Structs() == null || _frame is not FrameObjectSingleMesh mesh) return false;
List<MaterialStruct[]> levels = mesh.Material.Materials;
if (levels.Count != table.Count) return false;
for (int lod = 0; lod < levels.Count; lod++)
{
if ((levels[lod]?.Length ?? 0) != table[lod].Length) return false;
}
for (int lod = 0; lod < levels.Count; lod++)
{
for (int slot = 0; slot < table[lod].Length; slot++) levels[lod][slot].MaterialHash = table[lod][slot];
}
return true;
}

// The LOD0 material table, with the same construct-as-side-effect guard GetMaterials uses.
private MaterialStruct[]? Lod0Structs()
{
Expand Down
42 changes: 42 additions & 0 deletions src/Illusion.Assets/Adapters/SceneDocumentAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,48 @@ public IReadOnlyList<FrameObjectSingleMesh> GeometrySharers(FrameObjectSingleMes
return sharers;
}

/// <summary>
/// The other frame objects that draw from one of the given index buffers through a geometry block of their
/// OWN - which <see cref="GeometrySharers"/> does not see. No shipped district has one (measured: 0 of 1310
/// buffers in uppertown); an object imported twice with a level's draw distance changed in between does.
/// </summary>
public IReadOnlyList<FrameObjectSingleMesh> IndexBufferSharers(FrameObjectSingleMesh mesh, IReadOnlySet<ulong> indexBuffers)
{
ArgumentNullException.ThrowIfNull(mesh);
ArgumentNullException.ThrowIfNull(indexBuffers);
var sharers = new List<FrameObjectSingleMesh>();
foreach (object? value in _frame.FrameObjects.Values)
{
if (value is not FrameObjectSingleMesh other || ReferenceEquals(other, mesh)
|| other.Geometry is not { LOD: { } levels } || ReferenceEquals(other.Geometry, mesh.Geometry))
{
continue;
}
for (int lod = 0; lod < levels.Length; lod++)
{
if (other.GetIndexBuffer(lod) is { } buffer && indexBuffers.Contains(buffer.Hash))
{
sharers.Add(other);
break;
}
}
}
return sharers;
}

/// <summary>The other frame objects that draw with the same material block: re-pointing one of its slots
/// re-points it for all of them. Rare in the shipped districts (8 of 3099 mesh frames in uppertown).</summary>
public IReadOnlyList<FrameObjectSingleMesh> MaterialSharers(FrameObjectSingleMesh mesh)
{
ArgumentNullException.ThrowIfNull(mesh);
// Asked through the reference table first: the Material property makes an empty block for a mesh that
// has none, and a question must not leave every such mesh of the scene with one.
if (!mesh.Refs.ContainsKey(FrameEntryRefTypes.Material) || mesh.Material is not { } material) return [];
return [.. _frame.FrameObjects.Values.OfType<FrameObjectSingleMesh>()
.Where(other => !ReferenceEquals(other, mesh) && other.Refs.ContainsKey(FrameEntryRefTypes.Material)
&& ReferenceEquals(other.Material, material))];
}

/// <summary>The canonical <see cref="IFrameNode"/> adapter for a frame object of this document.</summary>
public FrameNodeAdapter Node(FrameObjectBase frame)
{
Expand Down
Loading
Loading