epic: world map at 133M cells - chunked format + editor/client/server migration #229

Closed
opened 2026-08-01 07:03:39 +02:00 by panda · 7 comments
Collaborator

Mandate

Build the infrastructure to author, store, render and serve a hand-drawn world map of 13,344 x 9,984 = 133,226,496 cells (417x312 chunks of 32x32; ~2,067 regions of 256x256). Procedural generation stays a bounded, opt-in, seed-logged tool - never the authoring path. Size is a requirement, not a value to reduce; the architecture must stay size-agnostic. Full brief: SPEC_CLAUDE_MAP_EDITOR_133M.

Sequencing decision (Andrea): format-first, no throwaway - no interim edits to the dense model.

Re-baselined current state (main @ 7614a5d, verified)

The spec analyzed an older snapshot; much has since merged:

Spec assumed Current main
21 terrains / 88 statics on a separate branch 29 terrains / 88 statics already merged - content convergence (Fase 0) largely done
2304x1728 only in a worktree committed: world.map 2304x1728 = 3,981,312 cells, 32,557 placements / 85 kinds
Z absent Z done in code (TileMap._z, GroundZAt, CanStep, Int16 -225..+255); world still flat (no world.zmap)
SDF over whole map (S6) already windowed (GroundBlendRenderer viewport-bounded); not yet a chunk cache
assets ~20 MB 28.6 MB merged pack

Still-true seams to migrate: IMapFormat.ReadMap(string,...)/WriteMap->(string,...) whole-string; TileMap/MapModel dense string[,] (full .Clone() per edit); Brush.Flood global HashSet+Queue; editor New fixed 32x32, brush 1-9, min zoom 0.25; 'S' spawn bug (write overwrites terrain, read restores as grass -> flooded map loses spawn on save/reopen).

Phased plan (each item = a PR, each leaves build+test green)

  • PR 1 - Benchmark & fixture harness (Fase 1). Deterministic synthetic fixtures (768^2, 2304^2, header-only 13,344x9,984 w/ implicit default terrain); measure current arch: open time, managed heap, per-brush allocations, save time. Empirically answer "does today's editor/server open 2304^2?". Output: reproducible baseline. No format change.
  • PR 2 - Chunked format + read seam (Fase 2). IReadOnlyWorldMap + IWorldMapStore + manifest/region/chunk (magic, endianness, CRC, catalog hash); legacy importer from world.map/world.statics/world.zmap; atomic temp->replace writer; fix 'S' -> spawn becomes manifest metadata. Golden + property (round-trip) tests. TextMapFormat kept as legacy adapter. No editor/runtime behavior change.
  • PR 3 - Editor model chunk-backed (Fase 3). LRU chunk cache + dirty-pinning replaces dense MapModel; delta undo/redo; Allaga mappa = manifest default-flip (single undo entry); autosave + journal recovery. The 133M world opens here.
  • PR 4+ - Authoring UX (Fase 4). Overview/mip + wide zoom; configurable New + 13,344x9,984 preset; big/shaped brushes; polygon/spline/stamp; statics palette (thumbnails/search/tags). Andrea draws the planet here.
  • Fase 5 - multi-static/cell, terrain/liquid split, footprint, sparse strata; walkability/LOS update.
  • Fase 6 - chunked SDF (local + halo, CPU/GPU LRU, gutter, neighbour invalidation); same visuals editor<->client.
  • Fase 7 - client chunk streaming; server compact warm-load; map+catalog hash handshake -> ProtocolVersion bump; region-border integration tests.
  • Fase 8 - text = import/debug only; remove full-map conversions from runtime; fault injection, recovery, stress; Win/macOS self-contained builds; ops docs + backup.

Invariants Check (persisted from the critical design review)

No FAIL, no HARD GATE tripped.

  • Scope OK - commissioned by Andrea; procedural gen stays bounded/opt-in, never the authoring path.
  • Server-authoritative OK - map is authored content; server authoritative on walkability/LOS/Z from its own warm-loaded copy; trust boundary = map+catalog hash handshake; no client-sent map trusted.
  • GM authorization N/A - no GM command (minimap /tp is separate); Allaga mappa is offline authoring.
  • Identity model N/A - no account/JWT surface.
  • Protocol versioned OK/WARN - map never on the wire (local content); chunked format = file-format version. The connect-time map/catalog hash handshake IS a wire change -> bump ProtocolVersion.Current in that PR (Fase 7).
  • String catalog N/A - no player-facing runtime text; editor dialog copy is tooling UI.
  • Single-threaded sim OK - server warm-loads + validates before accepting connections; in-memory O(1) queries; no file/decompress in the tick; workers touch only immutable data, never World.
  • World.cs HARD GATE OK - no logic added; delegates map queries to a component.
  • Screen HARD GATE OK - streaming/LRU/overview in rendering + ClientWorld components; GameScreen only wires.
  • Client engine-independence OK - format + IReadOnlyWorldMap + chunk/overview math in Shared/Client.Core (unit-tested, no MonoGame); only GPU LRU/atlas is Client glue; editor model stays in MapEditor.Core.
  • Gameplay/Networking separation OK - map format is neither; sits in Shared behind IMapFormat.
  • Act on the instance OK - no per-instance side-collections; undo deltas are transient per-stroke.
  • Extend by type, not switch OK - editor tools + static-catalog entries stay closed families extended by type/row.
  • Server-paced actions N/A - no timed player action.
  • Persistence (GameServer) OK - chunked map is authored content under IMapFormat, distinct from per-entity world.sav; no DB/central-switch/cross-player refs.
  • Persistence (Auth) N/A.
  • Process separation OK - JWT contract unchanged; map is a file each process reads independently.
  • Typed options OK - LRU budget / autosave interval / sizes bind onto an options record, one-place defaults.
  • Broadcasts / AoI OK/N/A - map not broadcast; AoI/absolute-position rules unaffected.
  • Multi-platform OK - managed compression only, no OS-specific deps; Win+macOS tests are an acceptance gate (S14.6).
  • Assets required OK - catalog must distinguish CollisionOnly/Invisible (e.g. redwood_trunk) from missing-asset.
  • Asset naming HARD GATE OK - descriptive names carry over; new fields don't relax naming.
  • ModernUO reference OK - adopt block/sector split + on-demand cache + file fingerprint; reject MUL layout / 8x8 historical limits.
  • Docs & DoD same change OK/WARN - each PR updates its canonical doc; new docs/world-map-format.md references code+golden-tests, never mirrors the byte layout; per-PR DoD delta.

