Authoritative terrain Z — UO-style diagonal heightfield (design + 3-slice plan) #224

Closed
opened 2026-07-30 22:26:53 +02:00 by panda · 1 comment
Collaborator

Authoritative per-point terrain Z — a UO-style diagonal heightfield. Z is a signed coordinate of the terrain (not a material property, not biome-derived); Z differences between neighbouring points deform the isometric surface diagonally. rocky_ground/mountain_rock are the first terrains to build and verify on, but it must work for any terrain. The final world map is hand-authored in the editor — this feature does not generate geography.

Design agreed via the critical-design-review skill (verdict: go-with-changes → go once the three calls below were made). This issue is the master tracker; the completed Invariants Check + Verification plan live here per CLAUDE.md.

Agreed design decisions

  1. Rendering — the ground is already a per-pixel SDF blend on a windowed screen quad (PR #223), not per-tile dual tiles. Z integrates by replacing the quad with a windowed triangulated grid mesh: one vertex per grid point in the visible window at ToScreen(x,y,GroundZAt), texcoord (x,y), two UO-diagonal triangles per cell; ground_blend.fx stays unchanged and the blend deforms for free (no per-tile masks to keep coincident). Rejected: reintroducing the per-tile dual-tile mesh model the original prompt assumed.
  2. Sequencing — the renderer depends on the windowed SDF mesh, which lives only on PR #223 (unmerged). Decision: merge #223 first, then branch Z off main (linear history, no stacked PR).
  3. Delivery3 sliced PRs, build green between each:
    • A — domain + format + math (Phases 1–2): GroundZAt, centralized Min/MaxTerrainZ (−225/+255), ZScale (4 px/Z), MaxStepZ (2); TextMapFormat numeric Z block; ToScreen(x,y,z), UO triangulation, point-in-triangle — all pure, no visible change.
    • B — renderer + world objects + picking + fixture (Phases 3–4): windowed grid mesh, foot points + depth sort fold in Z, height-aware picking, rocky/mountain fixture; screenshots.
    • C — editor Z tools + server walkability + docs (Phases 5–6): Raise/Lower/Set brush + hover, kill clone-per-stroke, MaxStepZ gate + mover audit, docs/architecture.md.

Invariants Check (CLAUDE.md ## Design checklist)

  • Scope ✓ owner-requested; sliced, not one epic.
  • Server-authoritative ✓ Z is authoritative map data (identical both ends); server validates abs(GroundZAt(into)−GroundZAt(from)) ≤ MaxStepZ; client never sends a Z result.
  • GM authorization N/A — no admin action this slice.
  • Identity model N/A.
  • Protocol versioned ✓/watch — intent: no wire change (Z from the shared map, move stays move+direction); if the audit finds a real wire need, STOP and bump ProtocolVersion.Current in that change.
  • String catalog N/A — no new player-facing copy.
  • Single-threaded sim ✓ server reads GroundZAt on the sim thread; no new lock.
  • World.cs HARD GATE ✓ step-Z rule in MovementSystem/TileMap.CanStep, not World.cs.
  • Screen HARD GATE (client) ✓ mesh/picking in renderer + Client.Core units; GameScreen only wires.
  • Client engine-independenceToScreen(x,y,z), triangulation, point-in-triangle in Client.Core; only vertex upload/draw in Client.
  • Gameplay/Networking separation ✓ walkability in Gameplay/; no transport touched.
  • Act on the instance ✓ editor mutates the z-grid via SetZ (clamped once); no side-collection shadow.
  • Extend by type, not switch N/A — Z is a coordinate, no new variant family.
  • Server-paced actions N/A.
  • Persistence (GameServer) ✓ Z is authored map content (world.map via IMapFormat), not world.sav; no entity-save/version change; no tick-loop file IO.
  • Persistence (Auth) N/A.
  • Process separation ✓ Auth untouched; only the shared map contract carries Z.
  • Typed optionsMaxStepZ = GameOptions field (default once); ZScale/Min/MaxTerrainZ = single Shared/Client.Core constants.
  • Broadcasts / AoI N/A — Z adds no observable entity/event.
  • Multi-platform ✓ DesktopGL DrawUserIndexedPrimitives, no OS-specific API; server pure .NET; shader stays pre-compiled .xnb.
  • Assets required / naming N/A — no new art/ids (textures stay flat; Z is geometry).
  • ModernUO as reference ✓ cite ClassicUO for the diagonal/winding/UV (test-pinned) + ModernUO for Z data + server walkability; note we diverge with per-pixel SDF over the mesh.
  • Docs & DoD in the same changedocs/architecture.md (durable model, no volatile byte layout); each PR carries its DoD delta.

Server-side validation (trust boundary)

Intent unchanged (move+direction). Server validates in MovementSystem: in-bounds → walkable → no blocker/creature (existing) → new abs(GroundZAt(into)−GroundZAt(from)) ≤ GameOptions.MaxStepZ. Audit all movers: player, creature/AI, spawn, teleport (resolves to a walkable dest, no step-delta), resurrection/relocation, GM. A forged client Z can't become authoritative.

Verification plan

  • Shared/domain unit: Z0 default on an old map; read ±Z / −225 / +255; reject −226/+256; brush clamps at both extremes (no wrap); exact round-trip; CanStep at 0 / MaxStepZ / MaxStepZ+1.
  • Client.Core unit: ToScreen(x,y,0)==flat; +1 Z==Y−4; adjacent shared vertices bit-identical; diagonal/winding; point-in-triangle on both triangles; picking a ramp + overlapping surfaces; facing invariant under Z.
  • GameServer (World unit + WebSocket flow): allowed step accepted; too-tall step/drop rejected; client can't forge Z; AI respects the surface; teleport semantics.
  • Screenshots (debug-harness, fresh DB): the rocky/mountain fixture in client (ramp no cracks, base+blend coincide, static/mobile sit on surface, no float/sink) and editor (Raise/Lower/Set, hover Z, save→reopen). I'll add a /tp to reach the fixture.
  • Perf: a brush stroke on 2304×1728 touches only its cells (no full clone) — instrumented.

Definition of Done

  • every terrain position has a signed authoritative Z; valid range centralized −225 ≤ Z ≤ +255.
  • old maps load fully at Z0; −z*4 in one shared place.
  • diamond uses four Z + two UO-conformant triangles; adjacent tiles show no cracks; texture/variants/dual-grid blend follow the same geometry; Z0 visually equals the previous version.
  • player, creatures, statics, items, corpses, ground VFX use the quoted foot point; depth sort correct; facing still X/Y.
  • mouse picks an elevated tile correctly (editor hover, targeting, drop/place, every TileUnderCursor caller); culling correct via per-chunk min/max Z (not global).
  • Map Editor Raise/Lower/Set persists Z; a stroke does not rebuild/clone the whole map; save/reopen preserves.
  • server accepts an allowed step, rejects an excessive delta; no client-supplied Z is authoritative; all movers audited.
  • rocky/mountain fixture verified in editor and client (not overwriting the world map).
  • all repo tests pass; client + editor build on DesktopGL with no OS-specific API; durable docs updated.
  • CDR + verification plan + actual results reported in the PR(s) per CLAUDE.md.

Out of scope (guards)

No auto-mountain generator; no biome→Z; Z0/Z<0 ≠ water (hydrology ⟂ elevation); no voxels; no multilayer/caves/overlapping surfaces at the same (x,y) — this slice is a single heightfield; no slope PNGs / fake vertical walls; no client-authoritative Z; no full-map scan per frame/click/stroke; never overwrite the user's world map.


Blocked on: PR #223 merged to main (owner merge) — then Slice A branches off main.

Authoritative **per-point terrain Z** — a UO-style diagonal heightfield. Z is a signed coordinate of the terrain (not a material property, not biome-derived); Z differences between neighbouring points deform the isometric surface diagonally. `rocky_ground`/`mountain_rock` are the first terrains to build and verify on, but it must work for **any** terrain. The final world map is hand-authored in the editor — this feature does **not** generate geography. Design agreed via the `critical-design-review` skill (verdict: go-with-changes → go once the three calls below were made). This issue is the master tracker; the completed Invariants Check + Verification plan live here per CLAUDE.md. ## Agreed design decisions 1. **Rendering** — the ground is already a per-pixel **SDF blend on a windowed screen quad** (PR #223), not per-tile dual tiles. Z integrates by **replacing the quad with a windowed triangulated grid mesh**: one vertex per grid point in the visible window at `ToScreen(x,y,GroundZAt)`, texcoord `(x,y)`, two UO-diagonal triangles per cell; `ground_blend.fx` stays **unchanged** and the blend deforms for free (no per-tile masks to keep coincident). Rejected: reintroducing the per-tile dual-tile mesh model the original prompt assumed. 2. **Sequencing** — the renderer depends on the windowed SDF mesh, which lives only on PR #223 (unmerged). Decision: **merge #223 first, then branch Z off `main`** (linear history, no stacked PR). 3. **Delivery** — **3 sliced PRs**, build green between each: - **A — domain + format + math** (Phases 1–2): `GroundZAt`, centralized `Min/MaxTerrainZ` (−225/+255), `ZScale` (4 px/Z), `MaxStepZ` (2); `TextMapFormat` numeric Z block; `ToScreen(x,y,z)`, UO triangulation, point-in-triangle — all pure, no visible change. - **B — renderer + world objects + picking + fixture** (Phases 3–4): windowed grid mesh, foot points + depth sort fold in Z, height-aware picking, rocky/mountain fixture; screenshots. - **C — editor Z tools + server walkability + docs** (Phases 5–6): Raise/Lower/Set brush + hover, kill clone-per-stroke, `MaxStepZ` gate + mover audit, `docs/architecture.md`. ## Invariants Check (CLAUDE.md `## Design checklist`) - **Scope** ✓ owner-requested; sliced, not one epic. - **Server-authoritative** ✓ Z is authoritative map data (identical both ends); server validates `abs(GroundZAt(into)−GroundZAt(from)) ≤ MaxStepZ`; client never sends a Z result. - **GM authorization** N/A — no admin action this slice. - **Identity model** N/A. - **Protocol versioned** ✓/watch — intent: **no wire change** (Z from the shared map, move stays `move+direction`); if the audit finds a real wire need, STOP and bump `ProtocolVersion.Current` in that change. - **String catalog** N/A — no new player-facing copy. - **Single-threaded sim** ✓ server reads `GroundZAt` on the sim thread; no new lock. - **`World.cs` HARD GATE** ✓ step-Z rule in `MovementSystem`/`TileMap.CanStep`, not `World.cs`. - **`Screen` HARD GATE (client)** ✓ mesh/picking in renderer + Client.Core units; `GameScreen` only wires. - **Client engine-independence** ✓ `ToScreen(x,y,z)`, triangulation, point-in-triangle in `Client.Core`; only vertex upload/draw in `Client`. - **Gameplay/Networking separation** ✓ walkability in `Gameplay/`; no transport touched. - **Act on the instance** ✓ editor mutates the z-grid via `SetZ` (clamped once); no side-collection shadow. - **Extend by type, not switch** N/A — Z is a coordinate, no new variant family. - **Server-paced actions** N/A. - **Persistence (GameServer)** ✓ Z is authored map content (`world.map` via `IMapFormat`), not `world.sav`; no entity-save/version change; no tick-loop file IO. - **Persistence (Auth)** N/A. - **Process separation** ✓ Auth untouched; only the shared map contract carries Z. - **Typed options** ✓ `MaxStepZ` = `GameOptions` field (default once); `ZScale`/`Min/MaxTerrainZ` = single Shared/Client.Core constants. - **Broadcasts / AoI** N/A — Z adds no observable entity/event. - **Multi-platform** ✓ DesktopGL `DrawUserIndexedPrimitives`, no OS-specific API; server pure .NET; shader stays pre-compiled `.xnb`. - **Assets required / naming** N/A — no new art/ids (textures stay flat; Z is geometry). - **ModernUO as reference** ✓ cite ClassicUO for the diagonal/winding/UV (test-pinned) + ModernUO for Z data + server walkability; note we diverge with per-pixel SDF over the mesh. - **Docs & DoD in the same change** ✓ `docs/architecture.md` (durable model, no volatile byte layout); each PR carries its DoD delta. ## Server-side validation (trust boundary) Intent unchanged (`move+direction`). Server validates in `MovementSystem`: in-bounds → walkable → no blocker/creature (existing) → **new** `abs(GroundZAt(into)−GroundZAt(from)) ≤ GameOptions.MaxStepZ`. Audit **all movers**: player, creature/AI, spawn, teleport (resolves to a walkable dest, no step-delta), resurrection/relocation, GM. A forged client Z can't become authoritative. ## Verification plan - **Shared/domain unit:** Z0 default on an old map; read ±Z / −225 / +255; reject −226/+256; brush clamps at both extremes (no wrap); exact round-trip; `CanStep` at 0 / `MaxStepZ` / `MaxStepZ+1`. - **Client.Core unit:** `ToScreen(x,y,0)`==flat; `+1 Z`==`Y−4`; adjacent shared vertices bit-identical; diagonal/winding; point-in-triangle on both triangles; picking a ramp + overlapping surfaces; facing invariant under Z. - **GameServer (`World` unit + WebSocket flow):** allowed step accepted; too-tall step/drop rejected; client can't forge Z; AI respects the surface; teleport semantics. - **Screenshots (debug-harness, fresh DB):** the rocky/mountain fixture in client (ramp no cracks, base+blend coincide, static/mobile sit on surface, no float/sink) and editor (Raise/Lower/Set, hover Z, save→reopen). I'll add a `/tp` to reach the fixture. - **Perf:** a brush stroke on 2304×1728 touches only its cells (no full clone) — instrumented. ## Definition of Done - [ ] every terrain position has a signed authoritative Z; valid range centralized `−225 ≤ Z ≤ +255`. - [ ] old maps load fully at Z0; `−z*4` in one shared place. - [ ] diamond uses four Z + two UO-conformant triangles; adjacent tiles show no cracks; texture/variants/dual-grid blend follow the same geometry; Z0 visually equals the previous version. - [ ] player, creatures, statics, items, corpses, ground VFX use the quoted foot point; depth sort correct; facing still X/Y. - [ ] mouse picks an elevated tile correctly (editor hover, targeting, drop/place, every `TileUnderCursor` caller); culling correct via per-chunk min/max Z (not global). - [ ] Map Editor Raise/Lower/Set persists Z; a stroke does not rebuild/clone the whole map; save/reopen preserves. - [ ] server accepts an allowed step, rejects an excessive delta; no client-supplied Z is authoritative; all movers audited. - [ ] rocky/mountain fixture verified in editor and client (not overwriting the world map). - [ ] all repo tests pass; client + editor build on DesktopGL with no OS-specific API; durable docs updated. - [ ] CDR + verification plan + actual results reported in the PR(s) per CLAUDE.md. ## Out of scope (guards) No auto-mountain generator; no biome→Z; Z0/Z<0 ≠ water (hydrology ⟂ elevation); no voxels; no multilayer/caves/overlapping surfaces at the same (x,y) — this slice is a **single heightfield**; no slope PNGs / fake vertical walls; no client-authoritative Z; no full-map scan per frame/click/stroke; never overwrite the user's world map. --- _Blocked on: PR #223 merged to `main` (owner merge) — then Slice A branches off main._
Author
Collaborator

Marco aiutami invece di farti le unghie a tema pokemon 💅

Risolta: terrain Z heightfield autoritativo su main (#225 · #227 · #228).

Marco aiutami invece di farti le unghie a tema pokemon 💅 Risolta: terrain Z heightfield autoritativo su main (#225 · #227 · #228).
panda closed this issue 2026-08-02 00:47:15 +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#224
No description provided.