Repository navigation
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.
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
marked this pull request as draft
October 7, 2026 17:23
…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.
…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
marked this pull request as ready for review
October 8, 2026 07:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
city_universat once, queued for Build, and is an undo step.zone_createmakes one between two corners. The zone gets its volume, its place in the frame name table and its line incityareas.bin. Undo takes it out again;zone_deletetakes an added zone out later. A zone the game ships with is not deleted.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.zones_at,zones_map,zone_move_face,zone_create,zone_delete, and Tools > Loading zones.city_univers(Whole map) in step.How it was checked
Illusion.slnxbuilds clean,dotnet formatclean on the touched files.zone_create, a face of it moved, the zone deleted and all of it undone, with the working copy compared byte for byte after the undo.Limits
city_universat once, not on Save.city_universonly, not in a DLC's copy.