Server-side validation (trust boundary)

Authoring is offline (no player intent). Runtime boundary: map+catalog hash handshake at connect (mismatched client -> diagnostic error, not silent desync) - the one wire addition -> ProtocolVersion bump. Walkability/LOS/Z stay server-decided from the server's warm-loaded map; client copy is render/prediction only.

Verification plan

  • Levels: Core/Shared unit (format, importer, chunk/overview math, Allaga mappa semantics); console benchmark harness (memory/time budgets); golden + property round-trip tests; WebSocket integration for the hash handshake (Fase 7); existing World walkability suite stays green on the chunk-backed map.
  • Key cases incl. trust boundary: round-trip bit-exactness; random-access single-chunk read without full load; Allaga mappa -> four corners + chunk/region borders + old spawn cell = water, survive save/reopen, statics/Z untouched, single-undo restores exactly; fault-injection mid-commit recovers old-or-new (never mixed); hash mismatch -> client diagnostic error; missing water in catalog -> readable error, no mutation.
  • Screenshots: N/A for PR 1-2 (no visible surface); required from PR 3 (editor opens 133M, overview->detail zoom, Allaga mappa before/after). Harness gap: the debug harness drives the client, not the editor - editor screenshotting to be added in Fase 4.
  • Budgets are informational per Andrea's dev PC (S14), not hard CI gates.

Open decisions

  • A. Sequencing - RESOLVED: format-first, no throwaway.
  • B. Draw target - pending (not a PR 1-2 blocker): start on a new ocean-initialized 13,344x9,984 (Allaga mappa -> draw) vs. evolve the existing 2304^2.
  • Format 1.0 (S16, freeze before 1.0): max overlapping strata; water model (type + surface-Z + depth); extra static fields (rotation/variant/hue); region files in Git vs. release content; first world name; RAM/VRAM budgets on the real Mac + PC.

Definition of Done (epic)

Andrea can open a 133,226,496-cell world, initialize it entirely to water with one atomic operation, navigate global->detail, hand-draw its geography, edit terrain/Z/liquids/statics without monolithic waits, save only what changed, and reuse the same content in editor, client and server.

Each phase lands as its own PR with its own DoD delta, whole solution builds, zero warnings, tests green, Win+macOS honored.

