Skip to content

feat: interior places (cityshops.bin), an archive's materials, imports under a frame - #13

Open
raighen wants to merge 150 commits into
mafia2online:mainfrom
raighen:pr/interior-places
Open

raighen wants to merge 150 commits into
mafia2online:mainfrom
raighen:pr/interior-places

Conversation

@raighen

@raighen raighen commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Stacked on #10, which is stacked on #11: please merge those first. Until then the diff here shows all three. This PR's own part, with main merged in: raighen/illusion-toolkit@pr/tools-and-zones...pr/interior-places

What

  • Interior places. The game stands a shop, a diner or a flat at a place through three things: a marker frame in the interior's archive, a pair of box volumes in city_univers and rows of missions\SHOPS\cityshops.bin. CityShopsTable reads and writes the table byte for byte (base game and Joe's Adventures). shop_places lists the interiors and their places, shop_place_add and shop_place_delete add and remove a place, shop_create makes a new interior as a copy of an existing one under its own name and row, shop_delete takes it out. A save is all or nothing.
  • What an archive needs registered. archive_materials tells each material an archive is drawn with as the game's own, changed or added, with its full definition and which of its textures are inside the archive. "The game's own" is an embedded list of the 7361 shipped materials, so the answer does not depend on one install. libraryTo writes the added and changed ones as a material library of their own.
  • Imports under a frame. object_import takes a parent frame and, like actor_import, works on the archive open in the resource editor. This is how an interior under shops\ is furnished and lit.
  • Car lights and rig bones, library only. The light list of a car's prefab is read and written back, and RigBones.Insert adds a bone to a skinned model's rig.
  • Fixes. Interiors under shops\ open with their rooms shown: 38 stock archives had the scene of rooms hidden as a proxy. yawDegrees of an import turns it about the vertical and keeps its tilt. A copied material shares no arrays with its original.

How it was checked

  • Illusion.slnx builds clean, dotnet format clean on the touched files.
  • Probes, passed/failed: shop-places 33/0, archive-materials 23/0, object-transplant 83/0, interior-scenes 13/0, mcp 66/0.

Limits

  • A new interior is a copy of a shipped one under a new name. What is in it is then edited like any archive.
  • The proxy and snow sorting of scenes now applies to the city archives only.
  • The car light list and the rig bones have no tool here. The tools come with the cars PR on top of this one.

raighen added 30 commits October 2, 2026 02:09
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.
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.
…lection

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.
…ing 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.
…ss 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.
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.
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.
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.
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.
…anslucent

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.
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.
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.
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.
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.
- 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).
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.
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.
- 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).
- 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.
- 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.
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.
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.
raighen added 28 commits October 7, 2026 20:57
…gram

