Skip to content

feat: loading zones: see, move, resize, create and delete them - #10

Open
raighen wants to merge 141 commits into
mafia2online:mainfrom
raighen:pr/tools-and-zones
Open

raighen wants to merge 141 commits into
mafia2online:mainfrom
raighen:pr/tools-and-zones

Conversation

@raighen

@raighen raighen commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #11 (the Tools menu, the Build list and the mesh tools, split out of this PR): please merge #11 first. Until then the diff here shows both; the zones are what is left after it.

What

  • In the viewport. With the Loading zones layer on, a click picks the zone that spot is in and the shelf's Move and Scale edit it: arrows at the centre carry it whole, an arrow on each face pulls that face. Every edit is written to city_univers at once, queued for Build, and is an undo step.
  • New zones. The button at the foot of the tool shelf makes a box where the view looks; zone_create makes one between two corners. The zone gets its volume, its place in the frame name table and its line in cityareas.bin. Undo takes it out again; zone_delete takes an added zone out later. A zone the game ships with is not deleted.
  • What makes a zone load. A zone loads its districts for a player who appears inside it when its name has two words after the number (AREA341_GREENFIELD_KINGSTONE), whatever the table says for it. The layer draws the others fainter, and the flyout says so when a name has one word.
  • The layer. Each zone is the edges of its box with a faint fill, hidden by the scene in front of it and fading with distance. The picked zone carries its name and districts. A zone that was added has a second frame round it and "added" in its label.
  • Tools. zones_at, zones_map, zone_move_face, zone_create, zone_delete, and Tools > Loading zones.
  • One write path for the gizmo, the window and the tools; it keeps an editor that holds city_univers (Whole map) in step.

How it was checked

Limits

  • A zone edit is written to the working copy of city_univers at once, not on Save.
  • A new zone is a box standing square to the map.
  • Zones are made and deleted in the base game's city_univers only, not in a DLC's copy.
  • Not with Whole map on: a zone is added or taken out with a single district loaded.

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.
…e commits

mafia2online#3 is in main as one commit whose tree is this stack's pr/editor-tools byte for byte,
so nothing of it changes here; git could not line the two histories up and reported
every file both had touched. The merge keeps this branch's tree and adds the one thing
main has that the branch did not: mafia2online#8 (crash props and meshes without normals no longer
drawn black).
The stack was merged with main after mafia2online#3 and mafia2online#8 went in there as single commits. This
branch already carries both, so its tree is unchanged.
mafia2online#6 is in main as one commit whose tree is this stack's pr/object-transplant byte for
byte, so nothing changes here: the merge keeps this branch's tree and only joins the
two histories, which git could not line up by itself.
mafia2online#6 is in main as one commit whose tree is this stack's pr/object-transplant byte for
byte, so nothing changes here: the merge keeps this branch's tree and only joins the
two histories, which git could not line up by itself.
mafia2online#6 is in main as one commit whose tree is this stack's pr/object-transplant byte for
byte, so nothing changes here: the merge keeps this branch's tree and only joins the
two histories, which git could not line up by itself.
@raighen
raighen marked this pull request as draft October 7, 2026 17:23
raighen added 17 commits October 7, 2026 20:34
…e commits

Main now holds the whole stack this branch stood on, as commits of its own whose tree
is the stack's top byte for byte. Nothing changes here: the merge keeps this branch's
tree and joins the two histories.
…review

Loading zones
- A volume moved in the editor as an object and saved was refused by every zone edit
  from then on: its matrix ended in a one where the disk's ends in zeros (a frame's
  matrix is stored as three columns), and the two were compared whole. What a save
  writes is compared now.
- A zone write carried into an editor's scene left that editor's glyph, selection
  outline and property panel on the old place, and the panel wrote it back on the next
  number typed. They are refreshed with the scene.
- A write made while Whole map was still loading city_univers was missing from the
  scene that arrived, with nothing to carry it in. It is refused until the scene is in.
- zone_move_face put its step on the map editor's history and left the tools pointing
  at the resource editor, so the editor_undo that followed took back something else.
- A move to where the face already stands wrote the scene, queued a Build and pushed
  an empty step. It does nothing now.
- A drag owns the keyboard whatever has the focus: with a text field focused, Esc did
  not drop it and Undo ran in the middle of it.
- A drag is dropped when the layer is switched off under it; a failure to tell the
  editors about a write that succeeded is no longer reported as a failed write.

Build list
- A Mipmap deleted alone was still not listed: it passed as named whenever its texture
  was. It is a resource with an entry of its own and is looked up like any other.
- A name several files carry, each in its own folder, is told apart by the folder; the
  list is read unescaped.

Material slots
- The refusal to re-point a slot of a material block other objects draw with lives in
  the controller, so the material editor's Assign has it as the tool does.
- Asking who shares a block no longer gives every mesh without materials an empty one.

Checked by hand as well: Build pressed in the Build window (the archive built equals
its working copy), Export car, Clone car and Mirror to winter run from the menus.
…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.
@raighen raighen changed the title feat: loading zones in the viewport, a Tools menu, a Build list and mesh tools feat: loading zones: see, move, resize, create and delete them Oct 8, 2026
…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.
@raighen
raighen marked this pull request as ready for review October 8, 2026 07:42
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