## Mandate Build the infrastructure to author, store, render and serve a **hand-drawn** world map of **13,344 x 9,984 = 133,226,496 cells** (417x312 chunks of 32x32; ~2,067 regions of 256x256). Procedural generation stays a bounded, opt-in, seed-logged tool - never the authoring path. Size is a requirement, not a value to reduce; the architecture must stay size-agnostic. Full brief: `SPEC_CLAUDE_MAP_EDITOR_133M`. Sequencing decision (Andrea): **format-first, no throwaway** - no interim edits to the dense model. ## Re-baselined current state (`main` @ `7614a5d`, verified) The spec analyzed an older snapshot; much has since merged: | Spec assumed | Current `main` | |---|---| | 21 terrains / 88 statics on a separate branch | **29 terrains / 88 statics already merged** - content convergence (Fase 0) largely done | | 2304x1728 only in a worktree | **committed**: `world.map` 2304x1728 = 3,981,312 cells, 32,557 placements / 85 kinds | | Z absent | **Z done in code** (`TileMap._z`, `GroundZAt`, `CanStep`, Int16 -225..+255); world still flat (no `world.zmap`) | | SDF over whole map (S6) | **already windowed** (`GroundBlendRenderer` viewport-bounded); not yet a chunk cache | | assets ~20 MB | **28.6 MB** merged pack | Still-true seams to migrate: `IMapFormat.ReadMap(string,...)`/`WriteMap->(string,...)` whole-string; `TileMap`/`MapModel` dense `string[,]` (full `.Clone()` per edit); `Brush.Flood` global `HashSet`+`Queue`; editor `New` fixed 32x32, brush 1-9, min zoom 0.25; **`'S'` spawn bug** (write overwrites terrain, read restores as grass -> flooded map loses spawn on save/reopen). ## Phased plan (each item = a PR, each leaves build+test green) - [ ] **PR 1 - Benchmark & fixture harness (Fase 1).** Deterministic synthetic fixtures (768^2, 2304^2, header-only 13,344x9,984 w/ implicit default terrain); measure **current** arch: open time, managed heap, per-brush allocations, save time. Empirically answer "does today's editor/server open 2304^2?". Output: reproducible baseline. No format change. - [ ] **PR 2 - Chunked format + read seam (Fase 2).** `IReadOnlyWorldMap` + `IWorldMapStore` + manifest/region/chunk (magic, endianness, CRC, catalog hash); legacy importer from `world.map`/`world.statics`/`world.zmap`; atomic temp->replace writer; **fix `'S'` -> spawn becomes manifest metadata**. Golden + property (round-trip) tests. `TextMapFormat` kept as legacy adapter. No editor/runtime behavior change. - [ ] **PR 3 - Editor model chunk-backed (Fase 3).** LRU chunk cache + dirty-pinning replaces dense `MapModel`; delta undo/redo; `Allaga mappa` = manifest default-flip (single undo entry); autosave + journal recovery. The 133M world opens here. - [ ] **PR 4+ - Authoring UX (Fase 4).** Overview/mip + wide zoom; configurable `New` + 13,344x9,984 preset; big/shaped brushes; polygon/spline/stamp; statics palette (thumbnails/search/tags). Andrea draws the planet here. - [ ] **Fase 5** - multi-static/cell, terrain/liquid split, footprint, sparse strata; walkability/LOS update. - [ ] **Fase 6** - chunked SDF (local + halo, CPU/GPU LRU, gutter, neighbour invalidation); same visuals editor<->client. - [ ] **Fase 7** - client chunk streaming; server compact warm-load; **map+catalog hash handshake -> `ProtocolVersion` bump**; region-border integration tests. - [ ] **Fase 8** - text = import/debug only; remove full-map conversions from runtime; fault injection, recovery, stress; Win/macOS self-contained builds; ops docs + backup. ## Invariants Check (persisted from the critical design review) No FAIL, no HARD GATE tripped. - **Scope** OK - commissioned by Andrea; procedural gen stays bounded/opt-in, never the authoring path. - **Server-authoritative** OK - map is authored content; server authoritative on walkability/LOS/Z from its own warm-loaded copy; trust boundary = map+catalog hash handshake; no client-sent map trusted. - **GM authorization** N/A - no GM command (minimap `/tp` is separate); `Allaga mappa` is offline authoring. - **Identity model** N/A - no account/JWT surface. - **Protocol versioned** OK/WARN - map never on the wire (local content); chunked format = file-format version. **The connect-time map/catalog hash handshake IS a wire change -> bump `ProtocolVersion.Current` in that PR (Fase 7).** - **String catalog** N/A - no player-facing runtime text; editor dialog copy is tooling UI. - **Single-threaded sim** OK - server warm-loads + validates before accepting connections; in-memory O(1) queries; no file/decompress in the tick; workers touch only immutable data, never `World`. - **`World.cs` HARD GATE** OK - no logic added; delegates map queries to a component. - **`Screen` HARD GATE** OK - streaming/LRU/overview in rendering + `ClientWorld` components; `GameScreen` only wires. - **Client engine-independence** OK - format + `IReadOnlyWorldMap` + chunk/overview math in `Shared`/`Client.Core` (unit-tested, no MonoGame); only GPU LRU/atlas is `Client` glue; editor model stays in `MapEditor.Core`. - **Gameplay/Networking separation** OK - map format is neither; sits in `Shared` behind `IMapFormat`. - **Act on the instance** OK - no per-instance side-collections; undo deltas are transient per-stroke. - **Extend by type, not switch** OK - editor tools + static-catalog entries stay closed families extended by type/row. - **Server-paced actions** N/A - no timed player action. - **Persistence (GameServer)** OK - chunked map is authored content under `IMapFormat`, distinct from per-entity `world.sav`; no DB/central-switch/cross-player refs. - **Persistence (Auth)** N/A. - **Process separation** OK - JWT contract unchanged; map is a file each process reads independently. - **Typed options** OK - LRU budget / autosave interval / sizes bind onto an options record, one-place defaults. - **Broadcasts / AoI** OK/N/A - map not broadcast; AoI/absolute-position rules unaffected. - **Multi-platform** OK - managed compression only, no OS-specific deps; Win+macOS tests are an acceptance gate (S14.6). - **Assets required** OK - catalog must distinguish `CollisionOnly`/Invisible (e.g. `redwood_trunk`) from missing-asset. - **Asset naming HARD GATE** OK - descriptive names carry over; new fields don't relax naming. - **ModernUO reference** OK - adopt block/sector split + on-demand cache + file fingerprint; reject MUL layout / 8x8 historical limits. - **Docs & DoD same change** OK/WARN - each PR updates its canonical doc; new `docs/world-map-format.md` **references code+golden-tests, never mirrors** the byte layout; per-PR DoD delta. ## Server-side validation (trust boundary) Authoring is offline (no player intent). Runtime boundary: **map+catalog hash handshake** at connect (mismatched client -> diagnostic error, not silent desync) - the one wire addition -> `ProtocolVersion` bump. Walkability/LOS/Z stay server-decided from the server's warm-loaded map; client copy is render/prediction only. ## Verification plan - **Levels:** `Core`/`Shared` unit (format, importer, chunk/overview math, `Allaga mappa` semantics); console benchmark harness (memory/time budgets); golden + property round-trip tests; WebSocket integration for the hash handshake (Fase 7); existing `World` walkability suite stays green on the chunk-backed map. - **Key cases incl. trust boundary:** round-trip bit-exactness; random-access single-chunk read without full load; `Allaga mappa` -> four corners + chunk/region borders + old spawn cell = `water`, survive save/reopen, statics/Z untouched, single-undo restores exactly; fault-injection mid-commit recovers old-or-new (never mixed); **hash mismatch -> client diagnostic error**; missing `water` in catalog -> readable error, no mutation. - **Screenshots:** N/A for PR 1-2 (no visible surface); required from PR 3 (editor opens 133M, overview->detail zoom, `Allaga mappa` before/after). **Harness gap:** the debug harness drives the client, not the editor - editor screenshotting to be added in Fase 4. - Budgets are informational per Andrea's dev PC (S14), not hard CI gates. ## Open decisions - **A. Sequencing** - RESOLVED: format-first, no throwaway. - **B. Draw target** - pending (not a PR 1-2 blocker): start on a new ocean-initialized 13,344x9,984 (`Allaga mappa` -> draw) vs. evolve the existing 2304^2. - **Format 1.0 (S16, freeze before 1.0):** max overlapping strata; water model (type + surface-Z + depth); extra static fields (rotation/variant/hue); region files in Git vs. release content; first world name; RAM/VRAM budgets on the real Mac + PC. ## Definition of Done (epic) > Andrea can open a 133,226,496-cell world, initialize it entirely to water with one atomic operation, navigate global->detail, hand-draw its geography, edit terrain/Z/liquids/statics without monolithic waits, save only what changed, and reuse the same content in editor, client and server. Each phase lands as its own PR with its own DoD delta, whole solution builds, zero warnings, tests green, Win+macOS honored.
Author
Collaborator