A hook of the frame can pump the dispatcher, and a frame run inside that pump may
replace the surface - at another size, the old one disposed. The outer frame then drew
into a surface that was gone and marked more of the image dirty than it holds: an
ArgumentOutOfRangeException out of the render pass ("dirtyRect 1194 must be less than
or equal to 232"), with nothing above it. Such a frame is skipped now, and the dirty
rectangle is never larger than the image.
zone_create adds a zone to the base copy of city_univers: a box standing square to the
map between two world corners, made as a copy of an existing zone (its flags, parents
and name-table membership) with a name, a place and a shape of its own, and a line in
cityareas.bin for the one or two districts it keeps loaded. It reports by default and
writes with apply: the scene, the frame name table and cityareas.bin, all three or
none. Refused while an editor holds city_univers (Whole map) - a new volume cannot be
carried into a scene that is already loaded.

cityareas.bin is read and written by CityAreasTable: "ratc", a version, the entry
count, the byte length of the district names, the names, then per zone its name, two
16-bit offsets into the names (0xFFFF for none) and one byte - 1 on the shipped
two-district entries but one, 0 on every single-district one. A district the table
has not named yet is added to its names.

LoadZones.Delete takes a volume and its line out again; it is what undoing a creation
is.

Measured in the game (a multiplayer client on a test server, Sand Island): a platform
with collision was put south of the island's zones, where no zone holds the player.
Logging in on it gave no ground for 25 seconds; with a new zone over it naming
sandisland and tunel, the ground was there from the first reading. Which of the two -
the second district or the byte - makes a zone load a district was not separated.

--probe-zones: the table byte for byte through its writer, a line added and removed, a
zone made and found at its box, the scene with it written and read back, the name
table listing it, and the zone taken out leaving the scene what it was.
…that can be read

The tool shelf gets "New loading zone", enabled while the Loading zones layer is on
and absent from a stage that has no map. Its flyout asks a name and the one or two
districts the zone keeps loaded, filled in from the nearest zone that loads
districts; Create puts a 120 m box where the middle of the view looks, as tall as that
neighbour, picks it and turns the Move tool on. It is written at once and is one step
of the history: undo takes the zone out, and puts the frame name table back as it
stood - a rebuilt table is the shipped one in another order - so making a zone and
undoing it leaves the working copy byte for byte what it was.

The layer was hundreds of filled boxes laid over one another, with no depth: one
coloured haze. Each zone is now the edges of its box with a faint fill, hidden by the
scene in front of it and fading between 300 and 1500 m; a zone the camera stands in
is not filled; one that names a single district - which does not load it by itself -
is drawn fainter; the picked zone is seen through walls and carries its name and
districts over it.
…reation

Measured in the game with new zones over one spot (nine fresh logins): a zone
loads its districts for a player who appears inside it when its NAME has two
words after the number - AREA901_FOO_BAR, AREA903_A_B - with one district in
cityareas.bin or two, its byte 0 or 1. With one word - AREA900_TEST,
AREA902_FOOXBAR - it does not, with two districts and the byte 1. The toolkit
said "a zone that names two districts"; the shipped names made the two rules
look like one. LoadZones.LoadsOnArrival states it, and the layer, the picked
zone's label, the Loading zones window, zones_at / zone_create and the README
go by it. The flyout suggests a two-word name and says so when one is typed
with a single word.

From a review of the zone-creation code:

- a district is written as cityareas.bin says it ("kingston" for the archive
  kingstone) and one that is no district of the city is refused - it was
  written as typed, and a typo became a new district name in the table;
  a line taken out takes the name it brought with it
- undoing a creation puts the three files back byte for byte only while they
  are still what the creation wrote; otherwise the zone is taken out of the
  scene as it stands, and the name table from before is put back only when
  nothing else wrote it since. A table that cannot be put back no longer
  reports the undo as failed
- adding or removing a zone is refused while ANY editor is loading or holding
  city_univers, and a dry run of zone_create is refused like the write
- a refusal late in Create left the new volume hanging on its parent; all
  refusals now come before anything is touched
- a failed save puts back the files it replaced, each swapped in whole, and
  says so when it could not
- the zone layer no longer fades out in a parallel view (Top), where the
  camera's place says nothing about distance
- the flyout: a click on its own button closes it instead of reopening it, it
  has the keyboard while open (a letter typed in a district list started a
  viewport tool), Esc closes it, Enter creates, and the disabled button shows
  its tooltip
…at shares its geometry

The window asked for a box in world coordinates, which nobody could aim. With it
open the triangles are now picked on the mesh itself: a click in the viewport
marks the triangle under it (of the level drawn up close, and only when nothing
stands in front of it), a click on a marked one takes it back. On the other
levels of detail the triangles that lie on the picked ones go with them, and the
count says how many per level - a level cut coarser stays whole there, and the
window says the opening closes from its distance. The box is still there, as the
second mode, for many triangles at once. Either way what Hide would take out is
outlined on the mesh first.

A mesh whose geometry other objects draw too was refused. The window now asks:
only this object - it gets a geometry block and buffers of its own first, the way
a duplicate does, as one undo step with the cut - or this one and the others,
which are redrawn with it. mesh_hide_triangles takes the same choice as `shared`
('own' / 'all'); left out it refuses as before and names the others.

A row of another object on a shared block is rebuilt from its own frame: the
block is shared, the materials need not be.
…e_delete

A zone that is not one of the volumes the game ships with (a list of their names
is embedded: the base city_univers and the DLC copies) is an added one. The layer
draws a second, near-white frame round it that fades half as much as the rest,
its label and its row in the Loading zones window say "added", and zones_at
reports it.

zone_create, with the map editor open, writes through the editor's own zone
editing: the zone is a step of its history and editor_undo takes it out. Without
an editor it writes as before.

zone_delete takes an added zone out again - its volume, its place in the frame
name table and its line in cityareas.bin - as an undo step that puts the files
back as they stood while nothing else has written to them. A zone the game ships
with is refused.
Picking triangles: the mesh's own rows no longer count as something standing in
front of it, and a click that picks nothing says whether the spot is off the
mesh or what stands in front of it there.

The probe runner's --probe-vertex-normals case is back where upstream has it,
with upstream's comment; the hide-triangles case no longer sits between the
geometry sweep's comment and its case. The mesh_hide_triangles description no
longer says a mesh sharing its geometry is always refused.
…review

- "This one and the others" redrew no other object when the mesh had several
  levels of detail: such an object draws from its level rows and its own row
  carries no mesh, so it was dropped before the edit could walk its levels. The
  buffer was cut for all and the viewport showed one.
- The refusal for an instanced mesh looked at the mesh's own row only; with
  several levels the instanced rows are the level rows, and the copies vanished
  from the viewport. Every row that draws the mesh is asked now, and an
  instanced object among the sharers refuses "all".
- On a coarser level a triangle whose three corners lay on the picked ones went
  with them even when it spanned what was not picked (a door, from frame to
  frame). Its middle and the middles of its edges are asked too.
- The picks are dropped when a Blender session starts or ends; the choice for
  shared geometry starts over with another mesh; a click on Hide that brought
  the window forward does not hide a count that changed under it; the texts
  no longer say more than is known.
A material made from another kept the same arrays: parameter values,
sampler states and both sampler sets. Editing the copy changed the
original, and a library read once could be altered through a clone.
The copy constructors now clone them, and a version 58 sampler copied
from another keeps all four numbers of its first set.
A car's lights are a list in its prefab entry, each tied to a bone: the
list is read, edited and written back (CarPrefab), which is what lets a
car be given a beacon. RigBones.Insert adds a bone to a skinned model's
rig, under a parent, with the tables that go with it.

--probe-car-lights and --probe-car-skeleton print both for any car.
…ts own

A mesh names its materials by hash; the definitions live in the game's
libraries, not in the archive. ArchiveMaterials reads an archive and
tells each material it is drawn with as the game's own, changed or
added, in full (flags, shader, samplers, parameters), with which of its
textures are inside the archive and which materials no library has.

"The game's own" is an embedded list of the 7361 materials the game
ships with (hash and a digest of the definition), so the answer does
not depend on backups or files of one install.

SaveLibrary writes the added and changed materials as a material
library of their own, the file a multiplayer host loads beside the
game's libraries. An archive that adds none writes nothing.

--probe-archive-materials: 23 checks.
The game loads a shop, a diner or a flat as an archive under shops\ and
stands it at a place through three things: a marker frame inside the
archive, a pair of box volumes in city_univers that load it and let it
go, and rows of missions\SHOPS\cityshops.bin.

- CityShopsTable reads and writes the table byte for byte (base game
  and Joe's Adventures).
- ShopPlaces lists the interiors and their places, adds a place for an
  interior, removes one, makes a new interior as a copy of an existing
  one under a new name with its own row, and deletes it. A save is all
  or nothing.
- A box sector's planes follow its bounds when the bounds are edited.

--probe-shop-places: 33 checks. --probe-world-lights and --probe-frames
print the lights, sectors and frames of any archive.
An interior's furniture hangs under the frame that carries the interior
to each place it stands at. TryTransplant takes the frame to hang a
copy under: the copy still lands where it is told to in the world, its
own matrix worked out against the parent's, off the name table, naming
its parent's scene in the second slot the way the shipped interiors do.
The import controller takes the parent's row, and its collision is
cooked where the copy stands.

--probe-object-transplant: 71 checks, five of them new.
…imports

- shop_places, shop_place_add, shop_place_delete, shop_create and
  shop_delete drive the interior table.
- archive_materials lists what an archive needs registered; saveTo
  writes the document, libraryTo the added materials as an .mtl.
- object_import takes a parent frame and, like actor_import, works on
  the archive open in the resource editor: how an interior under shops\
  is furnished and lit.
…n import about the vertical only

Scenes were sorted into proxy and snow by the name table's flags in every archive. The holders of an
interior carry the same bits, so the scene of rooms counted as a proxy and was hidden by the default
filter: 38 stock archives under shops\ opened without their rooms. The sorting now applies to the city
archives only.

object_import's yawDegrees replaced the whole rotation, so anything stored tilted fell over. It now
replaces the turn about the vertical and keeps the tilt, and the asked point is under the base of the
object as it stands. The same rule goes for an imported actor.

Probes: --probe-interior-scenes (new), 12 heading checks in --probe-object-transplant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant