Repository navigation
feat: push materials made in Blender into the game (diffuse, normal/specular) - #4
Conversation
Local work found uncommitted in the working copy (base a55dc22, v0.3.1): sphere/mesh preview toggle, the slot label on the assign panel, and the SDK pin moved to 10.0.302.
A push used to refuse any slot whose material had not come from the toolkit. A Blender-made material now travels with the image wired into its Principled Base Color and becomes a real game material: - addon: sends the image as RGBA8 (top-down, resampled to powers of two) once per push however many slots use it; the ack stamps the hash back on the datablock so later pushes send pixels only when they changed - DdsEncoder: DXT1 with a full MIP chain and the stock header, written as a single Texture entry (HasMIP 0) - the shape 2809 stock city textures ship in, so no companion Mipmap entry is needed - AuthoredMaterialResolver: fills in the hash before the mesh path runs; binds by name when the game already has the material, otherwise writes the texture into the object's archive (file + manifest) and creates a default-preset material with it in S000 (one undo entry) - a re-push with new pixels rewrites the texture in place and the renderer reloads it (TextureLibrary.Invalidate) Probe: --probe-bridge-material [district] [push.ilx] - encoder, resolver, re-push, bind-by-name, refusal, and optionally a container written by the addon itself.
The Sphere toggle is checked in markup, so its Checked handler ran before the Mesh toggle and the preview existed and the window died with a NullReferenceException on open. The handler now waits for both. The probe read the assign BUTTON's visibility; the button is hidden through its panel since the mesh-preview change, so it reads the panel. --probe-material-editor: 65 passed, 0 failed (was 50 + an exception).
A live probe drives the real viewport and a real bridge session (--probe-bridge-material-live): an off-screen D3DImageHost loads an archive, opens a mesh in a running bridge Blender and waits for an object made there, wearing a material made there, to be pushed back. It checks the scene, the renderer, a re-push with a changed image, undo/redo, Save, a reload from disk and a pack, then restores every file. 21 passed against Blender 5.1.2. What it found: - image resize went through Image.copy().scale(), which hands back the blank original of a painted or generated image - the texture arrived black. The addon now resamples the pixel array itself. - a rebuilt mesh's parts carried no material hash, so a later material edit (or a texture rewritten by a push) could not find them to re-resolve. The hash and tint now ride with the part, as on a mesh loaded from disk. - Blender keeps the hash of a material the library may have lost (creation undone, toolkit closed without Save). With pixels it is made again; without them the push is refused once and the ack tells the datablock to forget, so the next push arrives complete.
…g resources
A Blender-made material came out BLACK in game. The object was there and
the material was sound (same shader, flags and sampler states as 1888
stock materials) - the texture was not in a shape the game loads.
- From 256x256 up the game stores a texture split: the top level alone
in a Mipmap entry, everything from half resolution down in the Texture
entry with HasMIP=1 (6747 split textures surveyed, no exception; no
stock mip-chained texture is wider than 128 on its short side). The
encoder wrote a 1024x512 texture whole. DdsEncoder.Encode now returns
the pair, and ArchiveTextureWriter writes both files and both entries.
Two packer bugs found while comparing the rebuilt archive with stock:
- TextureHandler charged a split texture only its own payload. Stock
charges the Texture entry its payload PLUS its MIP companion's and
leaves the Mipmap entry at zero (381 of 381 in italy.sds), so every
Build under-reported the archive's video memory by the size of all its
top levels - 6 MB for italy.
- PruneMissingEntries joined a rooted resource name ("/missions/...")
with Path.Combine, looked for the file at the drive root, and unsaid a
resource that was present. italy.sds lost its AudioSectors on Build.
--probe-bridge-material: 30 passed (split layout, the memory figure
against the files on disk, pruning with a rooted name).
The billboard was still black in game after its texture was stored correctly. The material had been compared with stock on shader, flags, sampler states and parameters - through a view that does not show every field. Two were left at zero by the Default preset: - Unk0: 128 on 1837 of the 1929 stock materials on that shader - the diffuse sampler's TexType: 2 on 1947 of 1951 (0 on four) Material_v57's Default preset now sets both, which also covers the materials the glTF import creates. v58 (Definitive Edition) is left alone: there is no DE install here to measure it against. The probe compares a created material with the majority of stock materials on its shader in these fields, so a preset that drifts from the game's own records fails here instead of in game.
The previous commit did not build: the new stock-record check declared 'made', which the addon-container section of the same method already uses. --probe-bridge-material: 25 passed.
A Blender-made material arrived with its Base Color image only. It now also brings the image behind its Normal Map node and the one driving its specular level, and becomes a normal-mapped game material. Chosen from a survey of the stock library, not assumed: - shader 5159568776351604322 (S000 + S001, parameter D013): 1209 stock materials, drawn on the P|N|T|UV0 declaration a pushed mesh already has. Its S001 is one combined texture - normal X and Y in red and green, the SPECULAR level in blue (stock "...NS" textures: red/green 127 +- 10, blue 30..240), DXT1 like 97 of the 120 sampled. - NormalSpecularPacker builds that texture from the two Blender images (either may be missing) and inverts green: Blender's normal maps have green up, the game's bitangent follows texture V, which runs down. - MaterialPreset.DiffuseNormal creates the record in the commonest stock shape of that shader - Unk0, flags, both samplers' TexType and states, D013 - and the probe compares it field by field with the majority of the 1209. - Blender's roughness and specular level become D013's power and level; the defaults (0.5 / 0.5) land on the commonest stock pair, 16 / 0.3. A material that gains or loses its normal map needs another shader, so it is replaced under the same name - the hash meshes refer to does not move. The resolver now talks to an IAuthoredMaterialHost (create, update, replace) instead of two callbacks; the application's host records each on the undo history. Also: binding a texture to a sampler sets its TexType to 2 (a slot added in the Material Editor started at 0, which samples nothing in game); two materials sharing an image share one texture file. --probe-bridge-material: 33 passed. Driven live through the real viewport against a windowless Blender: the billboard in italy took a new material with all three maps and shows the relief in Render mode.
| { | ||
| (byte[] texture, byte[]? topLevel) = DdsEncoder.Encode(rgba, width, height); | ||
| string file = ArchiveTextureWriter.PickName(dir, imageName, reuse, texture); | ||
| ArchiveTextureWriter.Write(dir, file, texture, topLevel); |
There was a problem hiding this comment.
[P2] Include texture bytes in the push undo transaction
Repainting an already-authored image overwrites its DDS/MIP files here, but the history only records material bindings and mesh changes. If the texture name and geometry stay unchanged, UpdateAuthored records nothing: I reproduced History.CanUndo == false after a same-binding update. Undoing a push therefore cannot restore the previous pixels, even when it restores the mesh or material. Snapshot the affected files and manifest entries in the push transaction, and restore/invalidate them on undo and redo.
| if (content != null && existing.Exists && existing.Length == content.Length | ||
| && File.ReadAllBytes(existing.FullName).AsSpan().SequenceEqual(content)) | ||
| { | ||
| return candidate; |
There was a problem hiding this comment.
[P2] Compare the top mip before reusing a texture name
For split textures, content contains only the lower mip chain. Different images can have identical lower mips while differing in MIP_*.dds: a 256x256 black/white checkerboard and solid gray reproduce this with the current encoder. Importing them as stone.png and stone.jpg returns the same stone.dds name, then overwrites the first material's top mip. Include the top-level bytes and companion-file presence in the equality check before sharing a name.
| if (MafiaMaterials.FindHashByName(name) is { } known) | ||
| { | ||
| info.Hash = Format(known); | ||
| Acknowledge(name, known); | ||
| continue; |
There was a problem hiding this comment.
[P2] Package reused authored textures in every owning archive
When objects from two archives share a new Blender material in one push, the first object creates the catalog entry and writes its texture into archive A. The second object takes this branch and never packages that dependency into archive B. I reproduced two successful resolutions with the DDS present only in A and only one touched archive. B cannot resolve that texture when loaded independently in the game. Ensure each destination archive receives the authored material's texture dependencies, including when the catalog/hash is already known and no new pixels were sent.
| bool wasNormalMapped = current.Normal != null; | ||
| if (wasNormalMapped != updated.NormalMapped) | ||
| { | ||
| if (_host.Replace(hash, updated) is not { } replaced) |
There was a problem hiding this comment.
[P2] Preserve material identity when changing its shader
updated.Name comes from the current Blender datablock name, whereas ReplaceAuthored deletes the old material and creates one whose hash derives from that name. Rename a previously pushed shared material in Blender and add/remove its normal map: this replacement changes its hash and deletes the original catalog entry. Only slots in the current push get the new hash; other meshes retain the now-missing material. I reproduced the hash changing with the old entry absent. Preserve the existing catalog name/hash for shader replacement, or migrate all references as an explicit rename operation.
| string? diffuse = Write(document, dir, "d:" + info.DiffuseImage!.Block, info.DiffuseImage.Name, currentDiffuse, | ||
| diffusePixels, info.DiffuseImage.Width, info.DiffuseImage.Height, out reason); |
There was a problem hiding this comment.
[P2] Validate and stage all maps before overwriting existing files
The diffuse file is committed before normal/specular blocks are validated, and before the catalog update can succeed. An update with valid new diffuse pixels and an invalid normal block returns false but has already replaced the existing diffuse DDS; I reproduced this with an out-of-range normal block. Later map-write failures have the same partial-commit problem. The UI reports the object as skipped while its material's pixels have changed. Validate all inputs first and stage/rollback file and catalog changes so a refused update leaves the existing material intact.
| // changed; and one rewritten under its old name changed no material, so the renderer has | ||
| // to be told to read the file again. | ||
| foreach (FileInfo archive in authored.TouchedArchives.Values) _host.Persistence.MarkArchiveModified(archive); | ||
| foreach ((ulong hash, string texture) in authored.Rewritten) _host.MaterialEditing.ReloadTexture(hash, texture); |
There was a problem hiding this comment.
[P2] Coalesce texture reloads before refreshing shared materials
Rewritten gets an entry for every material occurrence even when _written has already handled the same image. A push of 100 objects sharing one material produces 100 identical reload entries in my harness. Each call here invalidates/reloads the DDS and RefreshMeshesUsing scans and rebinds all matching meshes on the UI thread, giving quadratic rebind work for a shared material. Deduplicate texture invalidations and refresh each affected material once after all its textures have been invalidated.
| { | ||
| if (!_cache.Remove(name, out Entry? entry)) return; | ||
| if (entry.Srv.Handle == _white.Handle) return; // a cached miss — nothing to keep alive | ||
| _retired[(nint)entry.Srv.Handle] = entry; |
There was a problem hiding this comment.
[P2] Release superseded mesh leases after a texture rebind
The new retirement path relies on old leases being returned, but GpuMesh.RebindPartTextures retains every previous lease until the mesh is disposed. Every repaint now leaves a distinct GPU texture alive for the entire time the mesh remains loaded. With a real GpuContext and GpuMesh, 20 invalidate/rebind cycles left 20 entries in _retired; they disappeared only on mesh disposal. This grows VRAM without a bound during painting, even after duplicate reloads are fixed. Track leases per part and release the superseded acquisitions once the part uses its replacement, preserving other meshes' references.
| new MeshPart(0, outIdx.Length, | ||
| part.DiffuseTexture, part.NormalTexture, part.SpecularTexture, part.MaterialHash), |
There was a problem hiding this comment.
[P2] Carry the material tint into the mesh preview
The replacement MeshPart omits part.Tint, so its default is white even though SetMaterial supplied the material's color. Switching to mesh preview therefore loses the color of tint-based materials, such as untextured painted surfaces. Calling BuildFlatMesh with a red part reproduced an output part with a white tint. Pass part.Tint to the new part so sphere and mesh previews show the same material color.
| if node.type != 'NORMAL_MAP': | ||
| return None | ||
| return _image_behind(node.inputs.get("Color")) |
There was a problem hiding this comment.
[P2] Check the Normal Map node's coordinate space
Checking only node.type accepts object- and world-space normal maps, but the game packer interprets their channels as tangent-space XY and flips green. These maps therefore export successfully with incorrect lighting. An OBJECT-space node is accepted by this helper in an isolated test. Blender exposes these choices through ShaderNodeNormalMap.space, verified through Context7. Require space == 'TANGENT' and report unsupported spaces, or convert them before packing.
| low = np.floor(position) | ||
| weight = (position - low).astype(np.float32) | ||
| first = np.clip(low.astype(np.int64), 0, current - 1) | ||
| second = np.clip(first + 1, 0, current - 1) |
There was a problem hiding this comment.
[P2] Clamp both interpolation indices independently
During upsampling, the first texel center lies before source texel zero, so low is -1. Clamping first before computing second turns the intended edge pair (0, 0) into (0, 1), blending heavily with the next texel. Resampling a row [0, 1, 0] from width 3 to 4 produces 0.875 at its left edge instead of 0. This corrupts the first row/column of images rounded up to a power of two. Compute second by clamping low + 1, independently of the already-clamped first.
…shared across archives Review of mafia2online#4, all ten findings. Texture files are part of the push's undo. A repaint under the same name changes no binding, so the catalog recorded nothing and Ctrl+Z could not bring the old picture back. The resolver now journals every file it writes (what the name held before, what it holds now) and the push carries a TextureFilesEdit in its one history entry: undo restores the old files - or removes a texture the push introduced, manifest entries included - and redo puts the new ones back. Nothing is written before everything is checked. The diffuse texture was on disk before the normal map had been looked at, and before the library had accepted the material. All images are now read, validated and packed first; what is written before a later file or the library refuses is rolled back from the same journal. A split texture is compared as two files. PickName compared only the entry, which for a texture of 256 and up starts at half resolution: a fine checkerboard and the grey it averages to are byte-identical there (reproduced), so the second picture took the first one's name and its write replaced the first one's top level. The MIP_ companion - and whether there is one - is now part of the comparison. A material shared by two archives is packed into both. The second object of a push binds the new material by name, and an unchanged material arrives with no pixels; either way an archive that had never held its textures got none. The files are now copied from the archive that has them (only for materials this bridge created - a stock material bound by name copies nothing). A renamed material keeps its hash across a shader change. Gaining or losing a normal map deletes and recreates the record, and it was recreated under the Blender datablock's current name - a new hash, with every other mesh left pointing at the old one. It is rebuilt under the name the library knows it by. Reloads are coalesced. A push of a hundred objects sharing a material asked for a hundred invalidate-and-rebind passes over every loaded mesh; each texture is now invalidated once and each material that names one rebinds once (ReloadTextureFiles). A rebound part returns the textures it held. GpuMesh kept every lease until dispose, so the library's retired copy of each repainted texture stayed on the GPU for as long as the mesh was loaded. Leases are kept per part and the superseded ones released once the part holds the new ones. The mesh preview keeps the material's tint. Add-on: a Normal Map node in Object or World space is no longer exported as if it were tangent space - the material is refused by name with what to change - and the resampler clamps its two neighbours independently: upsampling [0, 1, 0] to four texels gave 0.875 at the left edge, now 0. --probe-bridge-material 58/58 on eastside (30 new checks: each case above, including twenty repaints of a texture a live GpuMesh draws leaving nothing retired), --probe-material-editor 65/65, --probe-bridge-rebuild 17/17, --probe-mcp 66/66. The undo entry itself was checked at the file level (restore before/after), not by pressing Ctrl+Z in the editor, and the two add-on changes were not run inside Blender: the resampler was checked on the reviewer's numbers outside it.
838c69a made the texture files of a push part of its undo and put a refused material's files back. Read again before it went up: Undo removed a texture the push had introduced. The material that names it is created by an edit of its own and outlives the push's entry, and Blender - told the push arrived - sends pixels again only when the image changes: after one Ctrl+Z the texture was in no archive, and the next push put the material on the object with nothing to draw it from. The undo entry takes back pictures that were REPLACED; an introduced one stays with its material. Undo and redo did not put the archives back on the build list. Push, Build, Ctrl+Z: the folder had the old picture, the game's archive the new one, and Build answered that there was nothing to build. A push that only repainted a texture was dropped from the history by the first unload of any district: its entry names no objects, and 'all of its objects have left the scene' is true of none. A write that failed was remembered as done. The name was noted before the write; the next object of the push sharing the image was handed it as success and its material named a file that was never made. Noted after. A texture half written - the entry replaced and its top level, or the manifest, not - was caught with nothing on record to put it back from. It is put back before the failure is passed on, and neither that nor AtomicFile leaves its .tmp beside the target any more. An object is refused before any of its materials is touched: every slot is checked first for what can be refused without writing (normal map space, images that did not arrive whole). A bad second slot used to be found after the first slot's pictures had been replaced. A texture that cannot be copied into the object's archive refuses the object as well. --probe-bridge-material 61/61 (3 new: the two-slot object; a texture held open refuses both objects sharing the image and leaves the picture, the manifest and the folder as they were). --probe-material-editor 65/65. The undo entry itself (introduced texture kept, archives re-enlisted, the entry surviving an unload) is not covered by a probe - it lives behind the editor's history.
… undo entry Two things 4034b0f left. The check of every slot before any is written did not include a material that would have to be made and arrives with no picture to make it from - new, or one Blender remembers and the library no longer has: found at its own slot, after an earlier slot's texture had been replaced. Both are refused in the pre-pass now, in the words they had. And a push whose only texture changes were introductions made an undo entry with nothing in it - a Ctrl+Z that did nothing and cost the redo branch. --probe-bridge-material 62/62 (1 new: a repaint first, a new material with no image second - refused, the first left as it was).
… is sampled linear Review of mafia2online#5, the two findings outside the mirror. The material editor's preview still alpha-tested a translucent material. SphereMesh forwards a part's Blended flag, but the part the preview builds for a material carried only its tint, and the mesh-shape preview dropped the flag again - so a uniform 0.3-alpha pane vanished from the ball while the scene drew it blended. The flag is read from the catalog with the tint and kept in both preview shapes. Add-on: a colour image used as an alpha mask was read through image_rgba8, which turns a picture into what an albedo texture stores (sRGB bytes), and the luminance of those encoded values was written as coverage. A float mask at linear 0.25 came out as 137/255 instead of 64 - enough to carry a cut-out across the 0.5 it is tested against. The mask is now read in the values the shader works with (_shader_pixels): float buffers as they are, 8-bit sRGB linearized as the Image Texture node does, Non-Color data and the alpha channel as stored. Run in a background Blender on the add-on as it stands in this branch: linear 0.25 float mask -> 64, 8-bit sRGB 0.5 -> 55, Non-Color 0.5 -> 128, alpha channel 0.3 -> 77; the same run covers the two add-on fixes merged from mafia2online#4 (Normal Map node space, resampler edge). The preview change was not looked at in the material editor window.
|
All ten findings are fixed on top of
A city hall interior was built with this stack, M2O Client does not crash. If there are more remarks, I am ready to hear them. |
mafia2online#3 was stacked on this branch and was merged as one commit, so main already has every change made here and git could no longer line the two histories up. The merge takes main's tree as it is: nothing of this branch is left to add.
…ollows imported scenery (#6) * feat: preview a material on the context mesh slot in the Material Editor Local work found uncommitted in the working copy (base a55dc22, v0.3.1): sphere/mesh preview toggle, the slot label on the assign panel, and the SDK pin moved to 10.0.302. * feat: turn a material made in Blender into a game material on push A push used to refuse any slot whose material had not come from the toolkit. A Blender-made material now travels with the image wired into its Principled Base Color and becomes a real game material: - addon: sends the image as RGBA8 (top-down, resampled to powers of two) once per push however many slots use it; the ack stamps the hash back on the datablock so later pushes send pixels only when they changed - DdsEncoder: DXT1 with a full MIP chain and the stock header, written as a single Texture entry (HasMIP 0) - the shape 2809 stock city textures ship in, so no companion Mipmap entry is needed - AuthoredMaterialResolver: fills in the hash before the mesh path runs; binds by name when the game already has the material, otherwise writes the texture into the object's archive (file + manifest) and creates a default-preset material with it in S000 (one undo entry) - a re-push with new pixels rewrites the texture in place and the renderer reloads it (TextureLibrary.Invalidate) Probe: --probe-bridge-material [district] [push.ilx] - encoder, resolver, re-push, bind-by-name, refusal, and optionally a container written by the addon itself. * fix: stop the Material Editor throwing while its XAML loads The Sphere toggle is checked in markup, so its Checked handler ran before the Mesh toggle and the preview existed and the window died with a NullReferenceException on open. The handler now waits for both. The probe read the assign BUTTON's visibility; the button is hidden through its panel since the mesh-preview change, so it reads the panel. --probe-material-editor: 65 passed, 0 failed (was 50 + an exception). * fix: live-test the Blender material push and close what it found A live probe drives the real viewport and a real bridge session (--probe-bridge-material-live): an off-screen D3DImageHost loads an archive, opens a mesh in a running bridge Blender and waits for an object made there, wearing a material made there, to be pushed back. It checks the scene, the renderer, a re-push with a changed image, undo/redo, Save, a reload from disk and a pack, then restores every file. 21 passed against Blender 5.1.2. What it found: - image resize went through Image.copy().scale(), which hands back the blank original of a painted or generated image - the texture arrived black. The addon now resamples the pixel array itself. - a rebuilt mesh's parts carried no material hash, so a later material edit (or a texture rewritten by a push) could not find them to re-resolve. The hash and tint now ride with the part, as on a mesh loaded from disk. - Blender keeps the hash of a material the library may have lost (creation undone, toolkit closed without Save). With pixels it is made again; without them the push is refused once and the ack tells the datablock to forget, so the next push arrives complete. * fix: store large textures the way the game does, and stop Build losing resources A Blender-made material came out BLACK in game. The object was there and the material was sound (same shader, flags and sampler states as 1888 stock materials) - the texture was not in a shape the game loads. - From 256x256 up the game stores a texture split: the top level alone in a Mipmap entry, everything from half resolution down in the Texture entry with HasMIP=1 (6747 split textures surveyed, no exception; no stock mip-chained texture is wider than 128 on its short side). The encoder wrote a 1024x512 texture whole. DdsEncoder.Encode now returns the pair, and ArchiveTextureWriter writes both files and both entries. Two packer bugs found while comparing the rebuilt archive with stock: - TextureHandler charged a split texture only its own payload. Stock charges the Texture entry its payload PLUS its MIP companion's and leaves the Mipmap entry at zero (381 of 381 in italy.sds), so every Build under-reported the archive's video memory by the size of all its top levels - 6 MB for italy. - PruneMissingEntries joined a rooted resource name ("/missions/...") with Path.Combine, looked for the file at the drive root, and unsaid a resource that was present. italy.sds lost its AudioSectors on Build. --probe-bridge-material: 30 passed (split layout, the memory figure against the files on disk, pruning with a rooted name). * fix: give a created material the two fields the stock ones carry The billboard was still black in game after its texture was stored correctly. The material had been compared with stock on shader, flags, sampler states and parameters - through a view that does not show every field. Two were left at zero by the Default preset: - Unk0: 128 on 1837 of the 1929 stock materials on that shader - the diffuse sampler's TexType: 2 on 1947 of 1951 (0 on four) Material_v57's Default preset now sets both, which also covers the materials the glTF import creates. v58 (Definitive Edition) is left alone: there is no DE install here to measure it against. The probe compares a created material with the majority of stock materials on its shader in these fields, so a preset that drifts from the game's own records fails here instead of in game. * fix: rename a probe local that clashed with one further down The previous commit did not build: the new stock-record check declared 'made', which the addon-container section of the same method already uses. --probe-bridge-material: 25 passed. * feat: carry a Blender material's normal and specular maps into the game A Blender-made material arrived with its Base Color image only. It now also brings the image behind its Normal Map node and the one driving its specular level, and becomes a normal-mapped game material. Chosen from a survey of the stock library, not assumed: - shader 5159568776351604322 (S000 + S001, parameter D013): 1209 stock materials, drawn on the P|N|T|UV0 declaration a pushed mesh already has. Its S001 is one combined texture - normal X and Y in red and green, the SPECULAR level in blue (stock "...NS" textures: red/green 127 +- 10, blue 30..240), DXT1 like 97 of the 120 sampled. - NormalSpecularPacker builds that texture from the two Blender images (either may be missing) and inverts green: Blender's normal maps have green up, the game's bitangent follows texture V, which runs down. - MaterialPreset.DiffuseNormal creates the record in the commonest stock shape of that shader - Unk0, flags, both samplers' TexType and states, D013 - and the probe compares it field by field with the majority of the 1209. - Blender's roughness and specular level become D013's power and level; the defaults (0.5 / 0.5) land on the commonest stock pair, 16 / 0.3. A material that gains or loses its normal map needs another shader, so it is replaced under the same name - the hash meshes refer to does not move. The resolver now talks to an IAuthoredMaterialHost (create, update, replace) instead of two callbacks; the application's host records each on the undo history. Also: binding a texture to a sampler sets its TexType to 2 (a slot added in the Material Editor started at 0, which samples nothing in game); two materials sharing an image share one texture file. --probe-bridge-material: 33 passed. Driven live through the real viewport against a windowless Blender: the billboard in italy took a new material with all three maps and shows the relief in Render mode. * style: dotnet format the preview viewport's mesh builder * feat(mcp): tools that drive the running map editor The MCP server could read game files but not touch the editor, so a client working on a scene had to fall back on UI automation for everything the window does. These tools close that gap: - editor_status / editor_list_areas / editor_open_area (opens the editor from the launcher and waits for the area to finish loading) - scene_find (by name, kind, or a world box tested against a mesh's triangles rather than its bounds) and scene_select - blender_open / blender_end, and editor_notices for what a push reported - editor_save / editor_build, object_move, scene_delete_selected, editor_undo / editor_redo - camera_get / camera_look_at / camera_set / camera_frame_selection, view_set (shading mode and layers) and viewport_screenshot Illusion.Mcp gets the IEditorSession seam; the application implements it over the open MainWindow. Supporting changes: ViewportControl.CaptureFrame, LookAt and LookFrom, DistrictStreamer.IsBusy, LauncherWindow.OpenMapEditor, and a notice log the banner feeds. * feat(mcp): blender_push, and scene_select with no names clears the selection blender_push sends the bridge's request_push, waits for the push to land and returns what the editor reported, so a client that edited the session's objects in Blender gets the outcome in the same call instead of polling the notice log. An empty scene_select clears the selection: its outline is drawn through walls and would otherwise sit in every screenshot. * fix(bridge): refuse a pushed mesh over 65535 vertices instead of writing 32-bit indices A rebuild that came to more than 65535 split vertices switched the level's index buffer to 32-bit. The toolkit's viewport draws that; the game does not — an interior of 65 981 vertices came up as triangles smeared across the district, textures flickering, while the editor showed it whole. The push is now refused with the vertex count, so the modeller can split the object. A rebuild always writes 16-bit indices, and a level that still carries a wide buffer from before is rebuilt on its next push whatever that push changed, which is what puts it right. scene_find reports each mesh's vertex and triangle count, so the size of an object can be checked before it is built into an archive. * feat: bring a light into a district that has none — actor import across archives A city district ships with no lights of its own: its interiors are lit by LightEntity actors, and the only way the editor could make an actor was to copy one already in the pack. ActorsFile.Import copies an actor out of another archive's pack, with its own copy of the behaviour row it points at; the editor wraps it as one undoable edit and gives it a row in the tree, creating the entity type's section when the district never had one. Only an actor that places no object of its own scene can travel — a light, a sound — and the copy is linked to a frame named after itself, which is the frame such an entity makes. A light keeps its transform twice: in the actor record and at the head of its behaviour blob (every shipped light's x appears exactly twice). The record is what the editor moves, so the pack now carries the translation over into the blob when it is written; an untouched pack still writes byte for byte. MCP: actor_import, scene_duplicate_selected, object_properties and object_set_property (the property panel as data, undoable writes), and decode_actors can list each actor's behaviour fields. * Keep a light's inverse matrix and clip box in step with where it stands A light's behaviour blob ends with the inverse of its world matrix, and the game works the light out through it. A light imported from another archive (or moved in the editor) kept the old inverse: every field read correctly, the light stood in the right place, and it lit nothing. SyncLightFrame now rewrites the inverse whenever it is out of step with the head matrix, carries the clip box along by the distance the light moved, and runs on import as well as on write. Untouched packs still write byte for byte: shipped lights are within 1e-4 of a true inverse and are left alone. * Show and edit a mesh's draw distance in the property panel Each level of detail carries the distance at which it stops being drawn, stored squared. It is the one cost control a static object has - the game culls by distance and view cone only - and an object made through the Blender bridge starts at a million metres. The panel (and so the MCP object_set_property) now lists one draw distance per level, in metres. * fix: touch a light's row only when the light has moved The light sync rewrote any inverse matrix that was out of step with the head matrix. The install has 72 such rows that shipped that way (three Joe's Adventures archives carry zeroes or noise there), so re-saving those untouched packs changed them: --probe-act-relayout, --probe-actor-props and --probe-native-misc fell to 1047/1050. Now a light whose record and head translation agree is left alone, and a moved light gets a new inverse only if the one it had was a true inverse. The clip box and inverse are also written to the row's named fields: on write the core pokes every named field back over the blob, which put the imported light's old box straight back. --probe-act-relayout gains the import: a light from one archive into a pack of another, checking its own row, matrix, inverse, clip box, the writer round trip and a refused duplicate name. 1050/1050 packs re-save byte for byte again. * docs: the editor tools, the Blender material push and their limits README: the MCP section lists the 25 editor tools and says how they relate to the editor's own undo, save and build; the bridge section covers pushed materials and the 65535-vertex refusal; the property panel mentions draw distances; Known limits gains opaque-only pushed materials and undrawn light actors. --probe-mcp now expects the editor tools as well. * feat: carry a Blender material's alpha into the game - cut-out and translucent A material made in Blender was always opaque: its diffuse went to DXT1 and its flags were the plain ones. Now whatever feeds the Principled BSDF's Alpha - the image's own alpha, a mask image, or a value below 1 - is written into the diffuse texture, which is then stored as DXT5, and the material's render method picks the mode: Blended is a translucent surface, anything else a cut-out. The game has no separate shader for either on the shaders the bridge creates materials for; the flag word decides. Measured over default.mtl on the diffuse shader: 239 materials set Alpha and keep writing depth (fences, signs, foliage), 323 set Disable_ZWriting on DXT5/DXT3 textures (overlays), 11 carry both. A cut-out gets Alpha and keeps CastShadows; a blend gets Disable_ZWriting and drops CastShadows; every other bit is left alone. - DdsEncoder: DXT5 (interpolated alpha block + the DXT1 colour block), with the header stock DXT5 files carry. - MafiaMaterialCatalog: GetFlags / SetFlags; the editor's SetFlags is an undoable edit. - The addon sends alphaMode with the material and puts the alpha it means into the image; an opaque material keeps its old signature, so nothing is resent for nothing. - --probe-bridge-material: 40/0, with the encoder's alpha round trip, the flag words, and one material taken through cut-out, blend and back. * feat(viewport): draw alpha-blended materials blended The mesh shader alpha-tested every textured material at one half. That is right for a cut-out and wrong for glass: a pane whose alpha is 0.3 was clipped away entirely, so a translucent material - a stock one or one pushed from Blender - was simply not there in the viewport. A part now knows whether its material blends (Disable_ZWriting in the flag word), and such parts are drawn in a pass of their own after the opaque one: depth-tested, never depth-written, standard alpha blend, no clip. The flag follows a material edit the same way the textures and the tint do. Unsorted on purpose - a pane seen through a pane may composite in the wrong order. Instanced props keep the alpha test. * feat: mirror a district's edits into its winter archive A district ships twice and an edit made to <name>.sds is simply absent from <name>_z.sds. For most districts the two are one scene: measured on uppertown, every shared resource (buffer pools, name table, actors, collisions, item descriptions, prefab) is byte for byte the same, and the frame resource has the same length and differs in 1740 places - each a material hash swapped for its ^zima counterpart, 82 pairs. Textures aside, nothing else differs. SeasonMirror writes the summer working copy over the winter one with each object's winter materials taken from the winter object of the same name (and occurrence - names repeat), copies the shared files that differ, adds the pools summer gained with their manifest entries, and adds the textures the result names that winter lacks. The caller queues the winter archive for Build. Not every district is such a pair: 13 of 23 are, ten have a winter scene of their own. The pair is therefore judged first on the archives as shipped (each one's oldest backup, or the archive itself if never built), and a district that fails is refused rather than overwritten. - MCP: editor_mirror_winter. - --probe-season-mirror: 11/0 on scratch folders built from one working copy, so it does not depend on what the modder did to the install - an untouched pair is left alone, a missing object arrives and winter is byte for byte whole again, a stranger is refused untouched; reports which of the install's pairs are twins. * feat: carry objects from one archive into another A district can now be furnished from the rest of the game: a door from a shop, a chair or a plant from an interior, brought into the loaded area as one undoable edit (MCP object_import). - FrameTransplant copies a frame subtree across resources: frames re-homed (FrameEntry.MoveTo), geometry and material blocks deep-copied once per shared block, every buffer they draw from copied into the district's own pools under fresh names. Links inside the subtree follow the copies; the root becomes a prototype (an actor places it) or scenery anchored to the district's main scene, on the name table, standing where it is put. - ActorsFile.Import takes an actor that places an object once that object has been carried, links it by the copy's name through a scene reference of its own, and can turn it. - ArchiveCarry brings what the scene does not hold: the textures the materials name, the item descriptions the collision frames name, and the prefab entry the definition names (PrefabFile.Adopt, which keeps the container's size header right - the core writes it as it is handed it). Keys travel unchanged; what the district already has is shared. - ObjectImportController shows the copy (tree rows, GPU meshes, actor row) and undoes it as one edit. - A frame named by hash alone (weapon parts) kept a zero hash when copied: the copy constructor rebuilt the name from its empty string. Fixed for the duplicator and the prototype cloner too. --probe-object-transplant: 30/0 - a door, a prop an actor places and a piece of scenery go from shops/harry.sds into a scratch copy of a district, keep their shape frame for frame and their buffers byte for byte, survive a save and a reload (actors resolve, meshes decode from the district's pools, prefab and item descriptions in the manifest), and an undo leaves the scene byte for byte as it was. * feat: a Props tab to drag stock objects into a district The map editor's property rail gets a Props tab: the stock game's doors, seating, tables, beds, storage, plants, lamps and decor, one card per object with a picture. Drag a card onto the viewport to put the object where it lands (on the scene's surface under the cursor), or double-click it to put it in front of the camera. - PropCatalog finds them in the extracted interiors and districts: every actor that places an object of its archive (door, physics prop, framed furniture), and scenery whose name says what it is and whose size is a piece of furniture. One card per object across archives; interiors are read first, so a card points at the stock source. The scan (~6 s) is remembered per archive and redone only for what changed. - PropThumbnailRenderer draws each card's picture offscreen with the object's own textures and keeps it on disk. - Scenery now brings collision: the source's hulls that stand inside its footprint, re-placed by the same move (CollisionCarry), or one cooked from its triangles when it had none - small props usually have none. - A door carried out of its building gets ActorActionsEnabled: a shop's front door ships with the player's actions off, its shop script turns them on, and nothing does that where it lands. - The import core moved from the MCP session into ObjectImportController, shared by object_import and the tab. A card from the loaded district is duplicated in place rather than refused. - RefManager hands out ids atomically: the catalog parses scenes in the background while the editor loads districts. --probe-prop-catalog: 7/0 - 2747 props in 6.3 s, 52 ms remembered, known doors and chairs on their shelves, each shelf's first card gets a picture. * feat: simplified hulls for imported scenery, and a props column - A hull cooked for scenery that had none of its own is now its convex hull by default (ConvexHull: incremental, points snapped to a grid a fortieth of the object's size) - a coat rack goes from 1474 collision triangles to 22. Also a box (12 triangles) or every render triangle, chosen per import: object_import's 'collision' (auto / convex / box / mesh / none) and a chooser in the props panel. A flat thing has no convex hull and gets a box. - The props panel moves out of the property rail into a column of its own left of the viewport: as tall as the window, sized with a splitter, shown and hidden by a Props button on the toolbar; the viewport's tool shelf moves with the viewport and is no longer covered. - Textures an import carries are written down beside the working copy (illusion_carried.json, outside the manifest) and each save sweeps the ones no material of the scene names any more - an undone import, or one lost with a scene closed unsaved, no longer leaves megabytes behind. --probe-object-transplant: 33/0 (+ the hull of a cube is the cube, a stock model's hull holds all its vertices and faces out, a flat thing gets a box). * feat: an imported object's collision moves and is deleted with it Nothing in an archive ties a static object to its collision - the hull is a placement that happens to stand where the object stands. For scenery the toolkit carried in, the tie is now written down beside the working copy (ImportLinks, illusion_links.json, outside the manifest): the object's name and the hashes of the hulls it was given. A gizmo drag of the object drags those placements along (a scale mints a scaled hull as it does for any placement), and Delete takes them with it; a Delete that went through several editors is one Ctrl+Z (EditHistory.SquashSince). Stock objects are untouched: nothing ties them, and guessing by position would take a building's hull along with a bench. * feat: a duplicate of an imported object gets copies of its collision Duplicating scenery the toolkit carried in also places copies of the hulls it was given, where the copy stands, links them to the copy under its own name, and the whole duplicate is one Ctrl+Z. Linked hulls are now claimed one to one across the scene, nearest first and ties to the name that sorts first: a copy stands on top of its original with the very same hull, and dragging it used to drag the original's hull away. * feat: save drops geometry nothing draws; a re-import shares geometry - Save writes the buffer pools without the buffers no geometry block draws from - left behind by a deleted object, a sanitised block, a rebuilt mesh. Measured: none of the 130 archives as shipped carries one (--probe-geometry-sweep), the install's working copies had ~0.5 MB. Only the files lose them: memory keeps them, so an undo after a save brings the object back and the next save writes them again (SdsGeometrySaver.SavePools tracks what each pool file holds). - Importing an object again draws from the buffers - and, when every level is there, the geometry block - its earlier import brought, the way the shipped scenes put many copies of one chair on one mesh. Where geometry went is remembered per source archive beside the working copy (ImportGeometry, illusion_geometry.json); a remembered buffer the scene no longer has is simply copied again. - The bridge probes snapshotted only the pool they edited; a save now may rewrite others, so they snapshot and restore every pool. --probe-object-transplant: 38/0 (+ imported twice shares, the memory survives on disk, undoing copies keeps the first, a save leaves dead buffers out of the files and writes them back when drawn again). * perf(bridge): read a level's index width without decoding the mesh Review of #3. HasWideIndices decoded the whole level - every vertex channel, the index array and, on a model, the skin - to read one field of its index buffer, on every push, ahead of a path that decodes the mesh again. It now clamps the level and looks at the index buffer directly. --probe-bridge-rebuild 17/17, --probe-bridge-vertex 2/2, --probe-bridge-resplit 6/6. * fix(mcp): editor tools say no when the editor does, and find crash copies from the table Review of #3, the findings in the MCP session. editor_open_area dropped unsaved edits. Changing the area or the season reloads the scene, which clears the edited frames, the unsaved list and the undo history, and the tool only checked for a Blender session. A load that would replace a scene with unsaved edits is now refused unless the caller passes discardUnsavedEdits=true; asking for the area and season already shown stays a no-op. editor_save and editor_build reported success after a save that did not complete. The viewport's save does not throw for a material library that would not write or a car's working copy that was refused - it posts a notice and carries on - so the tool had nothing to return but success, and a build went on to pack archives whose materials were not on disk. D3DImageHost.SaveEditsReport hands back what was left unsaved; editor_save answers success=false with notSaved, and a build whose save did not complete packs nothing and says why. object_set_property could not rename. Base.Name is offered as an editable HashName and the parser had no case for it. It has one now, with the property panel's rule (an empty name is refused), and a rename to the name the object already has is not committed as an edit. Numeric properties took NaN and infinity. float.TryParse accepts "NaN", "Infinity" and "1e100" (as infinity); a float and each component of a vector now have to be finite. scene_find did not see crash copies until their row had been expanded in the tree, and a box query never returned one. They are now searched from the placement table: by the copy's own label or its prop's name, and by the prop's triangles at the copy's matrix. Only the copies returned get a tree node. --probe-editor-tools (new, no game install needed) drives the real tool methods against a scripted editor and the parser directly: 23 checks. Checked live in the editor on uppertown: 5 lamp copies found with no row expanded, 288 triangles of one in a box round it, none 400 m above it; a moved mesh blocks a load of another area and of the other season, the same area is a no-op, discardUnsavedEdits=true loads. A failing material write was not reproduced live - that path is covered by the scripted editor only. * fix(mirror): the winter mirror decides against the pair as shipped, not against the last mirror Review of #5, the four findings in SeasonMirror. Twins were told by size. Two archives passed as "one scene shipped twice" when their shared pools were equal and their scenes the same length - and a winter scene with one object moved 50 units is the same length. The scenes are now parsed, winter's material hashes replaced by summer's, and the two written back out: anything left that differs (a transform, a parent link, a draw distance) means winter is a scene of its own and the mirror is refused. The 13 stock twins still read as twins and the other ten as not. Everything else follows from reading that pair once (SeasonPair) and deciding against it, where the mirror used to ask the winter WORKING COPY what winter wore - which answers with whatever the previous mirror left: - A slot the modder re-pointed in summer got its old winter material back. A summer material no object of that name wore when the pair shipped is now carried into winter as it is (Report.Reassigned). - Namesakes were matched by position among objects of the same name, so deleting the first handed its materials to the second. The season's substitution is now looked up by name and SUMMER MATERIAL; position is only used when one summer material stands for two winter ones under one name, and only while the number of namesakes is what it shipped as. Otherwise the object keeps summer's material and is counted (Report.Ambiguous, raised as an error notice) rather than dressed in another object's snow. - A texture an earlier mirror had brought was never refreshed: the bridge repaints an authored texture in place, and "winter has a file of that name" ended the matter. A file winter did not SHIP with is now compared and rewritten, its MIP_ companion with it (and removed when the new picture has none); a file winter shipped with is the season's own and is left alone. Mirror() also refuses a winter folder that is not made of the pair's meshes before writing anything, and editor_mirror_winter stops when the save it starts with did not complete. Its result names slotsReassigned and objectsAmbiguous in place of objectsReshaped. --probe-season-mirror builds its own namesakes and runs every case above on scratch copies: 23/23 on uppertown and oysterbay, 22/22 on italy, 21/21 on greenfield (not a twin, so the shipped-texture checks do not apply). * fix(bridge): a material push that can be undone, refused cleanly and shared across archives Review of #4, all ten findings. Texture files are part of the push's undo. A repaint under the same name changes no binding, so the catalog recorded nothing and Ctrl+Z could not bring the old picture back. The resolver now journals every file it writes (what the name held before, what it holds now) and the push carries a TextureFilesEdit in its one history entry: undo restores the old files - or removes a texture the push introduced, manifest entries included - and redo puts the new ones back. Nothing is written before everything is checked. The diffuse texture was on disk before the normal map had been looked at, and before the library had accepted the material. All images are now read, validated and packed first; what is written before a later file or the library refuses is rolled back from the same journal. A split texture is compared as two files. PickName compared only the entry, which for a texture of 256 and up starts at half resolution: a fine checkerboard and the grey it averages to are byte-identical there (reproduced), so the second picture took the first one's name and its write replaced the first one's top level. The MIP_ companion - and whether there is one - is now part of the comparison. A material shared by two archives is packed into both. The second object of a push binds the new material by name, and an unchanged material arrives with no pixels; either way an archive that had never held its textures got none. The files are now copied from the archive that has them (only for materials this bridge created - a stock material bound by name copies nothing). A renamed material keeps its hash across a shader change. Gaining or losing a normal map deletes and recreates the record, and it was recreated under the Blender datablock's current name - a new hash, with every other mesh left pointing at the old one. It is rebuilt under the name the library knows it by. Reloads are coalesced. A push of a hundred objects sharing a material asked for a hundred invalidate-and-rebind passes over every loaded mesh; each texture is now invalidated once and each material that names one rebinds once (ReloadTextureFiles). A rebound part returns the textures it held. GpuMesh kept every lease until dispose, so the library's retired copy of each repainted texture stayed on the GPU for as long as the mesh was loaded. Leases are kept per part and the superseded ones released once the part holds the new ones. The mesh preview keeps the material's tint. Add-on: a Normal Map node in Object or World space is no longer exported as if it were tangent space - the material is refused by name with what to change - and the resampler clamps its two neighbours independently: upsampling [0, 1, 0] to four texels gave 0.875 at the left edge, now 0. --probe-bridge-material 58/58 on eastside (30 new checks: each case above, including twenty repaints of a texture a live GpuMesh draws leaving nothing retired), --probe-material-editor 65/65, --probe-bridge-rebuild 17/17, --probe-mcp 66/66. The undo entry itself was checked at the file level (restore before/after), not by pressing Ctrl+Z in the editor, and the two add-on changes were not run inside Blender: the resampler was checked on the reviewer's numbers outside it. * fix: the material preview blends what the scene blends; an alpha mask is sampled linear Review of #5, the two findings outside the mirror. The material editor's preview still alpha-tested a translucent material. SphereMesh forwards a part's Blended flag, but the part the preview builds for a material carried only its tint, and the mesh-shape preview dropped the flag again - so a uniform 0.3-alpha pane vanished from the ball while the scene drew it blended. The flag is read from the catalog with the tint and kept in both preview shapes. Add-on: a colour image used as an alpha mask was read through image_rgba8, which turns a picture into what an albedo texture stores (sRGB bytes), and the luminance of those encoded values was written as coverage. A float mask at linear 0.25 came out as 137/255 instead of 64 - enough to carry a cut-out across the 0.5 it is tested against. The mask is now read in the values the shader works with (_shader_pixels): float buffers as they are, 8-bit sRGB linearized as the Image Texture node does, Non-Color data and the alpha channel as stored. Run in a background Blender on the add-on as it stands in this branch: linear 0.25 float mask -> 64, 8-bit sRGB 0.5 -> 55, Non-Color 0.5 -> 128, alpha channel 0.3 -> 77; the same run covers the two add-on fixes merged from #4 (Normal Map node space, resampler edge). The preview change was not looked at in the material editor window. * fix(import): a failed import leaves nothing behind; redo gets its shared block and its textures back Found reviewing this branch the way #3-#5 were reviewed. An import that failed after the frames were copied left them in the scene. The copy is made first, then its textures are carried, its rows built and its collision read; a failure in any of those answered "the import failed" with the frames still in the resource - in the next save, with no row in the tree to delete them by and no undo entry. The catch also named five exception types, so a collision file that does not decode went past it as a tool error, same leftovers. Import now takes the copy back out when it fails before the edit is on the undo stack, catches whatever is thrown, and says in the refusal that nothing was left. Edits of one import that were already applied when a later one throws are undone again; an actor already in the pack is removed; GPU meshes made for rows that will never be shown are released. A failure AFTER the edit is registered (writing the note that lets the next import share geometry) no longer reports the import as failed. Redo of a copy that shares a geometry block threw on the next save. A second import of the same object draws from the first one's block and lists none of its own. With the first copy deleted and the second import undone, a save prunes the block; Reattach put back only the blocks the copy lists, so the redone mesh named a block the resource did not have and the save after it died in UpdateFrameData (KeyNotFoundException). Reattach now also re-registers what its meshes point at, as DetachedFrames does. A save swept textures that undo could still bring back into use. Carried textures no material of the scene names are removed at save - but the import can be redone and a deleted object's delete undone, and neither carries anything again: the object came back naming textures that were gone from the working copy and its manifest. Swept textures are now parked - out of the manifest, into illusion_parked\ under a name no scan for *.dds answers to, with their manifest entries kept field for field - and the next sweep that finds the scene naming one puts it back. What an earlier run of the program parked is dropped at the first save of the next; a texture carried again while parked wins over the parked copy. A file that cannot be moved leaves the texture where it is for the next save instead of failing the save. --probe-object-transplant hill shops\harry.sds 50/50 (12 new): sweep with the objects in / undone / saved twice / redone (same bytes, same manifest entry, MIP companion with it) / swept again / carried again while parked / left by an ended run; the shared block after delete + undo + save + redo, saved and read back. Without the Reattach change the same probe dies with the KeyNotFoundException. Live, against the editor: a source whose collision file is cut short - before the change every later import under that name answers "already taken"; after it the same refusal twice and no rows. A scenery import (250 hulls) and an actor import still work, with undo, redo, undo. Not checked: the failure paths inside ImportPlaced after the pack took the actor, and a failing GPU mesh. * test(import): the carried-again check asks for the textures, not for their count Higher in the stack a texture can also be borrowed from a third archive, so the second carry brings more than the three objects brought the first time. * fix(import): the lesser findings of the self-review - carry, links, names, numbers, thumbnails The second half of the review of this branch. One commit because the pieces lean on each other; by finding: Carry. A refused or failed import now takes its carry back: the working copy's manifest, prefab containers and carried register are noted before the carry and restored, and the files the manifest gained are removed (ArchiveCarry.Note / TakeBack). It used to leave textures, item descriptions and a prefab entry behind for an object that never arrived - the pack refuses an actor only after the carry. The texture a mesh names itself (OMTextureHash) is carried, and counted as in use by the save-time sweep. A MIP companion is copied before the texture that needs it, and a texture whose companion cannot be brought is not brought. RemoveEntry writes the manifest through a temp file, as AddEntry did. Object-to-hull link. The hulls an imported object was given are found by distance to the object's box, not its pivot - a district mesh's pivot can be metres from its geometry, and its hulls were then never linked. A move typed into the Transform panel takes the hulls along (it went through RecordTransform, which knew nothing of them; past 5 m they were lost for good and a later delete left them in the .col). Renaming the object moves its record. The record is written by an edit of the import (and of the duplicate), so undo takes it back out of the file. Links are worked out once per operation for the whole selection instead of once per node, each time re-reading the file and walking every object and placement. Source. object_import takes `occurrence`: the n-th thing of that name in the source (actors first, then frames - the ones that draw before helpers of the same name), and reports how many there are. Position and heading must be finite. The source archive is kept between imports from it (let go after a minute, re-read when its working copy changed) instead of being read whole for every object. "N hull(s) of its own" is now worded as what it is - the hulls standing inside its box - in the result and in the tool description. Prop thumbnails. The archive is read on a pool thread (Stage) and only drawn on the UI thread (Draw); the tick is guarded, since nothing above a DispatcherTimer catches; the kept picture is keyed by the working copy's time stamp as well and replaces the older ones; the held scene is let go when the tab has all its pictures; texture folders no longer accumulate across archives (TextureLibrary.ClearFolders). --probe-object-transplant 53/53 (the take-back on a scratch copy; an OM-named texture through carry, sweep, park and return). --probe-object-import-live (new; a real window with `hill` loaded, the district's working copy compared with its state before and put back): 30/30 - typed move and turn with undo/redo, drag, rename and its undo, delete and its undo, everything undone leaves no record, occurrence past the end, a source whose collision is cut short refused twice with the same answer, a door the pack refuses leaving manifest and folder as they were. --probe-prop-catalog 9/9, --probe-editor-tools 23/23. Not checked: the Props tab filling in a window (only Stage/Draw are), a companion that fails to copy, hulls linked through the box on an object whose pivot really is far off (the probe's object has it close). Not changed: "its own hulls" are still chosen by position - an archive has nothing else to go by; item descriptions and the prefab entry of an UNDONE import still stay in the working copy. * fix(import): review of #6 - links by place, blocks shared only when alike, the Props tab paged What the review of this branch found that the commits before this one had not already answered (the failed-import rollback, the swept textures and the numeric-edit path are in 56f5f8d and 45236da). A collision link is a placement, found by its place. The record kept one hash per hull: two placements of the same hull collapsed into one entry (Distinct), a resize re-cooked the hull under a new hash the record did not learn, and a placement was looked for within 5 m of the object's pivot, which says nothing about where a placement's origin is. Each entry is now one placement - the hull and where it stood in the object's own space when the link was made - and it is found where the object's matrix now puts that point, which every move, turn and resize the two make together leaves unchanged. The same hull twice is two entries; a resize rewrites the hashes in the record as an edit inside the same undo step; a record written before this (bare hashes) is read as before and matched by nearness to the object's box. A geometry block is shared only when it draws the same way. Matching buffers and decompression values was taken as "the same block", so a second import inherited whatever had been done to the first copy's block since - a draw distance set to 0, and an object that does not draw. The draw distance, vertex layout and count and both capsules are compared (FrameLOD.DrawsLike); the buffers are still shared, the block is copied when it differs. The Props tab makes cards a page at a time. A wrapping panel does not virtualize, and the whole catalog - 2,900 objects here - was given cards at once, about two seconds of the UI thread on opening and on every filter change. 120 cards, and the next 120 when the end of them comes within reach; pictures go to the cards in view first. The picture timer, stopped when the tab is hidden, starts again when it is shown - a catalog already loaded had nothing to restart it. --probe-object-import-live 36/36 (6 new: a resize gives the hull a new hash and the record follows, undo puts both back; a second placement of the same hull nine metres from the pivot is the object's too, and both go with it). --probe-object-transplant 54/54 (a copy whose draw distance was changed does not lend its block). --probe-prop-catalog 12/12 (the tab in a 330x700 window: 120 cards for 2928 objects, 240 after scrolling to the end, pictures resume after hide and show). Not checked: a record in the old format on a real working copy. * fix(props): the picture cache of an earlier build is left alone 45236da gave a prop's kept picture a name that carries the state of its archive, and on first use deleted every picture under the old name as something nothing reads. A build from before that change, in use beside this one on the same folder, reads exactly those: its Props tab lost all 2,928 pictures the first time a probe of this branch ran. Only a prop's own pictures from earlier states of its archive are dropped now. --probe-prop-catalog 12/12; 2,928 old-format pictures still there after it. * fix(bridge): what a second look at 838c69a found 838c69a made the texture files of a push part of its undo and put a refused material's files back. Read again before it went up: Undo removed a texture the push had introduced. The material that names it is created by an edit of its own and outlives the push's entry, and Blender - told the push arrived - sends pixels again only when the image changes: after one Ctrl+Z the texture was in no archive, and the next push put the material on the object with nothing to draw it from. The undo entry takes back pictures that were REPLACED; an introduced one stays with its material. Undo and redo did not put the archives back on the build list. Push, Build, Ctrl+Z: the folder had the old picture, the game's archive the new one, and Build answered that there was nothing to build. A push that only repainted a texture was dropped from the history by the first unload of any district: its entry names no objects, and 'all of its objects have left the scene' is true of none. A write that failed was remembered as done. The name was noted before the write; the next object of the push sharing the image was handed it as success and its material named a file that was never made. Noted after. A texture half written - the entry replaced and its top level, or the manifest, not - was caught with nothing on record to put it back from. It is put back before the failure is passed on, and neither that nor AtomicFile leaves its .tmp beside the target any more. An object is refused before any of its materials is touched: every slot is checked first for what can be refused without writing (normal map space, images that did not arrive whole). A bad second slot used to be found after the first slot's pictures had been replaced. A texture that cannot be copied into the object's archive refuses the object as well. --probe-bridge-material 61/61 (3 new: the two-slot object; a texture held open refuses both objects sharing the image and leaves the picture, the manifest and the folder as they were). --probe-material-editor 65/65. The undo entry itself (introduced texture kept, archives re-enlisted, the entry surviving an unload) is not covered by a probe - it lives behind the editor's history. * fix(mcp): a layer is not switched off over unsaved edits; moves and imports take finite numbers A second look at f12155b. editor_open_area was given a guard against dropping unsaved edits, and view_set was left without one: switching the crash (or collision) layer off unloads it, and an unsaved move of a crash copy went with it, undo entry included - the same loss by a shorter road, and an easier one to take now that scene_find hands out crash copies. Refused while the scene has unsaved edits, unless discardUnsavedEdits is passed; switching a layer on, or off with nothing unsaved, is as before. object_move and actor_import took a position or an offset as it came: a number too large for a float arrives as Infinity and was written into the transform. They refuse what is not finite, as property values already do. --probe-editor-tools-live (new; a real window with a district loaded, nothing saved) 12/12: both refusals of non-finite numbers; the layer off and on with nothing edited; a crash copy moved, the layer refused, zones still switch on, the layer off once the edits are given up. --probe-editor-tools 23/23, --probe-mcp 66/66. * fix(mirror): a second look at 3696630 and a3b5f73 Read again before they went up. A material block worn by two objects was settled twice. The mirror writes winter's hashes into the block, so the second wearer was resolved from them as if they were summer's - counted as a slot the modder had re-pointed, or substituted again where a winter hash is also a summer one. The wearers of each block are gathered first and the block is settled once, from summer's hashes, by whichever wearer the pair has an answer for. A mesh that existed only in winter was lost without a word. 'Dropped' had become the shipped pair's meshes summer no longer has; it is what the mirror takes out of winter - every winter mesh the mirrored scene has no counterpart for. A refreshed texture kept its old manifest entry. A repaint at another size changes whether the texture has a MIP companion, which its entry states (HasMIP); the bytes were brought up to date under an entry that promised a companion no longer there, or hid one that now was. The entry is compared with summer's and replaced with it. The material list's tiles draw a blended material blended, as the preview beside them does since a3b5f73. And the add-on's push signature includes the colour space of an alpha mask: an 8-bit mask is linearised or not by its tag, so retagging it changes what is sent while name, file and size stay the same - and nothing was resent. --probe-season-mirror uppertown 28/28 (5 new: a mesh only in winter is reported as dropped; a block two objects wear, the wearer that never wore the summer material first in the file; a refreshed texture's entry follows a changed HasMIP). --probe-bridge-material 68/68, --probe-material-editor 65/65. The add-on change is compiled, not run in Blender. * fix(import): a third pass over the collision links, the sweep and the Props tab Read again before it went up. A hull changed on its own lost its tie. Resized by itself it is re-cooked under a new hash, and only a resize of the OBJECT rewrote the record; the next move or delete of the object left the hull behind - or took some other placement of the old hull that stood near. Moved by itself past five metres it stopped being found. The record now follows a placement through whatever is done to it: hash and place are brought up to date in the same undo step, whether the placement was changed with its object or alone. Two ways of moving an object did not carry its collision: a transform pushed from Blender, and a move of a frame the object had been parented to. Both go through the same carrying as a typed move now (CarryLinked), and what was given to anything UNDER a node counts as riding with it - for a drag, a typed move, a push and a delete alike. One description of which placement is whose (Assignments) serves all of them. Renaming an imported object onto a name that already had a record wrote over that record, and the undo then took the remainder away from the other object as well. A record is never moved over another. Parking a swept texture could lose it: with the manifest entry dropped and the file's removal failing, nothing was on record, and the next sweep deleted the file for good. Both files are copied aside and written down first; whatever fails after that, they are on record. The sweep itself - housekeeping in the middle of a save - no longer lets an unreadable manifest end the save between the scene and its buffer pools. And a redo of an import puts its parked textures back at once, not at the next save. The Props tab could stop paging at the very end of the list: a page held back by the one-per-gesture rule was never asked for again, since no scroll event comes when the view is already at the bottom. It is looked at again a moment later. Reading a prop's archive no longer holds the lock the UI thread takes when the tab is left. --probe-object-import-live 44/44 (8 new: a hull resized alone and its undo; a hull moved fourteen metres alone still goes with its object; a transform pushed through the push batch carries the hull and undoes; an object renamed onto another's name leaves that record alone, before and after undo). --probe-object-transplant 55/55 (a redo returns parked textures). --probe-prop-catalog 12/12, --probe-season-mirror 28/28. Not checked: a move of a parent frame with an imported object under it; the paging retry and the tab being left during a read, in a window. * fix(bridge): the pre-pass refuses a material it cannot make; no empty undo entry Two things 4034b0f left. The check of every slot before any is written did not include a material that would have to be made and arrives with no picture to make it from - new, or one Blender remembers and the library no longer has: found at its own slot, after an earlier slot's texture had been replaced. Both are refused in the pre-pass now, in the words they had. And a push whose only texture changes were introductions made an undo entry with nothing in it - a Ctrl+Z that did nothing and cost the redo branch. --probe-bridge-material 62/62 (1 new: a repaint first, a new material with no image second - refused, the first left as it was). * fix(mcp): the layer guard is about the crash layer only 01973ba guarded switching the collision layer off as well, on the belief that it unloads the layer. It only hides it: the placements, their edits and their undo entries stay, so the guard refused for nothing and its discardUnsavedEdits discarded nothing. Crash only, which does unload. And object_move refuses a position plus offset that overflows, though each number alone is finite. --probe-editor-tools-live 14/14 (collision on and off with an edit unsaved; the overflowing move). * fix(mirror): wearers that disagree, names that differ in case, an entry that cannot be copied Three things 7750e77 left. Wearers of one material block that give different winter materials for a slot: the first in file order won and both were counted as settled; the slot keeps summer's and the objects are put down as in doubt. A texture entry compared with summer's by exact spelling of the file name: a material that spells it otherwise than the manifest made every mirror rewrite the entry and report a refresh. And the old entry was removed before knowing summer's could be copied in its place. --probe-season-mirror 28/28. * fix(import): the collision links once more - typed and pushed moves of a hull, two objects of one name, parking on record first What a second independent reading of d880b76 found. A hull moved on its own by a typed number or by a push from Blender still lost its tie past five metres: it was looked for after it had moved, and only the drag asked beforehand. It is recognised by where it stood (LinksOf takes the place it had), and the record follows. A push from Blender carried a hull that Blender itself places. With the hull in the session, the editor moved it with its object and the next push moved it back - or, with both moved in one push, it went twice. A push carries only placements the session does not hold; what rides with each moved object is worked out once for the batch, before anything is applied, and the records are brought in line once at the end, with everything where the push leaves it. Two objects of one name. A record's owner was the first frame of that name in the file, so renaming an earlier object onto a later one's name handed it the later one's collision. Among namesakes the owner is the one the record fits - whose matrix puts the recorded places where those hulls stand. And a rename moves a record only for the one object bearing the name: renaming the newcomer away again leaves the other's record be. A record from before places were kept gains its places the first time anything is done to one of its placements, instead of never. Parking wrote its index at the end of the sweep, after the originals were gone, and the sweep now swallows failures: an index that could not be written left textures removed and nowhere on record. The index is written before anything is taken out of the working copy; a texture whose removal only half succeeds stays carried and is tried again. The prop reader no longer re-holds an archive the tab let go while it was being read. --probe-object-import-live 48/48 (4 new: a hull typed sixteen metres away on its own stays the object's, with its undo; an earlier object renamed onto a later one's name does not take its collision, and renamed back each has its own). --probe-object-transplant 55/55, --probe-prop-catalog 12/12. Not checked: a push with a hull in the Blender session (no Blender in the run - the batch is driven directly, with no session); an imported object under a moved parent; the parking failure paths. * fix(mirror): the same bytes are not written again for an entry that cannot be copied b2f3b79 left one case: a texture whose bytes already match summer's but whose manifest entry differs from a summer entry that cannot be copied fell through to the write, so every mirror rewrote the same file and reported it as refreshed. Nothing is written and nothing is reported. --probe-season-mirror 28/28. The case itself has no probe: no stock archive has such an entry. * fix(import): a rename moves the collision record by whose it is, not by there being a namesake c5ec22a made the record follow a rename only when no other object bore the old name. That is the wrong question. An imported object renamed onto a stock object's name brought its record there, and the undo then left it behind: the old name had a second bearer. The same for an object renamed away from a name it shares - its collision stayed with the namesake. Whose the record is is now asked before the name changes, with the answer every move, delete and duplicate already goes by: the one bearer, or among several the one the record fits. The asking moved from the property catalog (which cannot see the collision layer) to PropertyEditController; the catalog's setter is the plain one again. An entry from before places were kept counts in the fit by how near its hull stands to the object; skipped, such a record fitted every bearer equally and went to the first in the file. Also: a click on the gizmo that moves nothing no longer brings an old record up to date and leaves an undo step with nothing to show for it. --probe-object-import-live 54/54 (6 new: onto a stock object's name and back, the namesake renamed away, the object renamed away, both undone), --probe-object-transplant 55/55, --probe-prop-catalog 12/12, --probe-editor-tools 23/23. Not exercised: two bearers of a name whose record has only old entries.
mafia2online#6 was stacked on this branch and was merged as one commit, so main already has every change made here. The merge takes main's tree as it is: nothing of this branch is left to add.
…the shipped memory requirements (#7) * feat: preview a material on the context mesh slot in the Material Editor Local work found uncommitted in the working copy (base a55dc22, v0.3.1): sphere/mesh preview toggle, the slot label on the assign panel, and the SDK pin moved to 10.0.302. * feat: turn a material made in Blender into a game material on push A push used to refuse any slot whose material had not come from the toolkit. A Blender-made material now travels with the image wired into its Principled Base Color and becomes a real game material: - addon: sends the image as RGBA8 (top-down, resampled to powers of two) once per push however many slots use it; the ack stamps the hash back on the datablock so later pushes send pixels only when they changed - DdsEncoder: DXT1 with a full MIP chain and the stock header, written as a single Texture entry (HasMIP 0) - the shape 2809 stock city textures ship in, so no companion Mipmap entry is needed - AuthoredMaterialResolver: fills in the hash before the mesh path runs; binds by name when the game already has the material, otherwise writes the texture into the object's archive (file + manifest) and creates a default-preset material with it in S000 (one undo entry) - a re-push with new pixels rewrites the texture in place and the renderer reloads it (TextureLibrary.Invalidate) Probe: --probe-bridge-material [district] [push.ilx] - encoder, resolver, re-push, bind-by-name, refusal, and optionally a container written by the addon itself. * fix: stop the Material Editor throwing while its XAML loads The Sphere toggle is checked in markup, so its Checked handler ran before the Mesh toggle and the preview existed and the window died with a NullReferenceException on open. The handler now waits for both. The probe read the assign BUTTON's visibility; the button is hidden through its panel since the mesh-preview change, so it reads the panel. --probe-material-editor: 65 passed, 0 failed (was 50 + an exception). * fix: live-test the Blender material push and close what it found A live probe drives the real viewport and a real bridge session (--probe-bridge-material-live): an off-screen D3DImageHost loads an archive, opens a mesh in a running bridge Blender and waits for an object made there, wearing a material made there, to be pushed back. It checks the scene, the renderer, a re-push with a changed image, undo/redo, Save, a reload from disk and a pack, then restores every file. 21 passed against Blender 5.1.2. What it found: - image resize went through Image.copy().scale(), which hands back the blank original of a painted or generated image - the texture arrived black. The addon now resamples the pixel array itself. - a rebuilt mesh's parts carried no material hash, so a later material edit (or a texture rewritten by a push) could not find them to re-resolve. The hash and tint now ride with the part, as on a mesh loaded from disk. - Blender keeps the hash of a material the library may have lost (creation undone, toolkit closed without Save). With pixels it is made again; without them the push is refused once and the ack tells the datablock to forget, so the next push arrives complete. * fix: store large textures the way the game does, and stop Build losing resources A Blender-made material came out BLACK in game. The object was there and the material was sound (same shader, flags and sampler states as 1888 stock materials) - the texture was not in a shape the game loads. - From 256x256 up the game stores a texture split: the top level alone in a Mipmap entry, everything from half resolution down in the Texture entry with HasMIP=1 (6747 split textures surveyed, no exception; no stock mip-chained texture is wider than 128 on its short side). The encoder wrote a 1024x512 texture whole. DdsEncoder.Encode now returns the pair, and ArchiveTextureWriter writes both files and both entries. Two packer bugs found while comparing the rebuilt archive with stock: - TextureHandler charged a split texture only its own payload. Stock charges the Texture entry its payload PLUS its MIP companion's and leaves the Mipmap entry at zero (381 of 381 in italy.sds), so every Build under-reported the archive's video memory by the size of all its top levels - 6 MB for italy. - PruneMissingEntries joined a rooted resource name ("/missions/...") with Path.Combine, looked for the file at the drive root, and unsaid a resource that was present. italy.sds lost its AudioSectors on Build. --probe-bridge-material: 30 passed (split layout, the memory figure against the files on disk, pruning with a rooted name). * fix: give a created material the two fields the stock ones carry The billboard was still black in game after its texture was stored correctly. The material had been compared with stock on shader, flags, sampler states and parameters - through a view that does not show every field. Two were left at zero by the Default preset: - Unk0: 128 on 1837 of the 1929 stock materials on that shader - the diffuse sampler's TexType: 2 on 1947 of 1951 (0 on four) Material_v57's Default preset now sets both, which also covers the materials the glTF import creates. v58 (Definitive Edition) is left alone: there is no DE install here to measure it against. The probe compares a created material with the majority of stock materials on its shader in these fields, so a preset that drifts from the game's own records fails here instead of in game. * fix: rename a probe local that clashed with one further down The previous commit did not build: the new stock-record check declared 'made', which the addon-container section of the same method already uses. --probe-bridge-material: 25 passed. * feat: carry a Blender material's normal and specular maps into the game A Blender-made material arrived with its Base Color image only. It now also brings the image behind its Normal Map node and the one driving its specular level, and becomes a normal-mapped game material. Chosen from a survey of the stock library, not assumed: - shader 5159568776351604322 (S000 + S001, parameter D013): 1209 stock materials, drawn on the P|N|T|UV0 declaration a pushed mesh already has. Its S001 is one combined texture - normal X and Y in red and green, the SPECULAR level in blue (stock "...NS" textures: red/green 127 +- 10, blue 30..240), DXT1 like 97 of the 120 sampled. - NormalSpecularPacker builds that texture from the two Blender images (either may be missing) and inverts green: Blender's normal maps have green up, the game's bitangent follows texture V, which runs down. - MaterialPreset.DiffuseNormal creates the record in the commonest stock shape of that shader - Unk0, flags, both samplers' TexType and states, D013 - and the probe compares it field by field with the majority of the 1209. - Blender's roughness and specular level become D013's power and level; the defaults (0.5 / 0.5) land on the commonest stock pair, 16 / 0.3. A material that gains or loses its normal map needs another shader, so it is replaced under the same name - the hash meshes refer to does not move. The resolver now talks to an IAuthoredMaterialHost (create, update, replace) instead of two callbacks; the application's host records each on the undo history. Also: binding a texture to a sampler sets its TexType to 2 (a slot added in the Material Editor started at 0, which samples nothing in game); two materials sharing an image share one texture file. --probe-bridge-material: 33 passed. Driven live through the real viewport against a windowless Blender: the billboard in italy took a new material with all three maps and shows the relief in Render mode. * style: dotnet format the preview viewport's mesh builder * feat(mcp): tools that drive the running map editor The MCP server could read game files but not touch the editor, so a client working on a scene had to fall back on UI automation for everything the window does. These tools close that gap: - editor_status / editor_list_areas / editor_open_area (opens the editor from the launcher and waits for the area to finish loading) - scene_find (by name, kind, or a world box tested against a mesh's triangles rather than its bounds) and scene_select - blender_open / blender_end, and editor_notices for what a push reported - editor_save / editor_build, object_move, scene_delete_selected, editor_undo / editor_redo - camera_get / camera_look_at / camera_set / camera_frame_selection, view_set (shading mode and layers) and viewport_screenshot Illusion.Mcp gets the IEditorSession seam; the application implements it over the open MainWindow. Supporting changes: ViewportControl.CaptureFrame, LookAt and LookFrom, DistrictStreamer.IsBusy, LauncherWindow.OpenMapEditor, and a notice log the banner feeds. * feat(mcp): blender_push, and scene_select with no names clears the selection blender_push sends the bridge's request_push, waits for the push to land and returns what the editor reported, so a client that edited the session's objects in Blender gets the outcome in the same call instead of polling the notice log. An empty scene_select clears the selection: its outline is drawn through walls and would otherwise sit in every screenshot. * fix(bridge): refuse a pushed mesh over 65535 vertices instead of writing 32-bit indices A rebuild that came to more than 65535 split vertices switched the level's index buffer to 32-bit. The toolkit's viewport draws that; the game does not — an interior of 65 981 vertices came up as triangles smeared across the district, textures flickering, while the editor showed it whole. The push is now refused with the vertex count, so the modeller can split the object. A rebuild always writes 16-bit indices, and a level that still carries a wide buffer from before is rebuilt on its next push whatever that push changed, which is what puts it right. scene_find reports each mesh's vertex and triangle count, so the size of an object can be checked before it is built into an archive. * feat: bring a light into a district that has none — actor import across archives A city district ships with no lights of its own: its interiors are lit by LightEntity actors, and the only way the editor could make an actor was to copy one already in the pack. ActorsFile.Import copies an actor out of another archive's pack, with its own copy of the behaviour row it points at; the editor wraps it as one undoable edit and gives it a row in the tree, creating the entity type's section when the district never had one. Only an actor that places no object of its own scene can travel — a light, a sound — and the copy is linked to a frame named after itself, which is the frame such an entity makes. A light keeps its transform twice: in the actor record and at the head of its behaviour blob (every shipped light's x appears exactly twice). The record is what the editor moves, so the pack now carries the translation over into the blob when it is written; an untouched pack still writes byte for byte. MCP: actor_import, scene_duplicate_selected, object_properties and object_set_property (the property panel as data, undoable writes), and decode_actors can list each actor's behaviour fields. * Keep a light's inverse matrix and clip box in step with where it stands A light's behaviour blob ends with the inverse of its world matrix, and the game works the light out through it. A light imported from another archive (or moved in the editor) kept the old inverse: every field read correctly, the light stood in the right place, and it lit nothing. SyncLightFrame now rewrites the inverse whenever it is out of step with the head matrix, carries the clip box along by the distance the light moved, and runs on import as well as on write. Untouched packs still write byte for byte: shipped lights are within 1e-4 of a true inverse and are left alone. * Show and edit a mesh's draw distance in the property panel Each level of detail carries the distance at which it stops being drawn, stored squared. It is the one cost control a static object has - the game culls by distance and view cone only - and an object made through the Blender bridge starts at a million metres. The panel (and so the MCP object_set_property) now lists one draw distance per level, in metres. * fix: touch a light's row only when the light has moved The light sync rewrote any inverse matrix that was out of step with the head matrix. The install has 72 such rows that shipped that way (three Joe's Adventures archives carry zeroes or noise there), so re-saving those untouched packs changed them: --probe-act-relayout, --probe-actor-props and --probe-native-misc fell to 1047/1050. Now a light whose record and head translation agree is left alone, and a moved light gets a new inverse only if the one it had was a true inverse. The clip box and inverse are also written to the row's named fields: on write the core pokes every named field back over the blob, which put the imported light's old box straight back. --probe-act-relayout gains the import: a light from one archive into a pack of another, checking its own row, matrix, inverse, clip box, the writer round trip and a refused duplicate name. 1050/1050 packs re-save byte for byte again. * docs: the editor tools, the Blender material push and their limits README: the MCP section lists the 25 editor tools and says how they relate to the editor's own undo, save and build; the bridge section covers pushed materials and the 65535-vertex refusal; the property panel mentions draw distances; Known limits gains opaque-only pushed materials and undrawn light actors. --probe-mcp now expects the editor tools as well. * feat: carry a Blender material's alpha into the game - cut-out and translucent A material made in Blender was always opaque: its diffuse went to DXT1 and its flags were the plain ones. Now whatever feeds the Principled BSDF's Alpha - the image's own alpha, a mask image, or a value below 1 - is written into the diffuse texture, which is then stored as DXT5, and the material's render method picks the mode: Blended is a translucent surface, anything else a cut-out. The game has no separate shader for either on the shaders the bridge creates materials for; the flag word decides. Measured over default.mtl on the diffuse shader: 239 materials set Alpha and keep writing depth (fences, signs, foliage), 323 set Disable_ZWriting on DXT5/DXT3 textures (overlays), 11 carry both. A cut-out gets Alpha and keeps CastShadows; a blend gets Disable_ZWriting and drops CastShadows; every other bit is left alone. - DdsEncoder: DXT5 (interpolated alpha block + the DXT1 colour block), with the header stock DXT5 files carry. - MafiaMaterialCatalog: GetFlags / SetFlags; the editor's SetFlags is an undoable edit. - The addon sends alphaMode with the material and puts the alpha it means into the image; an opaque material keeps its old signature, so nothing is resent for nothing. - --probe-bridge-material: 40/0, with the encoder's alpha round trip, the flag words, and one material taken through cut-out, blend and back. * feat(viewport): draw alpha-blended materials blended The mesh shader alpha-tested every textured material at one half. That is right for a cut-out and wrong for glass: a pane whose alpha is 0.3 was clipped away entirely, so a translucent material - a stock one or one pushed from Blender - was simply not there in the viewport. A part now knows whether its material blends (Disable_ZWriting in the flag word), and such parts are drawn in a pass of their own after the opaque one: depth-tested, never depth-written, standard alpha blend, no clip. The flag follows a material edit the same way the textures and the tint do. Unsorted on purpose - a pane seen through a pane may composite in the wrong order. Instanced props keep the alpha test. * feat: mirror a district's edits into its winter archive A district ships twice and an edit made to <name>.sds is simply absent from <name>_z.sds. For most districts the two are one scene: measured on uppertown, every shared resource (buffer pools, name table, actors, collisions, item descriptions, prefab) is byte for byte the same, and the frame resource has the same length and differs in 1740 places - each a material hash swapped for its ^zima counterpart, 82 pairs. Textures aside, nothing else differs. SeasonMirror writes the summer working copy over the winter one with each object's winter materials taken from the winter object of the same name (and occurrence - names repeat), copies the shared files that differ, adds the pools summer gained with their manifest entries, and adds the textures the result names that winter lacks. The caller queues the winter archive for Build. Not every district is such a pair: 13 of 23 are, ten have a winter scene of their own. The pair is therefore judged first on the archives as shipped (each one's oldest backup, or the archive itself if never built), and a district that fails is refused rather than overwritten. - MCP: editor_mirror_winter. - --probe-season-mirror: 11/0 on scratch folders built from one working copy, so it does not depend on what the modder did to the install - an untouched pair is left alone, a missing object arrives and winter is byte for byte whole again, a stranger is refused untouched; reports which of the install's pairs are twins. * feat: carry objects from one archive into another A district can now be furnished from the rest of the game: a door from a shop, a chair or a plant from an interior, brought into the loaded area as one undoable edit (MCP object_import). - FrameTransplant copies a frame subtree across resources: frames re-homed (FrameEntry.MoveTo), geometry and material blocks deep-copied once per shared block, every buffer they draw from copied into the district's own pools under fresh names. Links inside the subtree follow the copies; the root becomes a prototype (an actor places it) or scenery anchored to the district's main scene, on the name table, standing where it is put. - ActorsFile.Import takes an actor that places an object once that object has been carried, links it by the copy's name through a scene reference of its own, and can turn it. - ArchiveCarry brings what the scene does not hold: the textures the materials name, the item descriptions the collision frames name, and the prefab entry the definition names (PrefabFile.Adopt, which keeps the container's size header right - the core writes it as it is handed it). Keys travel unchanged; what the district already has is shared. - ObjectImportController shows the copy (tree rows, GPU meshes, actor row) and undoes it as one edit. - A frame named by hash alone (weapon parts) kept a zero hash when copied: the copy constructor rebuilt the name from its empty string. Fixed for the duplicator and the prototype cloner too. --probe-object-transplant: 30/0 - a door, a prop an actor places and a piece of scenery go from shops/harry.sds into a scratch copy of a district, keep their shape frame for frame and their buffers byte for byte, survive a save and a reload (actors resolve, meshes decode from the district's pools, prefab and item descriptions in the manifest), and an undo leaves the scene byte for byte as it was. * feat: a Props tab to drag stock objects into a district The map editor's property rail gets a Props tab: the stock game's doors, seating, tables, beds, storage, plants, lamps and decor, one card per object with a picture. Drag a card onto the viewport to put the object where it lands (on the scene's surface under the cursor), or double-click it to put it in front of the camera. - PropCatalog finds them in the extracted interiors and districts: every actor that places an object of its archive (door, physics prop, framed furniture), and scenery whose name says what it is and whose size is a piece of furniture. One card per object across archives; interiors are read first, so a card points at the stock source. The scan (~6 s) is remembered per archive and redone only for what changed. - PropThumbnailRenderer draws each card's picture offscreen with the object's own textures and keeps it on disk. - Scenery now brings collision: the source's hulls that stand inside its footprint, re-placed by the same move (CollisionCarry), or one cooked from its triangles when it had none - small props usually have none. - A door carried out of its building gets ActorActionsEnabled: a shop's front door ships with the player's actions off, its shop script turns them on, and nothing does that where it lands. - The import core moved from the MCP session into ObjectImportController, shared by object_import and the tab. A card from the loaded district is duplicated in place rather than refused. - RefManager hands out ids atomically: the catalog parses scenes in the background while the editor loads districts. --probe-prop-catalog: 7/0 - 2747 props in 6.3 s, 52 ms remembered, known doors and chairs on their shelves, each shelf's first card gets a picture. * feat: simplified hulls for imported scenery, and a props column - A hull cooked for scenery that had none of its own is now its convex hull by default (ConvexHull: incremental, points snapped to a grid a fortieth of the object's size) - a coat rack goes from 1474 collision triangles to 22. Also a box (12 triangles) or every render triangle, chosen per import: object_import's 'collision' (auto / convex / box / mesh / none) and a chooser in the props panel. A flat thing has no convex hull and gets a box. - The props panel moves out of the property rail into a column of its own left of the viewport: as tall as the window, sized with a splitter, shown and hidden by a Props button on the toolbar; the viewport's tool shelf moves with the viewport and is no longer covered. - Textures an import carries are written down beside the working copy (illusion_carried.json, outside the manifest) and each save sweeps the ones no material of the scene names any more - an undone import, or one lost with a scene closed unsaved, no longer leaves megabytes behind. --probe-object-transplant: 33/0 (+ the hull of a cube is the cube, a stock model's hull holds all its vertices and faces out, a flat thing gets a box). * feat: an imported object's collision moves and is deleted with it Nothing in an archive ties a static object to its collision - the hull is a placement that happens to stand where the object stands. For scenery the toolkit carried in, the tie is now written down beside the working copy (ImportLinks, illusion_links.json, outside the manifest): the object's name and the hashes of the hulls it was given. A gizmo drag of the object drags those placements along (a scale mints a scaled hull as it does for any placement), and Delete takes them with it; a Delete that went through several editors is one Ctrl+Z (EditHistory.SquashSince). Stock objects are untouched: nothing ties them, and guessing by position would take a building's hull along with a bench. * feat: a duplicate of an imported object gets copies of its collision Duplicating scenery the toolkit carried in also places copies of the hulls it was given, where the copy stands, links them to the copy under its own name, and the whole duplicate is one Ctrl+Z. Linked hulls are now claimed one to one across the scene, nearest first and ties to the name that sorts first: a copy stands on top of its original with the very same hull, and dragging it used to drag the original's hull away. * feat: save drops geometry nothing draws; a re-import shares geometry - Save writes the buffer pools without the buffers no geometry block draws from - left behind by a deleted object, a sanitised block, a rebuilt mesh. Measured: none of the 130 archives as shipped carries one (--probe-geometry-sweep), the install's working copies had ~0.5 MB. Only the files lose them: memory keeps them, so an undo after a save brings the object back and the next save writes them again (SdsGeometrySaver.SavePools tracks what each pool file holds). - Importing an object again draws from the buffers - and, when every level is there, the geometry block - its earlier import brought, the way the shipped scenes put many copies of one chair on one mesh. Where geometry went is remembered per source archive beside the working copy (ImportGeometry, illusion_geometry.json); a remembered buffer the scene no longer has is simply copied again. - The bridge probes snapshotted only the pool they edited; a save now may rewrite others, so they snapshot and restore every pool. --probe-object-transplant: 38/0 (+ imported twice shares, the memory survives on disk, undoing copies keeps the first, a save leaves dead buffers out of the files and writes them back when drawn again). * feat(mcp): drive the resource editor; read and set car tuning - editor_target switches the scene tools between the map editor and the resource editor's stage: find, select, properties, move, duplicate, delete, camera, screenshot, shading, save, build, undo/redo and the Blender tools follow it, and answer with that editor's status. - resource_list searches the archive library (pc\sds by name or folder), resource_open stages one - from the launcher the way its tile does, beside the map editor as a second window - and waits for it to load; resource_status reports what is on the stage. - car_tuning lists a car's entity-data tables (stock and tuned variants, labelled by mass and power) and their fields by band and element; car_tuning_set writes one, parsed by its kind, with the Tuning tab's bookkeeping: an undo entry, the archive on the build list, the tab re-read. A wheel's or gear's field can be named without its prefix together with its element. Tried on shubert_38: 6 tables x 771 fields; Power, CenterOfMass and Wheel0.Scale set, read back and undone. --probe-mcp: 66/0. * feat(cars): clone a car under a new name; a Build no longer drops XML - car_clone (MCP) / CarCloner: copies pc\sds\cars\<source>.sds and its winter twin to <name>.sds and renames what the name keys inside - the root frame and its actor link (and so the name table), the PREFAB entry (FNV64 of the name) and the entity-data storage (FNV64 of the name in lower case). Then registers the car: a vehicles.tbl row under a fresh id with the source's class, price and flags, its PaintCombinations row, its AiProps cover points, and - optionally - a slot in every traffic row (CARM*.tbl) that picks the source. Builds the new archives plus tables.sds and ingame.sds, backups kept. - GameTable: an extracted .tbl as rows of cells, written back through the table codec. PrefabFile.Rekey, EntityDataStorageFile.Hash. - Fix: PruneMissingEntries looked for an XML resource under its manifest name, but it sits on disk as name + ".xml" - so the first Build of an archive with XML resources (ingame.sds, tables.sds) unsaid every one of them, and the Script containers with them. XML is now found by its real file, containers are left alone. --probe-car-clone: 33/0 (on scratch copies: refusals write nothing, the rows and hashes the clone gains, the archives pack, nothing is pruned, every XML of both table archives decompiles to the same text after a pack). --probe-mcp 66/0, --probe-object-transplant 38/0. * feat(mcp): archive_build packs one archive's working copy For an edit made in the working copy itself - a script, a table file - that no editor session tracks: packs the extracted folder back into its .sds through SdsWriter.PackSds, keeping the usual timestamped backup. editor_build stays the way to pack what the editors changed. Used on pc\dlcs\cnt_jimmys_vendetta\sds_ru\missionscript\ fr_game_director.sds after a script edit: 8 resources before and after, only the script pack differs (+301 bytes, the edit). --probe-mcp: 66/0. * fix: a rebuilt name table keeps the order it was loaded in A cloned car sat in the garage with its name and no model. Two causes, both found against the game: - The name table was rebuilt in frame-index order. The order is authored, not derivable: every one of the 103 cars the game ships lists its root frame first, and only 34 happen to be in index order. The clone's table led with an effects frame. Each frame now carries the position the loaded table gave it (FrameNameTableOrder, set where the flags are linked), and BuildDataFromResource emits in that order, frames the table never listed after them by index. This is every rebuild, the Resource Editor's car save included. - The game finds archives through its cached file list (%LOCALAPPDATA%\2K Games\Mafia II\Data\vfs.bin) and does not pick up a NEW archive on its own. GameFileIndex.Reset removes the cache, the way Steam's install script does; car_clone calls it and says so. --probe-car-clone: 36/0 - the clone lists its root first; rebuilding the name table of each of 103 shipped cars reproduces it byte for byte (five tables that list a frame twice or name a frame the resource does not hold are left out). --probe-nametable: 1331/1331 semantic, 960 byte-identical. --probe-object-transplant 38/0, --probe-mcp 66/0. Whether the clone now shows in game is not yet confirmed. * feat(cars): a clone gets buffers and a title of its own - Buffers. The game keeps vertex and index buffers by name across every archive it has loaded, so a clone that kept the source's names would draw whichever shape streamed first once its model is edited - and so would the source car. Cloning now renames every buffer the geometry draws from, in the geometry blocks and in the pool files ("Shubert_38.Root.L0.VB0" -> "<name>.Root.L0.VB0"; a name that does not start with the model's gets the new one in front). - Title. car_clone takes an optional title: one new string in the text table of every installed language (sds_<lang>\text\text_default.sds, tables\TextDatabase.dat - UTF-8 "KEY:TEXT" lines, id 60000017 is key 00_60_00_0017), under an id from 60001000 + vehicle id that nothing uses, and the clone's vehicles.tbl row points at it. Without a title the clone shares the source car's name, as before. GameText reads and adds lines; the text archive is built with the others. --probe-car-clone: 48/0 (the clone's buffers share no name with the source's, every level of detail resolves, the bytes are the source's in the source's order; the title is one added line and nothing else, the text archive packs, its XML survives the pack, nothing is pruned). In game: the first clone showed in the garage with its model after the name-table fix; this build of it is confirmed to start, not yet looked at in the garage. * Packer keeps shipped memory requirements; export a car as an M2O resource A rebuilt archive stated payload sizes where the shipped one stated what the engine should budget (a car asked for 148 079 bytes of slot RAM against 170 232, and no 'other' RAM). The figures are now read from the archive as it shipped, kept beside the working copy (illusion_memory.json) and applied on pack; a clone carries its source car's. archive_build takes memoryFrom for a clone built before that. car_export_m2o writes a resource folder for Mafia II Online: package.json, cars/<name>.sds (+ _z) and vehicles.json with the model, titles, source car, key hashes and the vehicle/paint table rows. Probes: --probe-car-clone (memory), --probe-car-m2o. * feat(cars): build a car under another car's name car_substitute replaces <target>.sds with the source car's model keyed by the target's model name (root frame, name table, prefab entry, entity data, buffers), keeps a backup and touches no table - for trying a car where models are spawned from a fixed list of names. * feat(cars): the M2O car export follows the map export's layout; docs Archives sit under sds/cars/ in the exported folder and each entry names the path the game loads it from (sds, winterSds), the way the map export names its patch targets. README: the car tools, memory requirements, the tool count. * perf(bridge): read a level's index width without decoding the mesh Review of #3. HasWideIndices decoded the whole level - every vertex channel, the index array and, on a model, the skin - to read one field of its index buffer, on every push, ahead of a path that decodes the mesh again. It now clamps the level and looks at the index buffer directly. --probe-bridge-rebuild 17/17, --probe-bridge-vertex 2/2, --probe-bridge-resplit 6/6. * fix(mcp): editor tools say no when the editor does, and find crash copies from the table Review of #3, the findings in the MCP session. editor_open_area dropped unsaved edits. Changing the area or the season reloads the scene, which clears the edited frames, the unsaved list and the undo history, and the tool only checked for a Blender session. A load that would replace a scene with unsaved edits is now refused unless the caller passes discardUnsavedEdits=true; asking for the area and season already shown stays a no-op. editor_save and editor_build reported success after a save that did not complete. The viewport's save does not throw for a material library that would not write or a car's working copy that was refused - it posts a notice and carries on - so the tool had nothing to return but success, and a build went on to pack archives whose materials were not on disk. D3DImageHost.SaveEditsReport hands back what was left unsaved; editor_save answers success=false with notSaved, and a build whose save did not complete packs nothing and says why. object_set_property could not rename. Base.Name is offered as an editable HashName and the parser had no case for it. It has one now, with the property panel's rule (an empty name is refused), and a rename to the name the object already has is not committed as an edit. Numeric properties took NaN and infinity. float.TryParse accepts "NaN", "Infinity" and "1e100" (as infinity); a float and each component of a vector now have to be finite. scene_find did not see crash copies until their row had been expanded in the tree, and a box query never returned one. They are now searched from the placement table: by the copy's own label or its prop's name, and by the prop's triangles at the copy's matrix. Only the copies returned get a tree node. --probe-editor-tools (new, no game install needed) drives the real tool methods against a scripted editor and the parser directly: 23 checks. Checked live in the editor on uppertown: 5 lamp copies found with no row expanded, 288 triangles of one in a box round it, none 400 m above it; a moved mesh blocks a load of another area and of the other season, the same area is a no-op, discardUnsavedEdits=true loads. A failing material write was not reproduced live - that path is covered by the scripted editor only. * fix(mirror): the winter mirror decides against the pair as shipped, not against the last mirror Review of #5, the four findings in SeasonMirror. Twins were told by size. Two archives passed as "one scene shipped twice" when their shared pools were equal and their scenes the same length - and a winter scene with one object moved 50 units is the same length. The scenes are now parsed, winter's material hashes replaced by summer's, and the two written back out: anything left that differs (a transform, a parent link, a draw distance) means winter is a scene of its own and the mirror is refused. The 13 stock twins still read as twins and the other ten as not. Everything else follows from reading that pair once (SeasonPair) and deciding against it, where the mirror used to ask the winter WORKING COPY what winter wore - which answers with whatever the previous mirror left: - A slot the modder re-pointed in summer got its old winter material back. A summer material no object of that name wore when the pair shipped is now carried into winter as it is (Report.Reassigned). - Namesakes were matched by position among objects of the same name, so deleting the first handed its materials to the second. The season's substitution is now looked up by name and SUMMER MATERIAL; position is only used when one summer material stands for two winter ones under one name, and only while the number of namesakes is what it shipped as. Otherwise the object keeps summer's material and is counted (Report.Ambiguous, raised as an error notice) rather than dressed in another object's snow. - A texture an earlier mirror had brought was never refreshed: the bridge repaints an authored texture in place, and "winter has a file of that name" ended the matter. A file winter did not SHIP with is now compared and rewritten, its MIP_ companion with it (and removed when the new picture has none); a file winter shipped with is the season's own and is left alone. Mirror() also refuses a winter folder that is not made of the pair's meshes before writing anything, and editor_mirror_winter stops when the save it starts with did not complete. Its result names slotsReassigned and objectsAmbiguous in place of objectsReshaped. --probe-season-mirror builds its own namesakes and runs every case above on scratch copies: 23/23 on uppertown and oysterbay, 22/22 on italy, 21/21 on greenfield (not a twin, so the shipped-texture checks do not apply). * fix(bridge): a material push that can be undone, refused cleanly and shared across archives Review of #4, all ten findings. Texture files are part of the push's undo. A repaint under the same name changes no binding, so the catalog recorded nothing and Ctrl+Z could not bring the old picture back. The resolver now journals every file it writes (what the name held before, what it holds now) and the push carries a TextureFilesEdit in its one history entry: undo restores the old files - or removes a texture the push introduced, manifest entries included - and redo puts the new ones back. Nothing is written before everything is checked. The diffuse texture was on disk before the normal map had been looked at, and before the library had accepted the material. All images are now read, validated and packed first; what is written before a later file or the library refuses is rolled back from the same journal. A split texture is compared as two files. PickName compared only the entry, which for a texture of 256 and up starts at half resolution: a fine checkerboard and the grey it averages to are byte-identical there (reproduced), so the second picture took the first one's name and its write replaced the first one's top level. The MIP_ companion - and whether there is one - is now part of the comparison. A material shared by two archives is packed into both. The second object of a push binds the new material by name, and an unchanged material arrives with no pixels; either way an archive that had never held its textures got none. The files are now copied from the archive that has them (only for materials this bridge created - a stock material bound by name copies nothing). A renamed material keeps its hash across a shader change. Gaining or losing a normal map deletes and recreates the record, and it was recreated under the Blender datablock's current name - a new hash, with every other mesh left pointing at the old one. It is rebuilt under the name the library knows it by. Reloads are coalesced. A push of a hundred objects sharing a material asked for a hundred invalidate-and-rebind passes over every loaded mesh; each texture is now invalidated once and each material that names one rebinds once (ReloadTextureFiles). A rebound part returns the textures it held. GpuMesh kept every lease until dispose, so the library's retired copy of each repainted texture stayed on the GPU for as long as the mesh was loaded. Leases are kept per part and the superseded ones released once the part holds the new ones. The mesh preview keeps the material's tint. Add-on: a Normal Map node in Object or World space is no longer exported as if it were tangent space - the material is refused by name with what to change - and the resampler clamps its two neighbours independently: upsampling [0, 1, 0] to four texels gave 0.875 at the left edge, now 0. --probe-bridge-material 58/58 on eastside (30 new checks: each case above, including twenty repaints of a texture a live GpuMesh draws leaving nothing retired), --probe-material-editor 65/65, --probe-bridge-rebuild 17/17, --probe-mcp 66/66. The undo entry itself was checked at the file level (restore before/after), not by pressing Ctrl+Z in the editor, and the two add-on changes were not run inside Blender: the resampler was checked on the reviewer's numbers outside it. * fix: the material preview blends what the scene blends; an alpha mask is sampled linear Review of #5, the two findings outside the mirror. The material editor's preview still alpha-tested a translucent material. SphereMesh forwards a part's Blended flag, but the part the preview builds for a material carried only its tint, and the mesh-shape preview dropped the flag again - so a uniform 0.3-alpha pane vanished from the ball while the scene drew it blended. The flag is read from the catalog with the tint and kept in both preview shapes. Add-on: a colour image used as an alpha mask was read through image_rgba8, which turns a picture into what an albedo texture stores (sRGB bytes), and the luminance of those encoded values was written as coverage. A float mask at linear 0.25 came out as 137/255 instead of 64 - enough to carry a cut-out across the 0.5 it is tested against. The mask is now read in the values the shader works with (_shader_pixels): float buffers as they are, 8-bit sRGB linearized as the Image Texture node does, Non-Color data and the alpha channel as stored. Run in a background Blender on the add-on as it stands in this branch: linear 0.25 float mask -> 64, 8-bit sRGB 0.5 -> 55, Non-Color 0.5 -> 128, alpha channel 0.3 -> 77; the same run covers the two add-on fixes merged from #4 (Normal Map node space, resampler edge). The preview change was not looked at in the material editor window. * test: the scripted editor answers the resource status the tools now ask for --probe-editor-tools drives the tool methods against a stand-in editor. Since the resource editor became a target, a tool reads ResourceStatus to learn which editor it is reporting on; the stand-in had no answer and the save cases stopped there. It now hands back an empty status, which names no target, so the map's status is used. 23/23. * fix(cars): a clone or a substitution that fails takes itself back; resource_open stages what was asked for Found reviewing this branch the way #3-#5 were reviewed. car_clone deleted a working copy before it had checked anything. A folder under the new name with no archive beside it was removed as "left behind by an attempt that never produced its archive" - ahead of the checks on the title and the vehicle table, so a clone refused a moment later had already cost whatever the folder held (a parked car, work never built). Such a folder is now a refusal that names it, and nothing is deleted. Clone and substitute wrote several game files with no way back. A clone adds rows to shared working copies and then packs five or more archives; any pack failing (the game holding a file, a full disk) left a vehicle row with no archive, tables written and not packed, and a name that was now "taken", so the clone could not be tried again. A substitution that failed on the winter archive left summer substituted and winter stock, with the target's working copy already deleted. Both now keep a GameWriteJournal: rewritten files get their bytes back, new files and folders are removed, a replaced archive is restored from the backup taken of it a moment earlier (and that backup dropped), and a working copy that has to make way is moved aside until the operation has succeeded. The refusal says the operation was taken back and names anything that could not be. resource_open answered success with the previous archive on the stage. The resource editor's catalog is a walk of the game folder made when the window opens; an archive made since - a car cloned a moment ago, which is the workflow car_clone recommends - was never found, the request waited for good, and the tool's "something is loaded" test was met by the car already there. car_tuning_set then wrote into that car. The window now walks again for an archive it does not know (once per archive), and the tool waits for the archive it asked for and fails, naming what the stage shows, when it does not arrive. An archive with nothing to draw no longer runs out the whole timeout. --probe-car-clone 73/73 (8 new: every kind of journal entry undone on scratch files). --probe-car-clone-rollback (new; unlike the others it WRITES to the install and restores it): the real Clone with the winter pack blocked - refused onto an existing working copy without touching it; after the failure the summer archive, both folders, vehicles.tbl, the paint and traffic tables, cover points, manifest and text are as they were, tables.sds and ingame.sds unpacked with no backup left, and the same clone can be tried again. 11/11. resource_open checked live: a car archive copied into pc\sds\cars after the window opened is staged, by path and by bare name. The substitution's rollback is covered by the journal checks only, not by a staged failure against the install. * fix(import): a failed import leaves nothing behind; redo gets its shared block and its textures back Found reviewing this branch the way #3-#5 were reviewed. An import that failed after the frames were copied left them in the scene. The copy is made first, then its textures are carried, its rows built and its collision read; a failure in any of those answered "the import failed" with the frames still in the resource - in the next save, with no row in the tree to delete them by and no undo entry. The catch also named five exception types, so a collision file that does not decode went past it as a tool error, same leftovers. Import now takes the copy back out when it fails before the edit is on the undo stack, catches whatever is thrown, and says in the refusal that nothing was left. Edits of one import that were already applied when a later one throws are undone again; an actor already in the pack is removed; GPU meshes made for rows that will never be shown are released. A failure AFTER the edit is registered (writing the note that lets the next import share geometry) no longer reports the import as failed. Redo of a copy that shares a geometry block threw on the next save. A second import of the same object draws from the first one's block and lists none of its own. With the first copy deleted and the second import undone, a save prunes the block; Reattach put back only the blocks the copy lists, so the redone mesh named a block the resource did not have and the save after it died in UpdateFrameData (KeyNotFoundException). Reattach now also re-registers what its meshes point at, as DetachedFrames does. A save swept textures that undo could still bring back into use. Carried textures no material of the scene names are removed at save - but the import can be redone and a deleted object's delete undone, and neither carries anything again: the object came back naming textures that were gone from the working copy and its manifest. Swept textures are now parked - out of the manifest, into illusion_parked\ under a name no scan for *.dds answers to, with their manifest entries kept field for field - and the next sweep that finds the scene naming one puts it back. What an earlier run of the program parked is dropped at the first save of the next; a texture carried again while parked wins over the parked copy. A file that cannot be moved leaves the texture where it is for the next save instead of failing the save. --probe-object-transplant hill shops\harry.sds 50/50 (12 new): sweep with the objects in / undone / saved twice / redone (same bytes, same manifest entry, MIP companion with it) / swept again / carried again while parked / left by an ended run; the shared block after delete + undo + save + redo, saved and read back. Without the Reattach change the same probe dies with the KeyNotFoundException. Live, against the editor: a source whose collision file is cut short - before the change every later import under that name answers "already taken"; after it the same refusal twice and no rows. A scenery import (250 hulls) and an actor import still work, with undo, redo, undo. Not checked: the failure paths inside ImportPlaced after the pack took the actor, and a failing GPU mesh. * test(import): the carried-again check asks for the textures, not for their count Higher in the stack a texture can also be borrowed from a third archive, so the second carry brings more than the three objects brought the first time. * fix(import): the lesser findings of the self-review - carry, links, names, numbers, thumbnails The second half of the review of this branch. One commit because the pieces lean on each other; by finding: Carry. A refused or failed import now takes its carry back: the working copy's manifest, prefab containers and carried register are noted before the carry and restored, and the files the manifest gained are removed (ArchiveCarry.Note / TakeBack). It used to leave textures, item descriptions and a prefab entry behind for an object that never arrived - the pack refuses an actor only after the carry. The texture a mesh names itself (OMTextureHash) is carried, and counted as in use by the save-time sweep. A MIP companion is copied before the texture that needs it, and a texture whose companion cannot be brought is not brought. RemoveEntry writes the manifest through a temp file, as AddEntry did. Object-to-hull link. The hulls an imported object was given are found by distance to the object's box, not its pivot - a district mesh's pivot can be metres from its geometry, and its hulls were then never linked. A move typed into the Transform panel takes the hulls along (it went through RecordTransform, which knew nothing of them; past 5 m they were lost for good and a later delete left them in the .col). Renaming the object moves its record. The record is written by an edit of the import (and of the duplicate), so undo takes it back out of the file. Links are worked out once per operation for the whole selection instead of once per node, each time re-reading the file and walking every object and placement. Source. object_import takes `occurrence`: the n-th thing of that name in the source (actors first, then frames - the ones that draw before helpers of the same name), and reports how many there are. Position and heading must be finite. The source archive is kept between imports from it (let go after a minute, re-read when its working copy changed) instead of being read whole for every object. "N hull(s) of its own" is now worded as what it is - the hulls standing inside its box - in the result and in the tool description. Prop thumbnails. The archive is read on a pool thread (Stage) and only drawn on the UI thread (Draw); the tick is guarded, since nothing above a DispatcherTimer catches; the kept picture is keyed by the working copy's time stamp as well and replaces the older ones; the held scene is let go when the tab has all its pictures; texture folders no longer accumulate across archives (TextureLibrary.ClearFolders). --probe-object-transplant 53/53 (the take-back on a scratch copy; an OM-named texture through carry, sweep, park and return). --probe-object-import-live (new; a real window with `hill` loaded, the district's working copy compared with its state before and put back): 30/30 - typed move and turn with undo/redo, drag, rename and its undo, delete and its undo, everything undone leaves no record, occurrence past the end, a source whose collision is cut short refused twice with the same answer, a door the pack refuses leaving manifest and folder as they were. --probe-prop-catalog 9/9, --probe-editor-tools 23/23. Not checked: the Props tab filling in a window (only Stage/Draw are), a companion that fails to copy, hulls linked through the box on an object whose pivot really is far off (the probe's object has it close). Not changed: "its own hulls" are still chosen by position - an archive has nothing else to go by; item descriptions and the prefab entry of an UNDONE import still stay in the working copy. * fix(cars): the lesser findings of the self-review - target, notices, tuning values, builds that drop entries The second half of the review of this branch, by finding: Notices and Blender on the resource editor. Its notices went to its own banner only, while editor_notices and the Blender tools read the application's log: blender_push answered "no push arrived" for a push that had landed, and the line car_tuning_set posts was lost. The window now writes the log as the map editor does. blender_open counted the MAP's Blender objects whatever the target and ran out its timeout on a session opened in the resource editor; it asks the target. Target. editor_open_area, object_import, actor_import and editor_mirror_winter make the map the target, as resource_open makes the resource editor: left where it was, the find/select/delete/save/build after an editor_open_area went on acting on the car on the stage. car_tuning_set makes the resource editor the target - its undo entry and build list are that editor's. EditorStatus says which (Target). Open editors. car_clone and car_substitute are made from the working copy on disk: a source open in an editor with unsaved edits is refused. A substitution whose target is loaded in an editor is refused - its working copy is replaced under that editor. resource_open of the archive already on the stage loads nothing (a reload emptied the undo history), and the two cases the window answers with a dialog - an archive another editor has loaded, a save that fails - are answered before it is asked: a tool call that waits for a click never returns. Re-keying. No root frame of the model's name, no prefab entry, entity data filed under another name, no buffers: each was a note while the archive was packed and the car registered. They are failures; the clone or the substitution is taken back. Tuning values. A float that is not finite and an integer wider than its field are refused where the value is written (TuningEditing.Set), so the Tuning tab is covered too. Dropped entries. PackSds removes manifest entries whose file is missing and said nothing. PackResult.Dropped carries them: both editors post them as an error notice (never silenced), editor_build and the clone list them, and archive_build - the tool for hand edits - refuses such a build unless dropMissing is passed, since the entry is gone from the manifest for good. MissingEntries asks without packing. Said, not changed: a clone is registered in the main game's tables only - the three story DLCs ship an ingame.sds of their own, and traffic is added to CARM* tables only; the result's notes name both. The long file work of clone/substitute/build still runs on the UI thread (it keeps the editors from touching the files meanwhile); what was cut is the scratch extraction MemoryFor repeated on every build of an archive that has no shipped figures. --probe-car-clone 75/75 (a car not keyed by the name given is a failure; a working copy short of a file says which). --probe-car-clone-rollback 11/11, --probe-editor-tools 23/23, --probe-object-import-live 30/30. Live, on a probe copy of jeep.sds removed afterwards: NaN, Infinity, 1e40 and an integer past its field refused; the tuning notice in the log; the same car opened twice keeps undo; the target after editor_open_area and after car_tuning_set; resource_open of the district the map has loaded answered at once; clone and substitute refused against the open car; archive_build refused, then built with dropMissing. Not checked: blender_open/blender_push with the resource editor as the target (no Blender in the run), the Build dialogs' new line in a window. * fix(import): review of #6 - links by place, blocks shared only when alike, the Props tab paged What the review of this branch found that the commits before this one had not already answered (the failed-import rollback, the swept textures and the numeric-edit path are in 56f5f8d and 45236da). A collision link is a placement, found by its place. The record kept one hash per hull: two placements of the same hull collapsed into one entry (Distinct), a resize re-cooked the hull under a new hash the record did not learn, and a placement was looked for within 5 m of the object's pivot, which says nothing about where a placement's origin is. Each entry is now one placement - the hull and where it stood in the object's own space when the link was made - and it is found where the object's matrix now puts that point, which every move, turn and resize the two make together leaves unchanged. The same hull twice is two entries; a resize rewrites the hashes in the record as an edit inside the same undo step; a record written before this (bare hashes) is read as before and matched by nearness to the object's box. A geometry block is shared only when it draws the same way. Matching buffers and decompression values was taken as "the same block", so a second import inherited whatever had been done to the first copy's block since - a draw distance set to 0, and an object that does not draw. The draw distance, vertex layout and count and both capsules are compared (FrameLOD.DrawsLike); the buffers are still shared, the block is copied when it differs. The Props tab makes cards a page at a time. A wrapping panel does not virtualize, and the whole catalog - 2,900 objects here - was given cards at once, about two seconds of the UI thread on opening and on every filter change. 120 cards, and the next 120 when the end of them comes within reach; pictures go to the cards in view first. The picture timer, stopped when the tab is hidden, starts again when it is shown - a catalog already loaded had nothing to restart it. --probe-object-import-live 36/36 (6 new: a resize gives the hull a new hash and the record follows, undo puts both back; a second placement of the same hull nine metres from the pivot is the object's too, and both go with it). --probe-object-transplant 54/54 (a copy whose draw distance was changed does not lend its block). --probe-prop-catalog 12/12 (the tab in a 330x700 window: 120 cards for 2928 objects, 240 after scrolling to the end, pictures resume after hide and show). Not checked: a record in the old format on a real working copy. * fix(cars): review of #7 - the scratch clone takes itself back, manifests that do not read, undo re-enlists the archive What the review of this branch found that c4b33d4 and 64b4e9b had not already answered (the rollback of Clone and its packs, the stale catalog behind resource_open, the modal dialog on the tool path and the non-finite tuning numbers are in those). CloneExtracted is whole or nothing by itself. The rollback lived in Clone; the clone on working copies, called on its own, still wrote the vehicle row before the title, the paint row and the traffic slots, and a failure at any of them left a car registered by half and its name taken. It now keeps the journal itself (Clone passes its own in, to add the packs to it). The journal no longer tries to put back a file that was noted and never written - with the text table held open, that was the one thing the undo reported it could not restore. The M2O export does not write over a manifest it cannot read. An existing vehicles.json or package.json that does not parse was taken for 'none yet' and replaced with this one car - after the archives had been copied. Both are read before anything is written; one that is there and does not read is a refusal that leaves the folder as it was. Undo and redo of a value that lives in the working copy put the archive back on the build list (WorkingCopyEdit, for tuning and effect edits, in the panel and in car_tuning_set). A Build takes the archive off the list; an undo after it changed the working copy and the next Build packed nothing, leaving the game with the value that had been undone. --probe-car-clone 77/77 (a clone whose text table is held open is refused and leaves the vehicle table, the ingame tables, the text and the folders as they were; the same clone then succeeds). --probe-car-m2o 24/24 (a vehicles.json, then a package.json, that does not read: refused, both files and the folder untouched). --probe-car-clone-rollback 11/11. Live, on a probe copy of jeep.sds: set Mass, build, undo - the archive is pending again; redo - still pending. * fix(props): the picture cache of an earlier build is left alone 45236da gave a prop's kept picture a name that carries the state of its archive, and on first use deleted every picture under the old name as something nothing reads. A build from before that change, in use beside this one on the same folder, reads exactly those: its Props tab lost all 2,928 pictures the first time a probe of this branch ran. Only a prop's own pictures from earlier states of its archive are dropped now. --probe-prop-catalog 12/12; 2,928 old-format pictures still there after it. * fix(bridge): what a second look at 838c69a found 838c69a made the texture files of a push part of its undo and put a refused material's files back. Read again before it went up: Undo removed a texture the push had introduced. The material that names it is created by an edit of its own and outlives the push's entry, and Blender - told the push arrived - sends pixels again only when the image changes: after one Ctrl+Z the texture was in no archive, and the next push put the material on the object with nothing to draw it from. The undo entry takes back pictures that were REPLACED; an introduced one stays with its material. Undo and redo did not put the archives back on the build list. Push, Build, Ctrl+Z: the folder had the old picture, the game's archive the new one, and Build answered that there was nothing to build. A push that only repainted a texture was dropped from the history by the first unload of any district: its entry names no objects, and 'all of its objects have left the scene' is true of none. A write that failed was remembered as done. The name was noted before the write; the next object of the push sharing the image was handed it as success and its material named a file that was never made. Noted after. A texture half written - the entry replaced and its top level, or the manifest, not - was caught with nothing on record to put it back from. It is put back before the failure is passed on, and neither that nor AtomicFile leaves its .tmp beside the target any more. An object is refused before any of its materials is touched: every slot is checked first for what can be refused without writing (normal map space, images that did not arrive whole). A bad second slot used to be found after the first slot's pictures had been replaced. A texture that cannot be copied into the object's archive refuses the object as well. --probe-bridge-material 61/61 (3 new: the two-slot object; a texture held open refuses both objects sharing the image and leaves the picture, the manifest and the folder as they were). --probe-material-editor 65/65. The undo entry itself (introduced texture kept, archives re-enlisted, the entry surviving an unload) is not covered by a probe - it lives behind the editor's history. * fix(mcp): a layer is not switched off over unsaved edits; moves and imports take finite numbers A second look at f12155b. editor_open_area was given a guard against dropping unsaved edits, and view_set was left without one: switching the crash (or collision) layer off unloads it, and an unsaved move of a crash copy went with it, undo entry included - the same loss by a shorter road, and an easier one to take now that scene_find hands out crash copies. Refused while the scene has unsaved edits, unless discardUnsavedEdits is passed; switching a layer on, or off with nothing unsaved, is as before. object_move and actor_import took a position or an offset as it came: a number too large for a float arrives as Infinity and was written into the transform. They refuse what is not finite, as property values already do. --probe-editor-tools-live (new; a real window with a district loaded, nothing saved) 12/12: both refusals of non-finite numbers; the layer off and on with nothing edited; a crash copy moved, the layer refused, zones still switch on, the layer off once the edits are given up. --probe-editor-tools 23/23, --probe-mcp 66/66. * fix(mirror): a second look at 3696630 and a3b5f73 Read again before they went up. A material block worn by two objects was settled twice. The mirror writes winter's hashes into the block, so the second wearer was resolved from them as if they were summer's - counted as a slot the modder had re-pointed, or substituted again where a winter hash is also a summer one. The wearers of each block are gathered first and the block is settled once, from summer's hashes, by whichever wearer the pair has an answer for. A mesh that existed only in winter …
…#9) * feat: preview a material on the context mesh slot in the Material Editor Local work found uncommitted in the working copy (base a55dc22, v0.3.1): sphere/mesh preview toggle, the slot label on the assign panel, and the SDK pin moved to 10.0.302. * feat: turn a material made in Blender into a game material on push A push used to refuse any slot whose material had not come from the toolkit. A Blender-made material now travels with the image wired into its Principled Base Color and becomes a real game material: - addon: sends the image as RGBA8 (top-down, resampled to powers of two) once per push however many slots use it; the ack stamps the hash back on the datablock so later pushes send pixels only when they changed - DdsEncoder: DXT1 with a full MIP chain and the stock header, written as a single Texture entry (HasMIP 0) - the shape 2809 stock city textures ship in, so no companion Mipmap entry is needed - AuthoredMaterialResolver: fills in the hash before the mesh path runs; binds by name when the game already has the material, otherwise writes the texture into the object's archive (file + manifest) and creates a default-preset material with it in S000 (one undo entry) - a re-push with new pixels rewrites the texture in place and the renderer reloads it (TextureLibrary.Invalidate) Probe: --probe-bridge-material [district] [push.ilx] - encoder, resolver, re-push, bind-by-name, refusal, and optionally a container written by the addon itself. * fix: stop the Material Editor throwing while its XAML loads The Sphere toggle is checked in markup, so its Checked handler ran before the Mesh toggle and the preview existed and the window died with a NullReferenceException on open. The handler now waits for both. The probe read the assign BUTTON's visibility; the button is hidden through its panel since the mesh-preview change, so it reads the panel. --probe-material-editor: 65 passed, 0 failed (was 50 + an exception). * fix: live-test the Blender material push and close what it found A live probe drives the real viewport and a real bridge session (--probe-bridge-material-live): an off-screen D3DImageHost loads an archive, opens a mesh in a running bridge Blender and waits for an object made there, wearing a material made there, to be pushed back. It checks the scene, the renderer, a re-push with a changed image, undo/redo, Save, a reload from disk and a pack, then restores every file. 21 passed against Blender 5.1.2. What it found: - image resize went through Image.copy().scale(), which hands back the blank original of a painted or generated image - the texture arrived black. The addon now resamples the pixel array itself. - a rebuilt mesh's parts carried no material hash, so a later material edit (or a texture rewritten by a push) could not find them to re-resolve. The hash and tint now ride with the part, as on a mesh loaded from disk. - Blender keeps the hash of a material the library may have lost (creation undone, toolkit closed without Save). With pixels it is made again; without them the push is refused once and the ack tells the datablock to forget, so the next push arrives complete. * fix: store large textures the way the game does, and stop Build losing resources A Blender-made material came out BLACK in game. The object was there and the material was sound (same shader, flags and sampler states as 1888 stock materials) - the texture was not in a shape the game loads. - From 256x256 up the game stores a texture split: the top level alone in a Mipmap entry, everything from half resolution down in the Texture entry with HasMIP=1 (6747 split textures surveyed, no exception; no stock mip-chained texture is wider than 128 on its short side). The encoder wrote a 1024x512 texture whole. DdsEncoder.Encode now returns the pair, and ArchiveTextureWriter writes both files and both entries. Two packer bugs found while comparing the rebuilt archive with stock: - TextureHandler charged a split texture only its own payload. Stock charges the Texture entry its payload PLUS its MIP companion's and leaves the Mipmap entry at zero (381 of 381 in italy.sds), so every Build under-reported the archive's video memory by the size of all its top levels - 6 MB for italy. - PruneMissingEntries joined a rooted resource name ("/missions/...") with Path.Combine, looked for the file at the drive root, and unsaid a resource that was present. italy.sds lost its AudioSectors on Build. --probe-bridge-material: 30 passed (split layout, the memory figure against the files on disk, pruning with a rooted name). * fix: give a created material the two fields the stock ones carry The billboard was still black in game after its texture was stored correctly. The material had been compared with stock on shader, flags, sampler states and parameters - through a view that does not show every field. Two were left at zero by the Default preset: - Unk0: 128 on 1837 of the 1929 stock materials on that shader - the diffuse sampler's TexType: 2 on 1947 of 1951 (0 on four) Material_v57's Default preset now sets both, which also covers the materials the glTF import creates. v58 (Definitive Edition) is left alone: there is no DE install here to measure it against. The probe compares a created material with the majority of stock materials on its shader in these fields, so a preset that drifts from the game's own records fails here instead of in game. * fix: rename a probe local that clashed with one further down The previous commit did not build: the new stock-record check declared 'made', which the addon-container section of the same method already uses. --probe-bridge-material: 25 passed. * feat: carry a Blender material's normal and specular maps into the game A Blender-made material arrived with its Base Color image only. It now also brings the image behind its Normal Map node and the one driving its specular level, and becomes a normal-mapped game material. Chosen from a survey of the stock library, not assumed: - shader 5159568776351604322 (S000 + S001, parameter D013): 1209 stock materials, drawn on the P|N|T|UV0 declaration a pushed mesh already has. Its S001 is one combined texture - normal X and Y in red and green, the SPECULAR level in blue (stock "...NS" textures: red/green 127 +- 10, blue 30..240), DXT1 like 97 of the 120 sampled. - NormalSpecularPacker builds that texture from the two Blender images (either may be missing) and inverts green: Blender's normal maps have green up, the game's bitangent follows texture V, which runs down. - MaterialPreset.DiffuseNormal creates the record in the commonest stock shape of that shader - Unk0, flags, both samplers' TexType and states, D013 - and the probe compares it field by field with the majority of the 1209. - Blender's roughness and specular level become D013's power and level; the defaults (0.5 / 0.5) land on the commonest stock pair, 16 / 0.3. A material that gains or loses its normal map needs another shader, so it is replaced under the same name - the hash meshes refer to does not move. The resolver now talks to an IAuthoredMaterialHost (create, update, replace) instead of two callbacks; the application's host records each on the undo history. Also: binding a texture to a sampler sets its TexType to 2 (a slot added in the Material Editor started at 0, which samples nothing in game); two materials sharing an image share one texture file. --probe-bridge-material: 33 passed. Driven live through the real viewport against a windowless Blender: the billboard in italy took a new material with all three maps and shows the relief in Render mode. * style: dotnet format the preview viewport's mesh builder * feat(mcp): tools that drive the running map editor The MCP server could read game files but not touch the editor, so a client working on a scene had to fall back on UI automation for everything the window does. These tools close that gap: - editor_status / editor_list_areas / editor_open_area (opens the editor from the launcher and waits for the area to finish loading) - scene_find (by name, kind, or a world box tested against a mesh's triangles rather than its bounds) and scene_select - blender_open / blender_end, and editor_notices for what a push reported - editor_save / editor_build, object_move, scene_delete_selected, editor_undo / editor_redo - camera_get / camera_look_at / camera_set / camera_frame_selection, view_set (shading mode and layers) and viewport_screenshot Illusion.Mcp gets the IEditorSession seam; the application implements it over the open MainWindow. Supporting changes: ViewportControl.CaptureFrame, LookAt and LookFrom, DistrictStreamer.IsBusy, LauncherWindow.OpenMapEditor, and a notice log the banner feeds. * feat(mcp): blender_push, and scene_select with no names clears the selection blender_push sends the bridge's request_push, waits for the push to land and returns what the editor reported, so a client that edited the session's objects in Blender gets the outcome in the same call instead of polling the notice log. An empty scene_select clears the selection: its outline is drawn through walls and would otherwise sit in every screenshot. * fix(bridge): refuse a pushed mesh over 65535 vertices instead of writing 32-bit indices A rebuild that came to more than 65535 split vertices switched the level's index buffer to 32-bit. The toolkit's viewport draws that; the game does not — an interior of 65 981 vertices came up as triangles smeared across the district, textures flickering, while the editor showed it whole. The push is now refused with the vertex count, so the modeller can split the object. A rebuild always writes 16-bit indices, and a level that still carries a wide buffer from before is rebuilt on its next push whatever that push changed, which is what puts it right. scene_find reports each mesh's vertex and triangle count, so the size of an object can be checked before it is built into an archive. * feat: bring a light into a district that has none — actor import across archives A city district ships with no lights of its own: its interiors are lit by LightEntity actors, and the only way the editor could make an actor was to copy one already in the pack. ActorsFile.Import copies an actor out of another archive's pack, with its own copy of the behaviour row it points at; the editor wraps it as one undoable edit and gives it a row in the tree, creating the entity type's section when the district never had one. Only an actor that places no object of its own scene can travel — a light, a sound — and the copy is linked to a frame named after itself, which is the frame such an entity makes. A light keeps its transform twice: in the actor record and at the head of its behaviour blob (every shipped light's x appears exactly twice). The record is what the editor moves, so the pack now carries the translation over into the blob when it is written; an untouched pack still writes byte for byte. MCP: actor_import, scene_duplicate_selected, object_properties and object_set_property (the property panel as data, undoable writes), and decode_actors can list each actor's behaviour fields. * Keep a light's inverse matrix and clip box in step with where it stands A light's behaviour blob ends with the inverse of its world matrix, and the game works the light out through it. A light imported from another archive (or moved in the editor) kept the old inverse: every field read correctly, the light stood in the right place, and it lit nothing. SyncLightFrame now rewrites the inverse whenever it is out of step with the head matrix, carries the clip box along by the distance the light moved, and runs on import as well as on write. Untouched packs still write byte for byte: shipped lights are within 1e-4 of a true inverse and are left alone. * Show and edit a mesh's draw distance in the property panel Each level of detail carries the distance at which it stops being drawn, stored squared. It is the one cost control a static object has - the game culls by distance and view cone only - and an object made through the Blender bridge starts at a million metres. The panel (and so the MCP object_set_property) now lists one draw distance per level, in metres. * fix: touch a light's row only when the light has moved The light sync rewrote any inverse matrix that was out of step with the head matrix. The install has 72 such rows that shipped that way (three Joe's Adventures archives carry zeroes or noise there), so re-saving those untouched packs changed them: --probe-act-relayout, --probe-actor-props and --probe-native-misc fell to 1047/1050. Now a light whose record and head translation agree is left alone, and a moved light gets a new inverse only if the one it had was a true inverse. The clip box and inverse are also written to the row's named fields: on write the core pokes every named field back over the blob, which put the imported light's old box straight back. --probe-act-relayout gains the import: a light from one archive into a pack of another, checking its own row, matrix, inverse, clip box, the writer round trip and a refused duplicate name. 1050/1050 packs re-save byte for byte again. * docs: the editor tools, the Blender material push and their limits README: the MCP section lists the 25 editor tools and says how they relate to the editor's own undo, save and build; the bridge section covers pushed materials and the 65535-vertex refusal; the property panel mentions draw distances; Known limits gains opaque-only pushed materials and undrawn light actors. --probe-mcp now expects the editor tools as well. * feat: carry a Blender material's alpha into the game - cut-out and translucent A material made in Blender was always opaque: its diffuse went to DXT1 and its flags were the plain ones. Now whatever feeds the Principled BSDF's Alpha - the image's own alpha, a mask image, or a value below 1 - is written into the diffuse texture, which is then stored as DXT5, and the material's render method picks the mode: Blended is a translucent surface, anything else a cut-out. The game has no separate shader for either on the shaders the bridge creates materials for; the flag word decides. Measured over default.mtl on the diffuse shader: 239 materials set Alpha and keep writing depth (fences, signs, foliage), 323 set Disable_ZWriting on DXT5/DXT3 textures (overlays), 11 carry both. A cut-out gets Alpha and keeps CastShadows; a blend gets Disable_ZWriting and drops CastShadows; every other bit is left alone. - DdsEncoder: DXT5 (interpolated alpha block + the DXT1 colour block), with the header stock DXT5 files carry. - MafiaMaterialCatalog: GetFlags / SetFlags; the editor's SetFlags is an undoable edit. - The addon sends alphaMode with the material and puts the alpha it means into the image; an opaque material keeps its old signature, so nothing is resent for nothing. - --probe-bridge-material: 40/0, with the encoder's alpha round trip, the flag words, and one material taken through cut-out, blend and back. * feat(viewport): draw alpha-blended materials blended The mesh shader alpha-tested every textured material at one half. That is right for a cut-out and wrong for glass: a pane whose alpha is 0.3 was clipped away entirely, so a translucent material - a stock one or one pushed from Blender - was simply not there in the viewport. A part now knows whether its material blends (Disable_ZWriting in the flag word), and such parts are drawn in a pass of their own after the opaque one: depth-tested, never depth-written, standard alpha blend, no clip. The flag follows a material edit the same way the textures and the tint do. Unsorted on purpose - a pane seen through a pane may composite in the wrong order. Instanced props keep the alpha test. * feat: mirror a district's edits into its winter archive A district ships twice and an edit made to <name>.sds is simply absent from <name>_z.sds. For most districts the two are one scene: measured on uppertown, every shared resource (buffer pools, name table, actors, collisions, item descriptions, prefab) is byte for byte the same, and the frame resource has the same length and differs in 1740 places - each a material hash swapped for its ^zima counterpart, 82 pairs. Textures aside, nothing else differs. SeasonMirror writes the summer working copy over the winter one with each object's winter materials taken from the winter object of the same name (and occurrence - names repeat), copies the shared files that differ, adds the pools summer gained with their manifest entries, and adds the textures the result names that winter lacks. The caller queues the winter archive for Build. Not every district is such a pair: 13 of 23 are, ten have a winter scene of their own. The pair is therefore judged first on the archives as shipped (each one's oldest backup, or the archive itself if never built), and a district that fails is refused rather than overwritten. - MCP: editor_mirror_winter. - --probe-season-mirror: 11/0 on scratch folders built from one working copy, so it does not depend on what the modder did to the install - an untouched pair is left alone, a missing object arrives and winter is byte for byte whole again, a stranger is refused untouched; reports which of the install's pairs are twins. * feat: carry objects from one archive into another A district can now be furnished from the rest of the game: a door from a shop, a chair or a plant from an interior, brought into the loaded area as one undoable edit (MCP object_import). - FrameTransplant copies a frame subtree across resources: frames re-homed (FrameEntry.MoveTo), geometry and material blocks deep-copied once per shared block, every buffer they draw from copied into the district's own pools under fresh names. Links inside the subtree follow the copies; the root becomes a prototype (an actor places it) or scenery anchored to the district's main scene, on the name table, standing where it is put. - ActorsFile.Import takes an actor that places an object once that object has been carried, links it by the copy's name through a scene reference of its own, and can turn it. - ArchiveCarry brings what the scene does not hold: the textures the materials name, the item descriptions the collision frames name, and the prefab entry the definition names (PrefabFile.Adopt, which keeps the container's size header right - the core writes it as it is handed it). Keys travel unchanged; what the district already has is shared. - ObjectImportController shows the copy (tree rows, GPU meshes, actor row) and undoes it as one edit. - A frame named by hash alone (weapon parts) kept a zero hash when copied: the copy constructor rebuilt the name from its empty string. Fixed for the duplicator and the prototype cloner too. --probe-object-transplant: 30/0 - a door, a prop an actor places and a piece of scenery go from shops/harry.sds into a scratch copy of a district, keep their shape frame for frame and their buffers byte for byte, survive a save and a reload (actors resolve, meshes decode from the district's pools, prefab and item descriptions in the manifest), and an undo leaves the scene byte for byte as it was. * feat: a Props tab to drag stock objects into a district The map editor's property rail gets a Props tab: the stock game's doors, seating, tables, beds, storage, plants, lamps and decor, one card per object with a picture. Drag a card onto the viewport to put the object where it lands (on the scene's surface under the cursor), or double-click it to put it in front of the camera. - PropCatalog finds them in the extracted interiors and districts: every actor that places an object of its archive (door, physics prop, framed furniture), and scenery whose name says what it is and whose size is a piece of furniture. One card per object across archives; interiors are read first, so a card points at the stock source. The scan (~6 s) is remembered per archive and redone only for what changed. - PropThumbnailRenderer draws each card's picture offscreen with the object's own textures and keeps it on disk. - Scenery now brings collision: the source's hulls that stand inside its footprint, re-placed by the same move (CollisionCarry), or one cooked from its triangles when it had none - small props usually have none. - A door carried out of its building gets ActorActionsEnabled: a shop's front door ships with the player's actions off, its shop script turns them on, and nothing does that where it lands. - The import core moved from the MCP session into ObjectImportController, shared by object_import and the tab. A card from the loaded district is duplicated in place rather than refused. - RefManager hands out ids atomically: the catalog parses scenes in the background while the editor loads districts. --probe-prop-catalog: 7/0 - 2747 props in 6.3 s, 52 ms remembered, known doors and chairs on their shelves, each shelf's first card gets a picture. * feat: simplified hulls for imported scenery, and a props column - A hull cooked for scenery that had none of its own is now its convex hull by default (ConvexHull: incremental, points snapped to a grid a fortieth of the object's size) - a coat rack goes from 1474 collision triangles to 22. Also a box (12 triangles) or every render triangle, chosen per import: object_import's 'collision' (auto / convex / box / mesh / none) and a chooser in the props panel. A flat thing has no convex hull and gets a box. - The props panel moves out of the property rail into a column of its own left of the viewport: as tall as the window, sized with a splitter, shown and hidden by a Props button on the toolbar; the viewport's tool shelf moves with the viewport and is no longer covered. - Textures an import carries are written down beside the working copy (illusion_carried.json, outside the manifest) and each save sweeps the ones no material of the scene names any more - an undone import, or one lost with a scene closed unsaved, no longer leaves megabytes behind. --probe-object-transplant: 33/0 (+ the hull of a cube is the cube, a stock model's hull holds all its vertices and faces out, a flat thing gets a box). * feat: an imported object's collision moves and is deleted with it Nothing in an archive ties a static object to its collision - the hull is a placement that happens to stand where the object stands. For scenery the toolkit carried in, the tie is now written down beside the working copy (ImportLinks, illusion_links.json, outside the manifest): the object's name and the hashes of the hulls it was given. A gizmo drag of the object drags those placements along (a scale mints a scaled hull as it does for any placement), and Delete takes them with it; a Delete that went through several editors is one Ctrl+Z (EditHistory.SquashSince). Stock objects are untouched: nothing ties them, and guessing by position would take a building's hull along with a bench. * feat: a duplicate of an imported object gets copies of its collision Duplicating scenery the toolkit carried in also places copies of the hulls it was given, where the copy stands, links them to the copy under its own name, and the whole duplicate is one Ctrl+Z. Linked hulls are now claimed one to one across the scene, nearest first and ties to the name that sorts first: a copy stands on top of its original with the very same hull, and dragging it used to drag the original's hull away. * feat: save drops geometry nothing draws; a re-import shares geometry - Save writes the buffer pools without the buffers no geometry block draws from - left behind by a deleted object, a sanitised block, a rebuilt mesh. Measured: none of the 130 archives as shipped carries one (--probe-geometry-sweep), the install's working copies had ~0.5 MB. Only the files lose them: memory keeps them, so an undo after a save brings the object back and the next save writes them again (SdsGeometrySaver.SavePools tracks what each pool file holds). - Importing an object again draws from the buffers - and, when every level is there, the geometry block - its earlier import brought, the way the shipped scenes put many copies of one chair on one mesh. Where geometry went is remembered per source archive beside the working copy (ImportGeometry, illusion_geometry.json); a remembered buffer the scene no longer has is simply copied again. - The bridge probes snapshotted only the pool they edited; a save now may rewrite others, so they snapshot and restore every pool. --probe-object-transplant: 38/0 (+ imported twice shares, the memory survives on disk, undoing copies keeps the first, a save leaves dead buffers out of the files and writes them back when drawn again). * feat(mcp): drive the resource editor; read and set car tuning - editor_target switches the scene tools between the map editor and the resource editor's stage: find, select, properties, move, duplicate, delete, camera, screenshot, shading, save, build, undo/redo and the Blender tools follow it, and answer with that editor's status. - resource_list searches the archive library (pc\sds by name or folder), resource_open stages one - from the launcher the way its tile does, beside the map editor as a second window - and waits for it to load; resource_status reports what is on the stage. - car_tuning lists a car's entity-data tables (stock and tuned variants, labelled by mass and power) and their fields by band and element; car_tuning_set writes one, parsed by its kind, with the Tuning tab's bookkeeping: an undo entry, the archive on the build list, the tab re-read. A wheel's or gear's field can be named without its prefix together with its element. Tried on shubert_38: 6 tables x 771 fields; Power, CenterOfMass and Wheel0.Scale set, read back and undone. --probe-mcp: 66/0. * feat(cars): clone a car under a new name; a Build no longer drops XML - car_clone (MCP) / CarCloner: copies pc\sds\cars\<source>.sds and its winter twin to <name>.sds and renames what the name keys inside - the root frame and its actor link (and so the name table), the PREFAB entry (FNV64 of the name) and the entity-data storage (FNV64 of the name in lower case). Then registers the car: a vehicles.tbl row under a fresh id with the source's class, price and flags, its PaintCombinations row, its AiProps cover points, and - optionally - a slot in every traffic row (CARM*.tbl) that picks the source. Builds the new archives plus tables.sds and ingame.sds, backups kept. - GameTable: an extracted .tbl as rows of cells, written back through the table codec. PrefabFile.Rekey, EntityDataStorageFile.Hash. - Fix: PruneMissingEntries looked for an XML resource under its manifest name, but it sits on disk as name + ".xml" - so the first Build of an archive with XML resources (ingame.sds, tables.sds) unsaid every one of them, and the Script containers with them. XML is now found by its real file, containers are left alone. --probe-car-clone: 33/0 (on scratch copies: refusals write nothing, the rows and hashes the clone gains, the archives pack, nothing is pruned, every XML of both table archives decompiles to the same text after a pack). --probe-mcp 66/0, --probe-object-transplant 38/0. * feat(mcp): archive_build packs one archive's working copy For an edit made in the working copy itself - a script, a table file - that no editor session tracks: packs the extracted folder back into its .sds through SdsWriter.PackSds, keeping the usual timestamped backup. editor_build stays the way to pack what the editors changed. Used on pc\dlcs\cnt_jimmys_vendetta\sds_ru\missionscript\ fr_game_director.sds after a script edit: 8 resources before and after, only the script pack differs (+301 bytes, the edit). --probe-mcp: 66/0. * fix: a rebuilt name table keeps the order it was loaded in A cloned car sat in the garage with its name and no model. Two causes, both found against the game: - The name table was rebuilt in frame-index order. The order is authored, not derivable: every one of the 103 cars the game ships lists its root frame first, and only 34 happen to be in index order. The clone's table led with an effects frame. Each frame now carries the position the loaded table gave it (FrameNameTableOrder, set where the flags are linked), and BuildDataFromResource emits in that order, frames the table never listed after them by index. This is every rebuild, the Resource Editor's car save included. - The game finds archives through its cached file list (%LOCALAPPDATA%\2K Games\Mafia II\Data\vfs.bin) and does not pick up a NEW archive on its own. GameFileIndex.Reset removes the cache, the way Steam's install script does; car_clone calls it and says so. --probe-car-clone: 36/0 - the clone lists its root first; rebuilding the name table of each of 103 shipped cars reproduces it byte for byte (five tables that list a frame twice or name a frame the resource does not hold are left out). --probe-nametable: 1331/1331 semantic, 960 byte-identical. --probe-object-transplant 38/0, --probe-mcp 66/0. Whether the clone now shows in game is not yet confirmed. * feat(cars): a clone gets buffers and a title of its own - Buffers. The game keeps vertex and index buffers by name across every archive it has loaded, so a clone that kept the source's names would draw whichever shape streamed first once its model is edited - and so would the source car. Cloning now renames every buffer the geometry draws from, in the geometry blocks and in the pool files ("Shubert_38.Root.L0.VB0" -> "<name>.Root.L0.VB0"; a name that does not start with the model's gets the new one in front). - Title. car_clone takes an optional title: one new string in the text table of every installed language (sds_<lang>\text\text_default.sds, tables\TextDatabase.dat - UTF-8 "KEY:TEXT" lines, id 60000017 is key 00_60_00_0017), under an id from 60001000 + vehicle id that nothing uses, and the clone's vehicles.tbl row points at it. Without a title the clone shares the source car's name, as before. GameText reads and adds lines; the text archive is built with the others. --probe-car-clone: 48/0 (the clone's buffers share no name with the source's, every level of detail resolves, the bytes are the source's in the source's order; the title is one added line and nothing else, the text archive packs, its XML survives the pack, nothing is pruned). In game: the first clone showed in the garage with its model after the name-table fix; this build of it is confirmed to start, not yet looked at in the garage. * Packer keeps shipped memory requirements; export a car as an M2O resource A rebuilt archive stated payload sizes where the shipped one stated what the engine should budget (a car asked for 148 079 bytes of slot RAM against 170 232, and no 'other' RAM). The figures are now read from the archive as it shipped, kept beside the working copy (illusion_memory.json) and applied on pack; a clone carries its source car's. archive_build takes memoryFrom for a clone built before that. car_export_m2o writes a resource folder for Mafia II Online: package.json, cars/<name>.sds (+ _z) and vehicles.json with the model, titles, source car, key hashes and the vehicle/paint table rows. Probes: --probe-car-clone (memory), --probe-car-m2o. * feat(cars): build a car under another car's name car_substitute replaces <target>.sds with the source car's model keyed by the target's model name (root frame, name table, prefab entry, entity data, buffers), keeps a backup and touches no table - for trying a car where models are spawned from a fixed list of names. * feat(cars): the M2O car export follows the map export's layout; docs Archives sit under sds/cars/ in the exported folder and each entry names the path the game loads it from (sds, winterSds), the way the map export names its patch targets. README: the car tools, memory requirements, the tool count. * fix: an imported object brings textures its own archive does not hold A weapon lying in a shop is drawn with textures of weapons.sds; the import looked for them in the shop's archive only, reported them as 'in neither archive' and the object stood white in the district. A texture the source lacks is now taken from whichever extracted archive holds it - the game itself ships the same texture in many archives - and the notice names where it came from. --probe-object-transplant: 43/0 (+ a shop weapon's texture comes from weapons.sds byte for byte, none is written when no archive holds it). * perf(bridge): read a level's index width without decoding the mesh Review of #3. HasWideIndices decoded the whole level - every vertex channel, the index array and, on a model, the skin - to read one field of its index buffer, on every push, ahead of a path that decodes the mesh again. It now clamps the level and looks at the index buffer directly. --probe-bridge-rebuild 17/17, --probe-bridge-vertex 2/2, --probe-bridge-resplit 6/6. * fix(mcp): editor tools say no when the editor does, and find crash copies from the table Review of #3, the findings in the MCP session. editor_open_area dropped unsaved edits. Changing the area or the season reloads the scene, which clears the edited frames, the unsaved list and the undo history, and the tool only checked for a Blender session. A load that would replace a scene with unsaved edits is now refused unless the caller passes discardUnsavedEdits=true; asking for the area and season already shown stays a no-op. editor_save and editor_build reported success after a save that did not complete. The viewport's save does not throw for a material library that would not write or a car's working copy that was refused - it posts a notice and carries on - so the tool had nothing to return but success, and a build went on to pack archives whose materials were not on disk. D3DImageHost.SaveEditsReport hands back what was left unsaved; editor_save answers success=false with notSaved, and a build whose save did not complete packs nothing and says why. object_set_property could not rename. Base.Name is offered as an editable HashName and the parser had no case for it. It has one now, with the property panel's rule (an empty name is refused), and a rename to the name the object already has is not committed as an edit. Numeric properties took NaN and infinity. float.TryParse accepts "NaN", "Infinity" and "1e100" (as infinity); a float and each component of a vector now have to be finite. scene_find did not see crash copies until their row had been expanded in the tree, and a box query never returned one. They are now searched from the placement table: by the copy's own label or its prop's name, and by the prop's triangles at the copy's matrix. Only the copies returned get a tree node. --probe-editor-tools (new, no game install needed) drives the real tool methods against a scripted editor and the parser directly: 23 checks. Checked live in the editor on uppertown: 5 lamp copies found with no row expanded, 288 triangles of one in a box round it, none 400 m above it; a moved mesh blocks a load of another area and of the other season, the same area is a no-op, discardUnsavedEdits=true loads. A failing material write was not reproduced live - that path is covered by the scripted editor only. * fix(mirror): the winter mirror decides against the pair as shipped, not against the last mirror Review of #5, the four findings in SeasonMirror. Twins were told by size. Two archives passed as "one scene shipped twice" when their shared pools were equal and their scenes the same length - and a winter scene with one object moved 50 units is the same length. The scenes are now parsed, winter's material hashes replaced by summer's, and the two written back out: anything left that differs (a transform, a parent link, a draw distance) means winter is a scene of its own and the mirror is refused. The 13 stock twins still read as twins and the other ten as not. Everything else follows from reading that pair once (SeasonPair) and deciding against it, where the mirror used to ask the winter WORKING COPY what winter wore - which answers with whatever the previous mirror left: - A slot the modder re-pointed in summer got its old winter material back. A summer material no object of that name wore when the pair shipped is now carried into winter as it is (Report.Reassigned). - Namesakes were matched by position among objects of the same name, so deleting the first handed its materials to the second. The season's substitution is now looked up by name and SUMMER MATERIAL; position is only used when one summer material stands for two winter ones under one name, and only while the number of namesakes is what it shipped as. Otherwise the object keeps summer's material and is counted (Report.Ambiguous, raised as an error notice) rather than dressed in another object's snow. - A texture an earlier mirror had brought was never refreshed: the bridge repaints an authored texture in place, and "winter has a file of that name" ended the matter. A file winter did not SHIP with is now compared and rewritten, its MIP_ companion with it (and removed when the new picture has none); a file winter shipped with is the season's own and is left alone. Mirror() also refuses a winter folder that is not made of the pair's meshes before writing anything, and editor_mirror_winter stops when the save it starts with did not complete. Its result names slotsReassigned and objectsAmbiguous in place of objectsReshaped. --probe-season-mirror builds its own namesakes and runs every case above on scratch copies: 23/23 on uppertown and oysterbay, 22/22 on italy, 21/21 on greenfield (not a twin, so the shipped-texture checks do not apply). * fix(bridge): a material push that can be undone, refused cleanly and shared across archives Review of #4, all ten findings. Texture files are part of the push's undo. A repaint under the same name changes no binding, so the catalog recorded nothing and Ctrl+Z could not bring the old picture back. The resolver now journals every file it writes (what the name held before, what it holds now) and the push carries a TextureFilesEdit in its one history entry: undo restores the old files - or removes a texture the push introduced, manifest entries included - and redo puts the new ones back. Nothing is written before everything is checked. The diffuse texture was on disk before the normal map had been looked at, and before the library had accepted the material. All images are now read, validated and packed first; what is written before a later file or the library refuses is rolled back from the same journal. A split texture is compared as two files. PickName compared only the entry, which for a texture of 256 and up starts at half resolution: a fine checkerboard and the grey it averages to are byte-identical there (reproduced), so the second picture took the first one's name and its write replaced the first one's top level. The MIP_ companion - and whether there is one - is now part of the comparison. A material shared by two archives is packed into both. The second object of a push binds the new material by name, and an unchanged material arrives with no pixels; either way an archive that had never held its textures got none. The files are now copied from the archive that has them (only for materials this bridge created - a stock material bound by name copies nothing). A renamed material keeps its hash across a shader change. Gaining or losing a normal map deletes and recreates the record, and it was recreated under the Blender datablock's current name - a new hash, with every other mesh left pointing at the old one. It is rebuilt under the name the library knows it by. Reloads are coalesced. A push of a hundred objects sharing a material asked for a hundred invalidate-and-rebind passes over every loaded mesh; each texture is now invalidated once and each material that names one rebinds once (ReloadTextureFiles). A rebound part returns the textures it held. GpuMesh kept every lease until dispose, so the library's retired copy of each repainted texture stayed on the GPU for as long as the mesh was loaded. Leases are kept per part and the superseded ones released once the part holds the new ones. The mesh preview keeps the material's tint. Add-on: a Normal Map node in Object or World space is no longer exported as if it were tangent space - the material is refused by name with what to change - and the resampler clamps its two neighbours independently: upsampling [0, 1, 0] to four texels gave 0.875 at the left edge, now 0. --probe-bridge-material 58/58 on eastside (30 new checks: each case above, including twenty repaints of a texture a live GpuMesh draws leaving nothing retired), --probe-material-editor 65/65, --probe-bridge-rebuild 17/17, --probe-mcp 66/66. The undo entry itself was checked at the file level (restore before/after), not by pressing Ctrl+Z in the editor, and the two add-on changes were not run inside Blender: the resampler was checked on the reviewer's numbers outside it. * fix: the material preview blends what the scene blends; an alpha mask is sampled linear Review of #5, the two findings outside the mirror. The material editor's preview still alpha-tested a translucent material. SphereMesh forwards a part's Blended flag, but the part the preview builds for a material carried only its tint, and the mesh-shape preview dropped the flag again - so a uniform 0.3-alpha pane vanished from the ball while the scene drew it blended. The flag is read from the catalog with the tint and kept in both preview shapes. Add-on: a colour image used as an alpha mask was read through image_rgba8, which turns a picture into what an albedo texture stores (sRGB bytes), and the luminance of those encoded values was written as coverage. A float mask at linear 0.25 came out as 137/255 instead of 64 - enough to carry a cut-out across the 0.5 it is tested against. The mask is now read in the values the shader works with (_shader_pixels): float buffers as they are, 8-bit sRGB linearized as the Image Texture node does, Non-Color data and the alpha channel as stored. Run in a background Blender on the add-on as it stands in this branch: linear 0.25 float mask -> 64, 8-bit sRGB 0.5 -> 55, Non-Color 0.5 -> 128, alpha channel 0.3 -> 77; the same run covers the two add-on fixes merged from #4 (Normal Map node space, resampler edge). The preview change was not looked at in the material editor window. * test: the scripted editor answers the resource status the tools now ask for --probe-editor-tools drives the tool methods against a stand-in editor. Since the resource editor became a target, a tool reads ResourceStatus to learn which editor it is reporting on; the stand-in had no answer and the save cases stopped there. It now hands back an empty status, which names no target, so the map's status is used. 23/23. * fix(cars): a clone or a substitution that fails takes itself back; resource_open stages what was asked for Found reviewing this branch the way #3-#5 were reviewed. car_clone deleted a working copy before it had checked anything. A folder under the new name with no archive beside it was removed as "left behind by an attempt that never produced its archive" - ahead of the checks on the title and the vehicle table, so a clone refused a moment later had already cost whatever the folder held (a parked car, work never built). Such a folder is now a refusal that names it, and nothing is deleted. Clone and substitute wrote several game files with no way back. A clone adds rows to shared working copies and then packs five or more archives; any pack failing (the game holding a file, a full disk) left a vehicle row with no archive, tables written and not packed, and a name that was now "taken", so the clone could not be tried again. A substitution that failed on the winter archive left summer substituted and winter stock, with the target's working copy already deleted. Both now keep a GameWriteJournal: rewritten files get their bytes back, new files and folders are removed, a replaced archive is restored from the backup taken of it a moment earlier (and that backup dropped), and a working copy that has to make way is moved aside until the operation has succeeded. The refusal says the operation was taken back and names anything that could not be. resource_open answered success with the previous archive on the stage. The resource editor's catalog is a walk of the game folder made when the window opens; an archive made since - a car cloned a moment ago, which is the workflow car_clone recommends - was never found, the request waited for good, and the tool's "something is loaded" test was met by the car already there. car_tuning_set then wrote into that car. The window now walks again for an archive it does not know (once per archive), and the tool waits for the archive it asked for and fails, naming what the stage shows, when it does not arrive. An archive with nothing to draw no longer runs out the whole timeout. --probe-car-clone 73/73 (8 new: every kind of journal entry undone on scratch files). --probe-car-clone-rollback (new; unlike the others it WRITES to the install and restores it): the real Clone with the winter pack blocked - refused onto an existing working copy without touching it; after the failure the summer archive, both folders, vehicles.tbl, the paint and traffic tables, cover points, manifest and text are as they were, tables.sds and ingame.sds unpacked with no backup left, and the same clone can be tried again. 11/11. resource_open checked live: a car archive copied into pc\sds\cars after the window opened is staged, by path and by bare name. The substitution's rollback is covered by the journal checks only, not by a staged failure against the install. * fix(import): a failed import leaves nothing behind; redo gets its shared block and its textures back Found reviewing this branch the way #3-#5 were reviewed. An import that failed after the frames were copied left them in the scene. The copy is made first, then its textures are carried, its rows built and its collision read; a failure in any of those answered "the import failed" with the frames still in the resource - in the next save, with no row in the tree to delete them by and no undo entry. The catch also named five exception types, so a collision file that does not decode went past it as a tool error, same leftovers. Import now takes the copy back out when it fails before the edit is on the undo stack, catches whatever is thrown, and says in the refusal that nothing was left. Edits of one import that were already applied when a later one throws are undone again; an actor already in the pack is removed; GPU meshes made for rows that will never be shown are released. A failure AFTER the edit is registered (writing the note that lets the next import share geometry) no longer reports the import as failed. Redo of a copy that shares a geometry block threw on the next save. A second import of the same object draws from the first one's block and lists none of its own. With the first copy deleted and the second import undone, a save prunes the block; Reattach put back only the blocks the copy lists, so the redone mesh named a block the resource did not have and the save after it died in UpdateFrameData (KeyNotFoundException). Reattach now also re-registers what its meshes point at, as DetachedFrames does. A save swept textures that undo could still bring back into use. Carried textures no material of the scene names are removed at save - but the import can be redone and a deleted object's delete undone, and neither carries anything again: the object came back naming textures that were gone from the working copy and its manifest. Swept textures are now parked - out of the manifest, into illusion_parked\ under a name no scan for *.dds answers to, with their manifest entries kept field for field - and the next sweep that finds the scene naming one puts it back. What an earlier run of the program parked is dropped at the first save of the next; a texture carried again while parked wins over the parked copy. A file that cannot be moved leaves the texture where it is for the next save instead of failing the save. --probe-object-transplant hill shops\harry.sds 50/50 (12 new): sweep with the objects in / undone / saved twice / redone (same bytes, same manifest entry, MIP companion with it) / swept again / carried again while parked / left by an ended run; the shared block after delete + undo + save + redo, saved and read back. Without the Reattach change the same probe dies with the KeyNotFoundException. Live, against the editor: a source whose collision file is cut short - before the change every later import under that name answers "already taken"; after it the same refusal twice and no rows. A scenery import (250 hulls) and an actor import still work, with undo, redo, undo. Not checked: the failure paths inside ImportPlaced after the pack took the actor, and a failing GPU mesh. * test(import): the carried-again check asks for the textures, not for their count Higher in the stack a texture can also be borrowed from a third archive, so the second carry brings more than the three objects brought the first time. * fix(import): the lesser findings of the self-review - carry, links, names, numbers, thumbnails The second half of the review of this branch. One commit because the pieces lean on each other; by finding: Carry. A refused or failed import now takes its carry back: the working copy's manifest, prefab containers and carried register are noted before the carry and restored, and the files the manifest gained are removed (ArchiveCarry.Note / TakeBack). It used to leave textures, item descriptions and a prefab entry behind for an object that never arrived - the pack refuses an actor only after the carry. The texture a mesh names itself (OMTextureHash) is carried, and counted as in use by the save-time sweep. A MIP companion is copied before the texture that needs it, and a texture whose companion cannot be brought is not brought. RemoveEntry writes the manifest through a temp file, as AddEntry did. Object-to-hull link. The hulls an imported object was given are found by distance to the object's box, not its pivot - a district mesh's pivot can be metres from its geometry, and its hulls were then never linked. A move typed into the Transform panel takes the hulls along (it went through RecordTransform, which knew nothing of them; past 5 m they were lost for good and a later delete left them in the .col). Renaming the object moves its record. The record is written by an edit of the import (and of the duplicate), so undo takes it back out of the file. Links are worked out once per operation for the whole selection instead of once per node, each time re-reading the file and walking every object and placement. Source. object_import takes `occurrence`: the n-th thing of that name in the source (actors first, then frames - the ones that draw before helpers of the same name), and reports how many there are. Position and heading must be finite. The source archive is kept between imports from it (let go after a minute, re-read when its working copy changed) instead of being read whole for every object. "N hull(s) of its own" is now worded as what it is - the hulls standing inside its box - in the result and in the tool description. Prop thumbnails. The archive is read on a pool thread (Stage) and only drawn on the UI thread (Draw); the tick is guarded, since nothing above a DispatcherTimer catches; the kept picture is keyed by the working copy's time stamp as well and replaces the older ones; the held scene is let go when the tab has all its pictures; texture folders no longer accumulate across archives (TextureLibrary.ClearFolders). --probe-object-transplant 53/53 (the take-back on a scratch copy; an OM-named texture through carry, sweep, park and return). --probe-object-import-live (new; a real window with `hill` loaded, the district's working copy compared with its state before and put back): 30/30 - typed move and turn with undo/redo, drag, rename and its undo, delete and its undo, everything undone leaves no record, occurrence past the end, a source whose collision is cut short refused twice with the same answer, a door the pack refuses leaving manifest and folder as they were. --probe-prop-catalog 9/9, --probe-editor-tools 23/23. Not checked: the Props tab filling in a window (only Stage/Draw are), a companion that fails to copy, hulls linked through the box on an object whose pivot really is far off (the probe's object has it close). Not changed: "its own hulls" are still chosen by position - an archive has nothing else to go by; item descriptions and the prefab entry of an UNDONE import still stay in the working copy. * fix(cars): the lesser findings of the self-review - target, notices, tuning values, builds that drop entries The second half of the review of this branch, by finding: Notices and Blender on the resource editor. Its notices went to its own banner only, while editor_notices and the Blender tools read the application's log: blender_push answered "no push arrived" for a push that had landed, and the line car_tuning_set posts was lost. The window now writes the log as the map editor does. blender_open counted the MAP's Blender objects whatever the target and ran out its timeout on a session opened in the resource editor; it asks the target. Target. editor_open_area, object_import, actor_import and editor_mirror_winter make the map the target, as resource_open makes the resource editor: left where it was, the find/select/delete/save/build after an editor_open_area went on acting on the car on the stage. car_tuning_set makes the resource editor the target - its undo entry and build list are that editor's. EditorStatus says which (Target). Open editors. car_clone and car_substitute are made from the working copy on disk: a source open in an editor with unsaved edits is refused. A substitution whose target is loaded in an editor is refused - its working copy is replaced under that editor. resource_open of the archive already on the stage loads nothing (a reload emptied the undo history), and the two cases the window answers with a dialog - an archive another editor has loaded, a save that fails - are answered before it is asked: a tool call that waits for a click never returns. Re-keying. No root frame of the model's name, no prefab entry, entity data filed under another name, no buffers: each was a note while the archive was packed and the car registered. They are failures; the clone or the substitution is taken back. Tuning values. A float that is not finite and an integer wider than its field are refused where the value is written (TuningEditing.Set), so the Tuning tab is covered too. Dropped entries. PackSds removes manifest entries whose file is missing and said nothing. PackResult.Dropped carries them: both editors post them as an error notice (never silenced), editor_build and the clone list them, and archive_build - the tool for hand edits - refuses such a build unless dropMissing is passed, since the entry is gone from the manifest for good. MissingEntries asks without packing. Said, not changed: a clone is registered in the main game's tables only - the three story DLCs ship an ingame.sds of their own, and traffic is added to CARM* tables only; the result's notes name both. The long file work of clone/substitute/build still runs on the UI thread (it keeps the editors from touching the files meanwhile); what was cut is the scratch extraction MemoryFor repeated on every build of an archive that has no shipped figures. --probe-car-clone 75/75 (a car not keyed by the name given is a failure; a working copy short of a file says which). --probe-car-clone-rollback 11/11, --probe-editor-tools 23/23, --probe-object-import-live 30/30. Live, on a probe copy of jeep.sds removed afterwards: NaN, Infinity, 1e40 and an integer past its field refused; the tuning notice in the log; the same car opened twice keeps undo; the target after editor_open_area and after car_tuning_set; resource_open of the district the map has loaded answered at once; clone and substitute refused against the open car; archive_build refused, then built with dropMissing. Not checked: blender_open/blender_push with the resource editor as the target (no Blender in the run), the Build dialogs' new line in a window. * fix(import): the texture index keeps up with the mirror Found reviewing this branch. The index a third-archive lookup goes by was one scan, made once: an archive extracted later in the session was not in it (import a shop gun before weapons.sds was ever opened - 'in no extracted archive' - open weapons.sds, import again: the same answer until a restart), and it kept one path per name, so when that path was a copy the toolkit had carried into a working copy and later took out again, the lookup answered with a file that was not there and the texture was reported as held nowhere although its own archive had it. Every folder holding a name is remembered now (as an index into a list of folders, not sixty thousand paths), an answer is checked against the disk and falls through to the next copy when the first is gone, and the extractor announces the folder it has just made (RegisterFolder). --probe-object-transplant 60/60 (2 new: a folder announced after the scan; a name held twice - first copy, second when the first is gone, nothing when both are). --probe-material-editor 65/65. Not checked: the shop-gun scenario itself in the editor. * fix(import): review of #6 - links by place, blocks shared only when alike, the Props tab paged What the review of this branch found that the commits before this one had not already answered (the failed-import rollback, the swept textures and the numeric-edit path are in 56f5f8d and 45236da). A collision link is a placement, found by its place. The record kept one hash per hull: two placements of the same hull collapsed into one entry (Distinct), a resize re-cooked the hull under a new hash the record did not learn, and a placement was looked for within 5 m of the object's pivot, which says nothing about where a placement's origin is. Each entry is now one placement - the hull and where it stood in the object's own space when the link was made - and it is found where the object's matrix now puts that point, which every move, turn and resize the two make together leaves unchanged. The same hull twice is two entries; a resize rewrites the hashes in the record as an edit inside the same undo step; a record written before this (bare hashes) is read as before and matched by nearness to the object's box. A geometry block is shared only when it draws the same way. Matching buffers and decompression values was taken as "the same block", so a second import inherited whatever had been done to the first copy's block since - a draw distance set to 0, and an object that does not draw. The draw distance, vertex layout and count and both capsules are compared (FrameLOD.DrawsLike); the buffers are still shared, the block is copied when it differs. The Props tab makes cards a page at a time. A wrapping panel does not virtualize, and the whole catalog - 2,900 objects here - was given cards at once, about two seconds of the UI thread on opening and on every filter change. 120 cards, and the next 120 when the end of them comes within reach; pictures go to the cards in view first. The picture timer, stopped when the tab is hidden, starts again when it is shown - a catalog already loaded had nothing to restart it. --probe-object-import-live 36/36 (6 new: a resize gives the hull a new hash and the record follows, undo puts both back; a second placement of the same hull nine metres from the pivot is the object's too, and both go with it). --probe-object-transplant 54/54 (a copy whose draw distance was changed does not lend its block). --probe-prop-catalog 12/12 (the tab in a 330x700 window: 120 cards for 2928 objects, 240 after scrolling to the end, pictures resume after hide and show). Not checked: a record in the old format on a real working copy. * fix(cars): review of #7 - the scratch clone takes itself back, manifests that do not read, undo re-enlists the archive What the review of this branch found that c4b33d4 and 64b4e9b had not already answered (the rollback of Clone and its packs, the stale catalog behind resource_open, the modal dialog on the tool path and the non-finite tuning numbers are in those). CloneExtracted is whole or nothing by itself. The rollback lived in Clone; the clone on working copies, called on its own, still wrote the vehicle row before the title, the paint row and the traffic slots, and a failure at any of them left a car registered by half and its name taken. It now keeps the journal itself (Clone passes its own in, to add the packs to it). The journal no longer tries to put back a file that was noted and never written - with the text table held open, that was the one thing the undo reported it could not restore. The M2O export does not write over a manifest it cannot read. An existing vehicles.json or package.json that does not parse was taken for 'none yet' and replaced with this one car - after the archives had been copied. Both are read before anything is written; one that is there and does not read is a refusal that leaves the folder as it was. Undo and redo of a value that lives in the working copy put the archive back on the build list (WorkingCopyEdit, for tuning and effect edits, in the panel and in car_tuning_set). A Build takes the archive off the list; an undo after it changed the working copy and the next Build packed nothing, leaving the game with the value that had been undone. --probe-car-clone 77/77 (a clone whose text table is held open is refused and leaves the vehicle table, the ingame tables, the text and the folders as they were; the same clone then succeeds). --probe-car-m2o 24/24 (a vehicles.json, then a package.json, that does not read: refused, both files and the folder untouched). --probe-car-clone-rollback 11/11. Live, on a probe copy of jeep.sds: set Mass, build, undo - the archive is pending again; redo - still pending. * fix(import): review of #9 - the carry tries every copy the index knows The two findings of the review of this branch. The first - an archive extracted after the index was built is not in it - is answered by 91722f3 (the extractor announces the folder; the index checks the disk). What was left of the second: the lookup asked the index for ONE path and took a copy that could not lend - one in the destination itself, one its own archive does not list - as proof that no archive holds the texture. HolderOf now goes through every copy on disk (TextureSearchIndex.FindAll) and takes the first that is in another working copy and on that archive's list; a working copy whose manifest does not read lends nothing and is passed over. And the default lookup is covered by the probe, not only an injected path (asked for in the review): --probe-object-transplant 64/64 - a donor announced after the index was built is found by the carry's own lookup; a first copy that is not on its archive's list does not end the search; a donor that has gone and a copy that is the destination's own are passed over for the next donor. * fix(props): the picture cache of an earlier build is left alone 45236da gave a prop's kept picture a name that carries the state of its archive, and on first use deleted every picture under the old name as something nothing reads. A build from before that change, in use beside this one on the same folder, reads exactly those: its Props tab lost all 2,928 pictures the first time a probe of this branch ran. Only a prop's own pictures from earlier states of its archive are dropped now. --probe-prop-catalog 12/12; 2,928 old-format pictures still there after it. * fix(bridge): what a second look at 838c69a found 838c69a made the texture files of a push part of its undo and put a refused material's files back. Read again before it went up: Undo removed a texture the push had introduced. The material that names it is created by an edit of its own and outlives the push's entry, and Blender - told the push arrived - sends pixels again only when the image changes: after one Ctrl+Z the texture was in no archive, and the next push put the material on the object with nothing to draw it from. The undo entry takes back pictures that were REPLACED; an introduced one stays with its material. Undo and redo did not put the archives back on the build list. Push, Build, Ctrl+Z: the folder had the old picture, the game's archive the new one, and Build answered that there was nothing to build. A push that only repainted a texture was dropped from the history by the first unload of any district: its entry names no objects, and 'all of its objects have left the scene' is true of none. A write that failed was remembered as done. The name was noted before the write; the …
…irror for districts (#5) * feat: preview a material on the context mesh slot in the Material Editor Local work found uncommitted in the working copy (base a55dc22, v0.3.1): sphere/mesh preview toggle, the slot label on the assign panel, and the SDK pin moved to 10.0.302. * feat: turn a material made in Blender into a game material on push A push used to refuse any slot whose material had not come from the toolkit. A Blender-made material now travels with the image wired into its Principled Base Color and becomes a real game material: - addon: sends the image as RGBA8 (top-down, resampled to powers of two) once per push however many slots use it; the ack stamps the hash back on the datablock so later pushes send pixels only when they changed - DdsEncoder: DXT1 with a full MIP chain and the stock header, written as a single Texture entry (HasMIP 0) - the shape 2809 stock city textures ship in, so no companion Mipmap entry is needed - AuthoredMaterialResolver: fills in the hash before the mesh path runs; binds by name when the game already has the material, otherwise writes the texture into the object's archive (file + manifest) and creates a default-preset material with it in S000 (one undo entry) - a re-push with new pixels rewrites the texture in place and the renderer reloads it (TextureLibrary.Invalidate) Probe: --probe-bridge-material [district] [push.ilx] - encoder, resolver, re-push, bind-by-name, refusal, and optionally a container written by the addon itself. * fix: stop the Material Editor throwing while its XAML loads The Sphere toggle is checked in markup, so its Checked handler ran before the Mesh toggle and the preview existed and the window died with a NullReferenceException on open. The handler now waits for both. The probe read the assign BUTTON's visibility; the button is hidden through its panel since the mesh-preview change, so it reads the panel. --probe-material-editor: 65 passed, 0 failed (was 50 + an exception). * fix: live-test the Blender material push and close what it found A live probe drives the real viewport and a real bridge session (--probe-bridge-material-live): an off-screen D3DImageHost loads an archive, opens a mesh in a running bridge Blender and waits for an object made there, wearing a material made there, to be pushed back. It checks the scene, the renderer, a re-push with a changed image, undo/redo, Save, a reload from disk and a pack, then restores every file. 21 passed against Blender 5.1.2. What it found: - image resize went through Image.copy().scale(), which hands back the blank original of a painted or generated image - the texture arrived black. The addon now resamples the pixel array itself. - a rebuilt mesh's parts carried no material hash, so a later material edit (or a texture rewritten by a push) could not find them to re-resolve. The hash and tint now ride with the part, as on a mesh loaded from disk. - Blender keeps the hash of a material the library may have lost (creation undone, toolkit closed without Save). With pixels it is made again; without them the push is refused once and the ack tells the datablock to forget, so the next push arrives complete. * fix: store large textures the way the game does, and stop Build losing resources A Blender-made material came out BLACK in game. The object was there and the material was sound (same shader, flags and sampler states as 1888 stock materials) - the texture was not in a shape the game loads. - From 256x256 up the game stores a texture split: the top level alone in a Mipmap entry, everything from half resolution down in the Texture entry with HasMIP=1 (6747 split textures surveyed, no exception; no stock mip-chained texture is wider than 128 on its short side). The encoder wrote a 1024x512 texture whole. DdsEncoder.Encode now returns the pair, and ArchiveTextureWriter writes both files and both entries. Two packer bugs found while comparing the rebuilt archive with stock: - TextureHandler charged a split texture only its own payload. Stock charges the Texture entry its payload PLUS its MIP companion's and leaves the Mipmap entry at zero (381 of 381 in italy.sds), so every Build under-reported the archive's video memory by the size of all its top levels - 6 MB for italy. - PruneMissingEntries joined a rooted resource name ("/missions/...") with Path.Combine, looked for the file at the drive root, and unsaid a resource that was present. italy.sds lost its AudioSectors on Build. --probe-bridge-material: 30 passed (split layout, the memory figure against the files on disk, pruning with a rooted name). * fix: give a created material the two fields the stock ones carry The billboard was still black in game after its texture was stored correctly. The material had been compared with stock on shader, flags, sampler states and parameters - through a view that does not show every field. Two were left at zero by the Default preset: - Unk0: 128 on 1837 of the 1929 stock materials on that shader - the diffuse sampler's TexType: 2 on 1947 of 1951 (0 on four) Material_v57's Default preset now sets both, which also covers the materials the glTF import creates. v58 (Definitive Edition) is left alone: there is no DE install here to measure it against. The probe compares a created material with the majority of stock materials on its shader in these fields, so a preset that drifts from the game's own records fails here instead of in game. * fix: rename a probe local that clashed with one further down The previous commit did not build: the new stock-record check declared 'made', which the addon-container section of the same method already uses. --probe-bridge-material: 25 passed. * feat: carry a Blender material's normal and specular maps into the game A Blender-made material arrived with its Base Color image only. It now also brings the image behind its Normal Map node and the one driving its specular level, and becomes a normal-mapped game material. Chosen from a survey of the stock library, not assumed: - shader 5159568776351604322 (S000 + S001, parameter D013): 1209 stock materials, drawn on the P|N|T|UV0 declaration a pushed mesh already has. Its S001 is one combined texture - normal X and Y in red and green, the SPECULAR level in blue (stock "...NS" textures: red/green 127 +- 10, blue 30..240), DXT1 like 97 of the 120 sampled. - NormalSpecularPacker builds that texture from the two Blender images (either may be missing) and inverts green: Blender's normal maps have green up, the game's bitangent follows texture V, which runs down. - MaterialPreset.DiffuseNormal creates the record in the commonest stock shape of that shader - Unk0, flags, both samplers' TexType and states, D013 - and the probe compares it field by field with the majority of the 1209. - Blender's roughness and specular level become D013's power and level; the defaults (0.5 / 0.5) land on the commonest stock pair, 16 / 0.3. A material that gains or loses its normal map needs another shader, so it is replaced under the same name - the hash meshes refer to does not move. The resolver now talks to an IAuthoredMaterialHost (create, update, replace) instead of two callbacks; the application's host records each on the undo history. Also: binding a texture to a sampler sets its TexType to 2 (a slot added in the Material Editor started at 0, which samples nothing in game); two materials sharing an image share one texture file. --probe-bridge-material: 33 passed. Driven live through the real viewport against a windowless Blender: the billboard in italy took a new material with all three maps and shows the relief in Render mode. * style: dotnet format the preview viewport's mesh builder * feat(mcp): tools that drive the running map editor The MCP server could read game files but not touch the editor, so a client working on a scene had to fall back on UI automation for everything the window does. These tools close that gap: - editor_status / editor_list_areas / editor_open_area (opens the editor from the launcher and waits for the area to finish loading) - scene_find (by name, kind, or a world box tested against a mesh's triangles rather than its bounds) and scene_select - blender_open / blender_end, and editor_notices for what a push reported - editor_save / editor_build, object_move, scene_delete_selected, editor_undo / editor_redo - camera_get / camera_look_at / camera_set / camera_frame_selection, view_set (shading mode and layers) and viewport_screenshot Illusion.Mcp gets the IEditorSession seam; the application implements it over the open MainWindow. Supporting changes: ViewportControl.CaptureFrame, LookAt and LookFrom, DistrictStreamer.IsBusy, LauncherWindow.OpenMapEditor, and a notice log the banner feeds. * feat(mcp): blender_push, and scene_select with no names clears the selection blender_push sends the bridge's request_push, waits for the push to land and returns what the editor reported, so a client that edited the session's objects in Blender gets the outcome in the same call instead of polling the notice log. An empty scene_select clears the selection: its outline is drawn through walls and would otherwise sit in every screenshot. * fix(bridge): refuse a pushed mesh over 65535 vertices instead of writing 32-bit indices A rebuild that came to more than 65535 split vertices switched the level's index buffer to 32-bit. The toolkit's viewport draws that; the game does not — an interior of 65 981 vertices came up as triangles smeared across the district, textures flickering, while the editor showed it whole. The push is now refused with the vertex count, so the modeller can split the object. A rebuild always writes 16-bit indices, and a level that still carries a wide buffer from before is rebuilt on its next push whatever that push changed, which is what puts it right. scene_find reports each mesh's vertex and triangle count, so the size of an object can be checked before it is built into an archive. * feat: bring a light into a district that has none — actor import across archives A city district ships with no lights of its own: its interiors are lit by LightEntity actors, and the only way the editor could make an actor was to copy one already in the pack. ActorsFile.Import copies an actor out of another archive's pack, with its own copy of the behaviour row it points at; the editor wraps it as one undoable edit and gives it a row in the tree, creating the entity type's section when the district never had one. Only an actor that places no object of its own scene can travel — a light, a sound — and the copy is linked to a frame named after itself, which is the frame such an entity makes. A light keeps its transform twice: in the actor record and at the head of its behaviour blob (every shipped light's x appears exactly twice). The record is what the editor moves, so the pack now carries the translation over into the blob when it is written; an untouched pack still writes byte for byte. MCP: actor_import, scene_duplicate_selected, object_properties and object_set_property (the property panel as data, undoable writes), and decode_actors can list each actor's behaviour fields. * Keep a light's inverse matrix and clip box in step with where it stands A light's behaviour blob ends with the inverse of its world matrix, and the game works the light out through it. A light imported from another archive (or moved in the editor) kept the old inverse: every field read correctly, the light stood in the right place, and it lit nothing. SyncLightFrame now rewrites the inverse whenever it is out of step with the head matrix, carries the clip box along by the distance the light moved, and runs on import as well as on write. Untouched packs still write byte for byte: shipped lights are within 1e-4 of a true inverse and are left alone. * Show and edit a mesh's draw distance in the property panel Each level of detail carries the distance at which it stops being drawn, stored squared. It is the one cost control a static object has - the game culls by distance and view cone only - and an object made through the Blender bridge starts at a million metres. The panel (and so the MCP object_set_property) now lists one draw distance per level, in metres. * fix: touch a light's row only when the light has moved The light sync rewrote any inverse matrix that was out of step with the head matrix. The install has 72 such rows that shipped that way (three Joe's Adventures archives carry zeroes or noise there), so re-saving those untouched packs changed them: --probe-act-relayout, --probe-actor-props and --probe-native-misc fell to 1047/1050. Now a light whose record and head translation agree is left alone, and a moved light gets a new inverse only if the one it had was a true inverse. The clip box and inverse are also written to the row's named fields: on write the core pokes every named field back over the blob, which put the imported light's old box straight back. --probe-act-relayout gains the import: a light from one archive into a pack of another, checking its own row, matrix, inverse, clip box, the writer round trip and a refused duplicate name. 1050/1050 packs re-save byte for byte again. * docs: the editor tools, the Blender material push and their limits README: the MCP section lists the 25 editor tools and says how they relate to the editor's own undo, save and build; the bridge section covers pushed materials and the 65535-vertex refusal; the property panel mentions draw distances; Known limits gains opaque-only pushed materials and undrawn light actors. --probe-mcp now expects the editor tools as well. * feat: carry a Blender material's alpha into the game - cut-out and translucent A material made in Blender was always opaque: its diffuse went to DXT1 and its flags were the plain ones. Now whatever feeds the Principled BSDF's Alpha - the image's own alpha, a mask image, or a value below 1 - is written into the diffuse texture, which is then stored as DXT5, and the material's render method picks the mode: Blended is a translucent surface, anything else a cut-out. The game has no separate shader for either on the shaders the bridge creates materials for; the flag word decides. Measured over default.mtl on the diffuse shader: 239 materials set Alpha and keep writing depth (fences, signs, foliage), 323 set Disable_ZWriting on DXT5/DXT3 textures (overlays), 11 carry both. A cut-out gets Alpha and keeps CastShadows; a blend gets Disable_ZWriting and drops CastShadows; every other bit is left alone. - DdsEncoder: DXT5 (interpolated alpha block + the DXT1 colour block), with the header stock DXT5 files carry. - MafiaMaterialCatalog: GetFlags / SetFlags; the editor's SetFlags is an undoable edit. - The addon sends alphaMode with the material and puts the alpha it means into the image; an opaque material keeps its old signature, so nothing is resent for nothing. - --probe-bridge-material: 40/0, with the encoder's alpha round trip, the flag words, and one material taken through cut-out, blend and back. * feat(viewport): draw alpha-blended materials blended The mesh shader alpha-tested every textured material at one half. That is right for a cut-out and wrong for glass: a pane whose alpha is 0.3 was clipped away entirely, so a translucent material - a stock one or one pushed from Blender - was simply not there in the viewport. A part now knows whether its material blends (Disable_ZWriting in the flag word), and such parts are drawn in a pass of their own after the opaque one: depth-tested, never depth-written, standard alpha blend, no clip. The flag follows a material edit the same way the textures and the tint do. Unsorted on purpose - a pane seen through a pane may composite in the wrong order. Instanced props keep the alpha test. * feat: mirror a district's edits into its winter archive A district ships twice and an edit made to <name>.sds is simply absent from <name>_z.sds. For most districts the two are one scene: measured on uppertown, every shared resource (buffer pools, name table, actors, collisions, item descriptions, prefab) is byte for byte the same, and the frame resource has the same length and differs in 1740 places - each a material hash swapped for its ^zima counterpart, 82 pairs. Textures aside, nothing else differs. SeasonMirror writes the summer working copy over the winter one with each object's winter materials taken from the winter object of the same name (and occurrence - names repeat), copies the shared files that differ, adds the pools summer gained with their manifest entries, and adds the textures the result names that winter lacks. The caller queues the winter archive for Build. Not every district is such a pair: 13 of 23 are, ten have a winter scene of their own. The pair is therefore judged first on the archives as shipped (each one's oldest backup, or the archive itself if never built), and a district that fails is refused rather than overwritten. - MCP: editor_mirror_winter. - --probe-season-mirror: 11/0 on scratch folders built from one working copy, so it does not depend on what the modder did to the install - an untouched pair is left alone, a missing object arrives and winter is byte for byte whole again, a stranger is refused untouched; reports which of the install's pairs are twins. * perf(bridge): read a level's index width without decoding the mesh Review of #3. HasWideIndices decoded the whole level - every vertex channel, the index array and, on a model, the skin - to read one field of its index buffer, on every push, ahead of a path that decodes the mesh again. It now clamps the level and looks at the index buffer directly. --probe-bridge-rebuild 17/17, --probe-bridge-vertex 2/2, --probe-bridge-resplit 6/6. * fix(mcp): editor tools say no when the editor does, and find crash copies from the table Review of #3, the findings in the MCP session. editor_open_area dropped unsaved edits. Changing the area or the season reloads the scene, which clears the edited frames, the unsaved list and the undo history, and the tool only checked for a Blender session. A load that would replace a scene with unsaved edits is now refused unless the caller passes discardUnsavedEdits=true; asking for the area and season already shown stays a no-op. editor_save and editor_build reported success after a save that did not complete. The viewport's save does not throw for a material library that would not write or a car's working copy that was refused - it posts a notice and carries on - so the tool had nothing to return but success, and a build went on to pack archives whose materials were not on disk. D3DImageHost.SaveEditsReport hands back what was left unsaved; editor_save answers success=false with notSaved, and a build whose save did not complete packs nothing and says why. object_set_property could not rename. Base.Name is offered as an editable HashName and the parser had no case for it. It has one now, with the property panel's rule (an empty name is refused), and a rename to the name the object already has is not committed as an edit. Numeric properties took NaN and infinity. float.TryParse accepts "NaN", "Infinity" and "1e100" (as infinity); a float and each component of a vector now have to be finite. scene_find did not see crash copies until their row had been expanded in the tree, and a box query never returned one. They are now searched from the placement table: by the copy's own label or its prop's name, and by the prop's triangles at the copy's matrix. Only the copies returned get a tree node. --probe-editor-tools (new, no game install needed) drives the real tool methods against a scripted editor and the parser directly: 23 checks. Checked live in the editor on uppertown: 5 lamp copies found with no row expanded, 288 triangles of one in a box round it, none 400 m above it; a moved mesh blocks a load of another area and of the other season, the same area is a no-op, discardUnsavedEdits=true loads. A failing material write was not reproduced live - that path is covered by the scripted editor only. * fix(mirror): the winter mirror decides against the pair as shipped, not against the last mirror Review of #5, the four findings in SeasonMirror. Twins were told by size. Two archives passed as "one scene shipped twice" when their shared pools were equal and their scenes the same length - and a winter scene with one object moved 50 units is the same length. The scenes are now parsed, winter's material hashes replaced by summer's, and the two written back out: anything left that differs (a transform, a parent link, a draw distance) means winter is a scene of its own and the mirror is refused. The 13 stock twins still read as twins and the other ten as not. Everything else follows from reading that pair once (SeasonPair) and deciding against it, where the mirror used to ask the winter WORKING COPY what winter wore - which answers with whatever the previous mirror left: - A slot the modder re-pointed in summer got its old winter material back. A summer material no object of that name wore when the pair shipped is now carried into winter as it is (Report.Reassigned). - Namesakes were matched by position among objects of the same name, so deleting the first handed its materials to the second. The season's substitution is now looked up by name and SUMMER MATERIAL; position is only used when one summer material stands for two winter ones under one name, and only while the number of namesakes is what it shipped as. Otherwise the object keeps summer's material and is counted (Report.Ambiguous, raised as an error notice) rather than dressed in another object's snow. - A texture an earlier mirror had brought was never refreshed: the bridge repaints an authored texture in place, and "winter has a file of that name" ended the matter. A file winter did not SHIP with is now compared and rewritten, its MIP_ companion with it (and removed when the new picture has none); a file winter shipped with is the season's own and is left alone. Mirror() also refuses a winter folder that is not made of the pair's meshes before writing anything, and editor_mirror_winter stops when the save it starts with did not complete. Its result names slotsReassigned and objectsAmbiguous in place of objectsReshaped. --probe-season-mirror builds its own namesakes and runs every case above on scratch copies: 23/23 on uppertown and oysterbay, 22/22 on italy, 21/21 on greenfield (not a twin, so the shipped-texture checks do not apply). * fix(bridge): a material push that can be undone, refused cleanly and shared across archives Review of #4, all ten findings. Texture files are part of the push's undo. A repaint under the same name changes no binding, so the catalog recorded nothing and Ctrl+Z could not bring the old picture back. The resolver now journals every file it writes (what the name held before, what it holds now) and the push carries a TextureFilesEdit in its one history entry: undo restores the old files - or removes a texture the push introduced, manifest entries included - and redo puts the new ones back. Nothing is written before everything is checked. The diffuse texture was on disk before the normal map had been looked at, and before the library had accepted the material. All images are now read, validated and packed first; what is written before a later file or the library refuses is rolled back from the same journal. A split texture is compared as two files. PickName compared only the entry, which for a texture of 256 and up starts at half resolution: a fine checkerboard and the grey it averages to are byte-identical there (reproduced), so the second picture took the first one's name and its write replaced the first one's top level. The MIP_ companion - and whether there is one - is now part of the comparison. A material shared by two archives is packed into both. The second object of a push binds the new material by name, and an unchanged material arrives with no pixels; either way an archive that had never held its textures got none. The files are now copied from the archive that has them (only for materials this bridge created - a stock material bound by name copies nothing). A renamed material keeps its hash across a shader change. Gaining or losing a normal map deletes and recreates the record, and it was recreated under the Blender datablock's current name - a new hash, with every other mesh left pointing at the old one. It is rebuilt under the name the library knows it by. Reloads are coalesced. A push of a hundred objects sharing a material asked for a hundred invalidate-and-rebind passes over every loaded mesh; each texture is now invalidated once and each material that names one rebinds once (ReloadTextureFiles). A rebound part returns the textures it held. GpuMesh kept every lease until dispose, so the library's retired copy of each repainted texture stayed on the GPU for as long as the mesh was loaded. Leases are kept per part and the superseded ones released once the part holds the new ones. The mesh preview keeps the material's tint. Add-on: a Normal Map node in Object or World space is no longer exported as if it were tangent space - the material is refused by name with what to change - and the resampler clamps its two neighbours independently: upsampling [0, 1, 0] to four texels gave 0.875 at the left edge, now 0. --probe-bridge-material 58/58 on eastside (30 new checks: each case above, including twenty repaints of a texture a live GpuMesh draws leaving nothing retired), --probe-material-editor 65/65, --probe-bridge-rebuild 17/17, --probe-mcp 66/66. The undo entry itself was checked at the file level (restore before/after), not by pressing Ctrl+Z in the editor, and the two add-on changes were not run inside Blender: the resampler was checked on the reviewer's numbers outside it. * fix: the material preview blends what the scene blends; an alpha mask is sampled linear Review of #5, the two findings outside the mirror. The material editor's preview still alpha-tested a translucent material. SphereMesh forwards a part's Blended flag, but the part the preview builds for a material carried only its tint, and the mesh-shape preview dropped the flag again - so a uniform 0.3-alpha pane vanished from the ball while the scene drew it blended. The flag is read from the catalog with the tint and kept in both preview shapes. Add-on: a colour image used as an alpha mask was read through image_rgba8, which turns a picture into what an albedo texture stores (sRGB bytes), and the luminance of those encoded values was written as coverage. A float mask at linear 0.25 came out as 137/255 instead of 64 - enough to carry a cut-out across the 0.5 it is tested against. The mask is now read in the values the shader works with (_shader_pixels): float buffers as they are, 8-bit sRGB linearized as the Image Texture node does, Non-Color data and the alpha channel as stored. Run in a background Blender on the add-on as it stands in this branch: linear 0.25 float mask -> 64, 8-bit sRGB 0.5 -> 55, Non-Color 0.5 -> 128, alpha channel 0.3 -> 77; the same run covers the two add-on fixes merged from #4 (Normal Map node space, resampler edge). The preview change was not looked at in the material editor window. * fix(bridge): what a second look at 838c69a found 838c69a made the texture files of a push part of its undo and put a refused material's files back. Read again before it went up: Undo removed a texture the push had introduced. The material that names it is created by an edit of its own and outlives the push's entry, and Blender - told the push arrived - sends pixels again only when the image changes: after one Ctrl+Z the texture was in no archive, and the next push put the material on the object with nothing to draw it from. The undo entry takes back pictures that were REPLACED; an introduced one stays with its material. Undo and redo did not put the archives back on the build list. Push, Build, Ctrl+Z: the folder had the old picture, the game's archive the new one, and Build answered that there was nothing to build. A push that only repainted a texture was dropped from the history by the first unload of any district: its entry names no objects, and 'all of its objects have left the scene' is true of none. A write that failed was remembered as done. The name was noted before the write; the next object of the push sharing the image was handed it as success and its material named a file that was never made. Noted after. A texture half written - the entry replaced and its top level, or the manifest, not - was caught with nothing on record to put it back from. It is put back before the failure is passed on, and neither that nor AtomicFile leaves its .tmp beside the target any more. An object is refused before any of its materials is touched: every slot is checked first for what can be refused without writing (normal map space, images that did not arrive whole). A bad second slot used to be found after the first slot's pictures had been replaced. A texture that cannot be copied into the object's archive refuses the object as well. --probe-bridge-material 61/61 (3 new: the two-slot object; a texture held open refuses both objects sharing the image and leaves the picture, the manifest and the folder as they were). --probe-material-editor 65/65. The undo entry itself (introduced texture kept, archives re-enlisted, the entry surviving an unload) is not covered by a probe - it lives behind the editor's history. * fix(mcp): a layer is not switched off over unsaved edits; moves and imports take finite numbers A second look at f12155b. editor_open_area was given a guard against dropping unsaved edits, and view_set was left without one: switching the crash (or collision) layer off unloads it, and an unsaved move of a crash copy went with it, undo entry included - the same loss by a shorter road, and an easier one to take now that scene_find hands out crash copies. Refused while the scene has unsaved edits, unless discardUnsavedEdits is passed; switching a layer on, or off with nothing unsaved, is as before. object_move and actor_import took a position or an offset as it came: a number too large for a float arrives as Infinity and was written into the transform. They refuse what is not finite, as property values already do. --probe-editor-tools-live (new; a real window with a district loaded, nothing saved) 12/12: both refusals of non-finite numbers; the layer off and on with nothing edited; a crash copy moved, the layer refused, zones still switch on, the layer off once the edits are given up. --probe-editor-tools 23/23, --probe-mcp 66/66. * fix(mirror): a second look at 3696630 and a3b5f73 Read again before they went up. A material block worn by two objects was settled twice. The mirror writes winter's hashes into the block, so the second wearer was resolved from them as if they were summer's - counted as a slot the modder had re-pointed, or substituted again where a winter hash is also a summer one. The wearers of each block are gathered first and the block is settled once, from summer's hashes, by whichever wearer the pair has an answer for. A mesh that existed only in winter was lost without a word. 'Dropped' had become the shipped pair's meshes summer no longer has; it is what the mirror takes out of winter - every winter mesh the mirrored scene has no counterpart for. A refreshed texture kept its old manifest entry. A repaint at another size changes whether the texture has a MIP companion, which its entry states (HasMIP); the bytes were brought up to date under an entry that promised a companion no longer there, or hid one that now was. The entry is compared with summer's and replaced with it. The material list's tiles draw a blended material blended, as the preview beside them does since a3b5f73. And the add-on's push signature includes the colour space of an alpha mask: an 8-bit mask is linearised or not by its tag, so retagging it changes what is sent while name, file and size stay the same - and nothing was resent. --probe-season-mirror uppertown 28/28 (5 new: a mesh only in winter is reported as dropped; a block two objects wear, the wearer that never wore the summer material first in the file; a refreshed texture's entry follows a changed HasMIP). --probe-bridge-material 68/68, --probe-material-editor 65/65. The add-on change is compiled, not run in Blender. * fix(bridge): the pre-pass refuses a material it cannot make; no empty undo entry Two things 4034b0f left. The check of every slot before any is written did not include a material that would have to be made and arrives with no picture to make it from - new, or one Blender remembers and the library no longer has: found at its own slot, after an earlier slot's texture had been replaced. Both are refused in the pre-pass now, in the words they had. And a push whose only texture changes were introductions made an undo entry with nothing in it - a Ctrl+Z that did nothing and cost the redo branch. --probe-bridge-material 62/62 (1 new: a repaint first, a new material with no image second - refused, the first left as it was). * fix(mcp): the layer guard is about the crash layer only 01973ba guarded switching the collision layer off as well, on the belief that it unloads the layer. It only hides it: the placements, their edits and their undo entries stay, so the guard refused for nothing and its discardUnsavedEdits discarded nothing. Crash only, which does unload. And object_move refuses a position plus offset that overflows, though each number alone is finite. --probe-editor-tools-live 14/14 (collision on and off with an edit unsaved; the overflowing move). * fix(mirror): wearers that disagree, names that differ in case, an entry that cannot be copied Three things 7750e77 left. Wearers of one material block that give different winter materials for a slot: the first in file order won and both were counted as settled; the slot keeps summer's and the objects are put down as in doubt. A texture entry compared with summer's by exact spelling of the file name: a material that spells it otherwise than the manifest made every mirror rewrite the entry and report a refresh. And the old entry was removed before knowing summer's could be copied in its place. --probe-season-mirror 28/28. * fix(mirror): the same bytes are not written again for an entry that cannot be copied b2f3b79 left one case: a texture whose bytes already match summer's but whose manifest entry differs from a summer entry that cannot be copied fell through to the write, so every mirror rewrote the same file and reported it as refreshed. Nothing is written and nothing is reported. --probe-season-mirror 28/28. The case itself has no probe: no stock archive has such an entry.
What
A material created in Blender now comes across the bridge with the object that wears it. Until now the bridge refused any material the toolkit had not handed out itself, so a new object could only reuse stock materials.
Defaultpreset) plus its texture.Fixes found on the way (each one was a black or missing object in the game)
Mipmapentry (MIP_<name>.dds), the rest of the chain in theTextureentry withHasMIP = 1.Textureresource's VRAM figure includes its MIP companion's payload./missions/...) — it removed a district'sAudioSectors.Unk0 = 128andTexType = 2on its sampler, like the stock materials on that shader (1837/1929 and 1947/1951 of them).How it was checked
--probe-bridge-material: 33 passed, 0 failed.--probe-bridge-material-livedrives a real viewport + bridge session against a running Blender.--probe-material-editor65/0,--probe-bridge-rebuild17/0,--probe-bridge-newobj6/0,--probe-save7/0,--probe-content-edit35/0,--probe-native-misc31/0 — the same numbersmaingives on the same install.-c Release -p:MfCoreMode=Prebuilt) clean,dotnet formatclean on every touched file.italyand a full interior inuppertown(11 materials, 13 textures) render with their own textures, normal and specular maps.Limits
Opaque only — alpha is not carried. Material-library edits stay outside the backup flow, as before.
The MCP editor tools, actor import and light placement built on top of this are in #3.