PR 1 baseline recorded (just bench) — current dense/whole-string architecture

Measured on Andrea's dev PC (Windows 10.0.26200, x64, 16 CPUs, .NET 10.0.0, ~64 GiB). Reproduce with just bench.

2304x1728 = 3,981,312 cells (measured): ReadMap 222 ms / 76 MiB alloc, MapModel.FromTileMap 78 ms / 40 MiB, brush stroke 9x9 ~0 ms / 0 MiB, ToTileMap (full clone) 9 ms / 40 MiB, WriteMap 88 ms / 64 MiB. Peak retained ~95 MiB.

Projection to 13,344x9,984 = 133,226,496 cells: ReadMap ~7.4 s / ~2.5 GiB, WriteMap ~3.0 s / ~2.1 GiB. Absolute floors: one dense ref grid ~1.0 GiB retained; three in flight ~3.0 GiB; UTF-16 map string ~254 MiB.

Takeaways for PR 2+: a local edit is essentially free, but every dirty render/save touches the whole grid — the O(map) cost is in ReadMap/FromTileMap/ToTileMap/WriteMap, not in the brush. At the target the current model needs GBs of retained heap and multi-second monolithic stalls -> confirms the chunked, addressable format is required. Numbers are informational per this PC, not a CI gate.

PR: #230.

## PR 1 baseline recorded (`just bench`) — current dense/whole-string architecture Measured on Andrea's dev PC (Windows 10.0.26200, x64, 16 CPUs, .NET 10.0.0, ~64 GiB). Reproduce with `just bench`. **2304x1728 = 3,981,312 cells (measured):** `ReadMap` 222 ms / 76 MiB alloc, `MapModel.FromTileMap` 78 ms / 40 MiB, brush stroke 9x9 ~0 ms / 0 MiB, `ToTileMap` (full clone) 9 ms / 40 MiB, `WriteMap` 88 ms / 64 MiB. Peak retained ~95 MiB. **Projection to 13,344x9,984 = 133,226,496 cells:** `ReadMap` ~7.4 s / ~2.5 GiB, `WriteMap` ~3.0 s / ~2.1 GiB. Absolute floors: one dense ref grid ~1.0 GiB retained; three in flight ~3.0 GiB; UTF-16 map string ~254 MiB. **Takeaways for PR 2+:** a local edit is essentially free, but every dirty render/save touches the whole grid — the O(map) cost is in `ReadMap`/`FromTileMap`/`ToTileMap`/`WriteMap`, not in the brush. At the target the current model needs GBs of retained heap and multi-second monolithic stalls -> confirms the chunked, addressable format is required. Numbers are informational per this PC, not a CI gate. PR: #230.
Author
Collaborator

PR 2 up: #231 — chunked binary format core (additive, no runtime wiring yet).

Delivered: IReadOnlyWorldMap + WorldMapStore (random-access, bounded cache) + WorldMapWriter (atomic region+manifest) + LegacyMapImporter (spawn->metadata, skips uniform default chunks); CRC32 per chunk+manifest; docs/world-map-format.md. 24 new tests. Headline: the store opens the full 13,344x9,984 = 133,226,496-cell world as an empty ocean by reading only the manifest — zero region files, no per-cell allocation.

Stacked on #230; will rebase onto main after #230 merges. Next: PR 3 (editor model chunk-backed).

**PR 2 up: #231** — chunked binary format core (additive, no runtime wiring yet). Delivered: `IReadOnlyWorldMap` + `WorldMapStore` (random-access, bounded cache) + `WorldMapWriter` (atomic region+manifest) + `LegacyMapImporter` (spawn->metadata, skips uniform default chunks); CRC32 per chunk+manifest; `docs/world-map-format.md`. 24 new tests. Headline: the store opens the full 13,344x9,984 = 133,226,496-cell world as an empty ocean by reading only the manifest — **zero region files**, no per-cell allocation. Stacked on #230; will rebase onto main after #230 merges. Next: PR 3 (editor model chunk-backed).
Author
Collaborator

PR 3 up: #232 — chunk-backed editor model (ChunkedEditModel, MapEditor.Core). Scoped to the model only.

Delivered: overlay reads (edited chunk / base store / default); materialize-on-edit (memory bounded by the dirty set); FloodMap = manifest default-flip (ocean initializer); incremental save (only dirty regions rewritten, untouched regions byte-identical, emptied regions deleted). 9 new tests including opens the 133M world + edits one cell with a single dirty chunk and incremental save leaves untouched regions byte-identical.

Deliberately deferred to keep it reviewable: undo/redo, autosave/journal, and the EditorGame UI wiring + authoring UX (zoom/overview/brushes) — the last is the slice where you actually see and draw the world (with screenshots). Stacked on #231/#230; rebase after they merge.

**PR 3 up: #232** — chunk-backed editor model (`ChunkedEditModel`, MapEditor.Core). Scoped to the model only. Delivered: overlay reads (edited chunk / base store / default); materialize-on-edit (memory bounded by the dirty set); `FloodMap` = manifest default-flip (ocean initializer); incremental save (only dirty regions rewritten, untouched regions byte-identical, emptied regions deleted). 9 new tests including *opens the 133M world + edits one cell with a single dirty chunk* and *incremental save leaves untouched regions byte-identical*. **Deliberately deferred to keep it reviewable:** undo/redo, autosave/journal, and the `EditorGame` UI wiring + authoring UX (zoom/overview/brushes) — the last is the slice where you actually *see and draw* the world (with screenshots). Stacked on #231/#230; rebase after they merge.
Author
Collaborator

Fase 4 (editor UI) — critical design review persisted

Verdict: go-with-changes. The enabling change is the render path: the editor renders by building a full dense TileMap (_model.ToTileMap()GridRenderer), which can't exist at 133M. Core of the design = render a viewport window, and decompose Fase 4 into sub-slices.

Decisions

  • A (render reuse): window+offset — build a small window TileMap in MapEditor.Core and pass its world offset to the reused GridRenderer; the client's render hot-path stays untouched. (Generalizing GridRenderer to an IReadOnlyWorldMap source is deferred to Fase 7 if wanted.)
  • B (draw target): a fresh 13,344×9,984 ocean (NewOcean) is the main flow; the 2304² map stays a fixture, not the source. A "import legacy world.map" path is kept as a secondary option.
  • C (world on disk): committed dir content/worlds/asterra/ = manifest.bin + regions/. Never under bin/.

Sub-slices

  • PR 4a — model swap + windowed render + New/open/save (the enabler). EditorGame on ChunkedEditModel; viewport-window render (window+offset, rebuilt on view move); New configurable + 13,344×9,984 ocean preset; open/save the content/worlds/asterra/ dir + import a legacy world.map once; chunked (bounded) flood; extend --screenshot with --open/--goto for piloted screenshots. Outcome: open, pan, draw and save the 133M world in the editor.
  • PR 4b — overview/mip + jump-to-coordinate + wider zoom + chunk/region overlay.
  • PR 4c — shaped/large brushes, line/rect/polygon, statics palette (thumbnails/search/tags).
  • Later: undo/redo + autosave/journal (deferred from PR 3), spline/stamp.

Invariants Check (key lines; full walk in-session)

No . Most invariants are N/A (offline authoring tool: no server/wire/sim/persistence-of-game-state). Load-bearing gate: the editor Screen-analog HARD GATE (tools/IsoMmo.MapEditor/CLAUDE.md) — windowing math, chunked flood, tool/coordinate logic go in MapEditor.Core (unit-tested); EditorGame only wires + draws. Client engine-independence ✓ (Core stays MonoGame-free). Persistence ✓ (editor writes authored map content, distinct from runtime world.sav). Multi-platform ✓. Assets ✓ (real art via GridRenderer; missing = not drawn). Docs: update docs/map-editor.md in the slice that changes open/save/New + render.

Server-side validation

N/A here (offline). The server validating the authored world (spawn in-bounds+walkable, catalog hash) is the Fase-7 warm-load, not this slice.

Verification plan

MapEditor.Core unit tests: visible-cell-window bounds (camera+viewport+zoom → cell AABB), bounded chunked flood (no 133M enumeration), New-preset construction, open/import/save wiring. Screenshots (required, visible): piloted via the extended --screenshot (--open <dir> --goto x y + a scripted paint) — fresh ocean, a drawn patch, the imported legacy world. Harness item for 4a: scripted editor input so the screenshot shows authored terrain, not an empty ocean.

## Fase 4 (editor UI) — critical design review persisted **Verdict: go-with-changes.** The enabling change is the render path: the editor renders by building a *full dense* `TileMap` (`_model.ToTileMap()` → `GridRenderer`), which can't exist at 133M. Core of the design = render a **viewport window**, and decompose Fase 4 into sub-slices. ### Decisions - **A (render reuse):** window+offset — build a small window `TileMap` in `MapEditor.Core` and pass its world offset to the reused `GridRenderer`; the client's render hot-path stays untouched. (Generalizing `GridRenderer` to an `IReadOnlyWorldMap` source is deferred to Fase 7 if wanted.) - **B (draw target):** a fresh **13,344×9,984 ocean** (`NewOcean`) is the main flow; the 2304² map stays a fixture, not the source. A "import legacy world.map" path is kept as a secondary option. - **C (world on disk):** committed dir **`content/worlds/asterra/`** = `manifest.bin` + `regions/`. Never under `bin/`. ### Sub-slices - **PR 4a — model swap + windowed render + New/open/save (the enabler).** `EditorGame` on `ChunkedEditModel`; viewport-window render (window+offset, rebuilt on view move); `New` configurable + 13,344×9,984 ocean preset; open/save the `content/worlds/asterra/` dir + import a legacy `world.map` once; chunked (bounded) flood; extend `--screenshot` with `--open`/`--goto` for piloted screenshots. **Outcome: open, pan, draw and save the 133M world in the editor.** - **PR 4b —** overview/mip + jump-to-coordinate + wider zoom + chunk/region overlay. - **PR 4c —** shaped/large brushes, line/rect/polygon, statics palette (thumbnails/search/tags). - Later: undo/redo + autosave/journal (deferred from PR 3), spline/stamp. ### Invariants Check (key lines; full walk in-session) No `✗`. Most invariants are N/A (offline authoring tool: no server/wire/sim/persistence-of-game-state). Load-bearing gate: the **editor `Screen`-analog HARD GATE** (`tools/IsoMmo.MapEditor/CLAUDE.md`) — windowing math, chunked flood, tool/coordinate logic go in `MapEditor.Core` (unit-tested); `EditorGame` only wires + draws. Client engine-independence ✓ (Core stays MonoGame-free). Persistence ✓ (editor writes authored map content, distinct from runtime `world.sav`). Multi-platform ✓. Assets ✓ (real art via `GridRenderer`; missing = not drawn). Docs: update `docs/map-editor.md` in the slice that changes open/save/New + render. ### Server-side validation N/A here (offline). The server validating the authored world (spawn in-bounds+walkable, catalog hash) is the Fase-7 warm-load, not this slice. ### Verification plan `MapEditor.Core` unit tests: visible-cell-window bounds (camera+viewport+zoom → cell AABB), bounded chunked flood (no 133M enumeration), New-preset construction, open/import/save wiring. Screenshots (required, visible): piloted via the extended `--screenshot` (`--open <dir> --goto x y` + a scripted paint) — fresh ocean, a drawn patch, the imported legacy world. Harness item for 4a: scripted editor input so the screenshot shows authored terrain, not an empty ocean.
Author
Collaborator

Fase 7 (chunked world in the game) — critical design review persisted

Verdict: go-with-changes, decomposed into 7a/7b/7c. The whole runtime (movement/combat/creatures/items/spells + client render) is built on TileMap's interface (IsWalkable/HasLineOfSight/GroundZAt/TerrainAt/CanStep/Statics/Width/Height/Spawn), backed today by a dense string[,] — which can't hold 133M cells. So Fase 7 = re-back the map, don't rewrite the consumers, + client streaming + a connect handshake.

Decisions

  • A (7a world): load asterra — but it needs an in-bounds walkable spawn first (Andrea/editor or a scripted set-spawn); the server refuses a world with no valid spawn.
  • Render (7b): client draws a viewport window streamed from the local chunked world (reuse IsoViewport + a bounded chunk LRU; adapt GridRenderer to a windowed source). Not full predictive streaming yet.
  • Re-backing (my call): extract IReadOnlyTileMap and add CompactWorldMap (byte[] terrain indices + Int16 Z + sparse statics + catalog→TerrainDef) implementing it; point World + systems at the interface. TileMap stays for import/tests. Low churn per file.
  • Compact Z (my call): dense Int16[W*H] for O(1) parity with the current GroundZAt.

Sub-slices

  • PR 7a — server compact warm-load. IReadOnlyTileMap + CompactWorldMap + a loader from WorldMapStore; server warm-loads asterra at startup (before connections, no tick I/O) + validates spawn; consumers switched to the interface. Parity unit tests (walkability/LOS/Z match the text map cell-for-cell). No wire change.
  • PR 7b — client chunk streaming. Viewport-window render from the local chunked world; bounded chunk LRU; GridRenderer windowed source. Piloted screenshots of the game on the chunked world.
  • PR 7c — map+catalog hash handshake + ProtocolVersion bump. Client sends its world hash at connect; server rejects mismatch with a diagnostic SystemNotice; WebSocket integration test for the reject path.

Invariants Check (key lines; no ✗ unresolved)

Server-authoritative ✓ (authoritative compact map). Protocol versioned ✗→✓: the connect hash handshake IS a wire change → bump ProtocolVersion.Current in 7c. Single-threaded sim ✓ (warm-load + validate before accepting connections; O(1) in-memory queries; no file/decompress in the tick; workers touch only immutable data). World.cs HARD GATE ✓ (map access delegated; compact backing inside the map component). Screen HARD GATE ✓ (streaming/LRU in a component; GameScreen wires). Client engine-independence ✓ (window math in Client.Core/IsoViewport; GPU glue in Client). Persistence (GameServer) ✓ (map is authored content, distinct from world.sav; no tick I/O). Typed options ✓ (world path/stream budget on GameOptions). Broadcasts/AoI ✓ (unchanged; absolute positions from the compact map). Multi-platform ✓. Assets ✓. Docs+DoD ✓ (architecture.md runtime map path; world-map-format.md handshake).

Server-side validation

Map+catalog hash handshake (mismatch → diagnostic notice, not silent desync) — the one wire addition → ProtocolVersion bump. Walkability/LOS/Z stay server-decided from the compact authoritative map. Release validation: server refuses a world with no in-bounds walkable spawn.

Verification plan

7a: parity harness (CompactWorldMap vs TileMap cell-for-cell on the same content) + startup warm-load/validate + compact memory (no string[,]); no screenshots. 7b: piloted client screenshots on the chunked world (fresh DB) + region-border correctness. 7c: WebSocket integration test (mismatched hash rejected; matching connects).

## Fase 7 (chunked world in the game) — critical design review persisted **Verdict: go-with-changes**, decomposed into 7a/7b/7c. The whole runtime (movement/combat/creatures/items/spells + client render) is built on `TileMap`'s interface (`IsWalkable`/`HasLineOfSight`/`GroundZAt`/`TerrainAt`/`CanStep`/`Statics`/`Width`/`Height`/`Spawn`), backed today by a dense `string[,]` — which can't hold 133M cells. So Fase 7 = **re-back the map, don't rewrite the consumers**, + client streaming + a connect handshake. ### Decisions - **A (7a world):** load **asterra** — but it needs an in-bounds walkable **spawn** first (Andrea/editor or a scripted set-spawn); the server refuses a world with no valid spawn. - **Render (7b):** client draws a **viewport window** streamed from the local chunked world (reuse `IsoViewport` + a bounded chunk LRU; adapt `GridRenderer` to a windowed source). Not full predictive streaming yet. - **Re-backing (my call):** extract **`IReadOnlyTileMap`** and add **`CompactWorldMap`** (byte[] terrain indices + Int16 Z + sparse statics + catalog→TerrainDef) implementing it; point `World` + systems at the interface. `TileMap` stays for import/tests. Low churn per file. - **Compact Z (my call):** dense `Int16[W*H]` for O(1) parity with the current `GroundZAt`. ### Sub-slices - **PR 7a — server compact warm-load.** `IReadOnlyTileMap` + `CompactWorldMap` + a loader from `WorldMapStore`; server warm-loads asterra at startup (before connections, no tick I/O) + validates spawn; consumers switched to the interface. Parity unit tests (walkability/LOS/Z match the text map cell-for-cell). No wire change. - **PR 7b — client chunk streaming.** Viewport-window render from the local chunked world; bounded chunk LRU; `GridRenderer` windowed source. Piloted screenshots of the game on the chunked world. - **PR 7c — map+catalog hash handshake + `ProtocolVersion` bump.** Client sends its world hash at connect; server rejects mismatch with a diagnostic `SystemNotice`; WebSocket integration test for the reject path. ### Invariants Check (key lines; no ✗ unresolved) Server-authoritative ✓ (authoritative compact map). **Protocol versioned ✗→✓: the connect hash handshake IS a wire change → bump `ProtocolVersion.Current` in 7c.** Single-threaded sim ✓ (warm-load + validate before accepting connections; O(1) in-memory queries; no file/decompress in the tick; workers touch only immutable data). `World.cs` HARD GATE ✓ (map access delegated; compact backing inside the map component). `Screen` HARD GATE ✓ (streaming/LRU in a component; `GameScreen` wires). Client engine-independence ✓ (window math in `Client.Core`/`IsoViewport`; GPU glue in `Client`). Persistence (GameServer) ✓ (map is authored content, distinct from `world.sav`; no tick I/O). Typed options ✓ (world path/stream budget on `GameOptions`). Broadcasts/AoI ✓ (unchanged; absolute positions from the compact map). Multi-platform ✓. Assets ✓. Docs+DoD ✓ (architecture.md runtime map path; world-map-format.md handshake). ### Server-side validation Map+catalog hash handshake (mismatch → diagnostic notice, not silent desync) — the one wire addition → `ProtocolVersion` bump. Walkability/LOS/Z stay server-decided from the compact authoritative map. Release validation: server refuses a world with no in-bounds walkable spawn. ### Verification plan 7a: parity harness (`CompactWorldMap` vs `TileMap` cell-for-cell on the same content) + startup warm-load/validate + compact memory (no `string[,]`); no screenshots. 7b: piloted client screenshots on the chunked world (fresh DB) + region-border correctness. 7c: WebSocket integration test (mismatched hash rejected; matching connects).
Author
Collaborator

Fase 7b — design review (client renders the chunked asterra world in-game)

Verdict: go-with-changes. Owner picked warm-load whole (client holds asterra in a CompactWorldMap, ~127 MiB; asterra is flat → no Z sidecar). Reuses 7a's tested code; renderers already window (IsoViewport.VisibleCellBounds), so a future chunk-streaming loader is a drop-in behind IReadOnlyTileMap. No wire change (server already sends absolute positions; the map/catalog hash handshake is 7c).

Invariants Check (each line of CLAUDE.md ## Design checklist):

  • Scope ✓ (exactly the agreed slice) · Server-authoritative ✓ (client is display-only; server validates moves on its asterra from 7a) · GM auth N/A · Identity N/A · Protocol versioned ✓ no wire-shape change → no ProtocolVersion bump (handshake = 7c) · String catalog N/A (load failure = fatal startup log) · Single-threaded sim N/A (server-side) · World.cs gate N/A · Screen gate ✓ (map load in IsoGame composition root; windowing stays in the renderers/ClientWorld, not GameScreen) · Client engine-independence ✓ (window math already in Client.Core; CompactWorldMap in Shared) · Gameplay/Networking sep ✓ · Act-on-instance N/A · Extend-by-type ✓ (source chosen by path/config, not a type switch) · Typed content-def N/A · Server-paced N/A · Persistence N/A (authored content, not world.sav) · Process sep ✓ · Typed options: client hardcodes asterra + a dev --world/ISOMMO_WORLD override (not IConfiguration in a service) · Broadcasts/AoI N/A · Multi-platform ✓ (pure .NET; cross-platform paths) · Assets ✓ (real land/<landArt> or biome tint, same path as today) · Asset naming N/A · ModernUO: UO streams map blocks around the player — we consciously diverge (warm-load whole) for this slice, streaming is the aligned follow-up · Docs & DoD ✓ (update world-map-format.md; screenshots required).

Server-side validation: nothing new — 7b is pure rendering; the trust boundary is unchanged from 7a (every move validated against the server's asterra).

Verification plan: screenshots REQUIRED (visible) — fresh DB → just dev → pilot the client, player spawns on asterra (~5183,4378), capture the rendered continent/coast; confirm a step into ocean is still server-rejected; whole-solution build 0 warnings + dotnet test green.

Definition of Done (7b, delta)

  • The client renders asterra around the player (not the legacy world.map), proven by a piloted screenshot from a fresh DB.
  • No ProtocolVersion bump and no wire-DTO change in 7b (handshake deferred to 7c).
  • Client holds the map behind IReadOnlyTileMap; a dev override still loads the small legacy map.
  • Whole solution builds with zero warnings; dotnet test green.
### Fase 7b — design review (client renders the chunked asterra world in-game) **Verdict: go-with-changes.** Owner picked **warm-load whole** (client holds asterra in a `CompactWorldMap`, ~127 MiB; asterra is flat → no Z sidecar). Reuses 7a's tested code; renderers already window (`IsoViewport.VisibleCellBounds`), so a future chunk-streaming loader is a drop-in behind `IReadOnlyTileMap`. **No wire change** (server already sends absolute positions; the map/catalog hash handshake is 7c). **Invariants Check** (each line of `CLAUDE.md` `## Design checklist`): - Scope ✓ (exactly the agreed slice) · Server-authoritative ✓ (client is display-only; server validates moves on its asterra from 7a) · GM auth N/A · Identity N/A · **Protocol versioned ✓ no wire-shape change → no `ProtocolVersion` bump (handshake = 7c)** · String catalog N/A (load failure = fatal startup log) · Single-threaded sim N/A (server-side) · World.cs gate N/A · **Screen gate ✓** (map load in `IsoGame` composition root; windowing stays in the renderers/`ClientWorld`, not `GameScreen`) · Client engine-independence ✓ (window math already in `Client.Core`; `CompactWorldMap` in `Shared`) · Gameplay/Networking sep ✓ · Act-on-instance N/A · Extend-by-type ✓ (source chosen by path/config, not a type switch) · Typed content-def N/A · Server-paced N/A · Persistence N/A (authored content, not `world.sav`) · Process sep ✓ · Typed options: client hardcodes asterra + a dev `--world`/`ISOMMO_WORLD` override (not `IConfiguration` in a service) · Broadcasts/AoI N/A · **Multi-platform ✓** (pure .NET; cross-platform paths) · **Assets ✓** (real `land/<landArt>` or biome tint, same path as today) · Asset naming N/A · ModernUO: UO streams map blocks around the player — we consciously diverge (warm-load whole) for this slice, streaming is the aligned follow-up · Docs & DoD ✓ (update `world-map-format.md`; screenshots required). **Server-side validation:** nothing new — 7b is pure rendering; the trust boundary is unchanged from 7a (every move validated against the server's asterra). **Verification plan:** screenshots REQUIRED (visible) — fresh DB → `just dev` → pilot the client, player spawns on asterra (~5183,4378), capture the rendered continent/coast; confirm a step into ocean is still server-rejected; whole-solution build 0 warnings + `dotnet test` green. ### Definition of Done (7b, delta) - [ ] The client renders **asterra** around the player (not the legacy `world.map`), proven by a piloted screenshot from a fresh DB. - [ ] No `ProtocolVersion` bump and no wire-DTO change in 7b (handshake deferred to 7c). - [ ] Client holds the map behind `IReadOnlyTileMap`; a dev override still loads the small legacy map. - [ ] Whole solution builds with zero warnings; `dotnet test` green.
Author
Collaborator

Marco aiutami invece di farti le unghie a tema pokemon 💅

Epic completata: formato chunked + migrazione editor/client/server su main (#230–#236). Il mondo asterra gira sul formato chunked.

Marco aiutami invece di farti le unghie a tema pokemon 💅 Epic completata: formato chunked + migrazione editor/client/server su main (#230–#236). Il mondo `asterra` gira sul formato chunked.
panda closed this issue 2026-08-02 00:47:18 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo#229
No description provided.