Skip to content

3D maps: surface-graph (z-levels, ramps, overhangs), network sync, feature negotiation - #3

Draft
OddlyDoddly wants to merge 7 commits into
feature/vulkanfrom
claude/exciting-ptolemy-0g5qdb
Draft

3D maps: surface-graph (z-levels, ramps, overhangs), network sync, feature negotiation#3
OddlyDoddly wants to merge 7 commits into
feature/vulkanfrom
claude/exciting-ptolemy-0g5qdb

Conversation

@OddlyDoddly

@OddlyDoddly OddlyDoddly commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Context

Armagetron is currently strictly 2D (tCoord is x,y only; map <Point> is x,y; protobufs carry no Z; the floor renders at z=0). This PR adds a third dimension as a single change set: three z-levels (basement/ground/air) joined by fixed-angle ramps, with true vertical overhangs (tunnels-under-bridges, stacked highways), custom-shaped arenas, and Tron-Legacy building blocks.

Architecture: surface-graph (2.5D manifold)

tCoord stays 2D — no engine-wide coordinate refactor. Each drivable surface (floor/ramp/tunnel) is its own planar eGrid chart plus a z-mapping; surfaces are stitched by portals. Within a surface, all existing eGrid / movement / rubber / winding / wall code works unchanged. eGrid is a planar DCEL and cannot represent overlapping (x,y), so overhangs require multiple grids + portals — which is also what maximizes reuse. Z is derived per-surface; frames are identity (local == world), so a portal crossing keeps (x,y) and only changes surface id and derived height.

With the 3D feature off (and for all legacy maps), the game is byte-identical 2D.

What's implemented

Data model (src/engine/eSurface.*, eWorld.*)

  • eSurface/ePortal: a chart wrapping an eGrid with a z-mapping, footprint, and seam portals (with frame mapping). eWorld: surface registry; surface 0 wraps the arena's primary grid so existing eGrid* code is untouched. Buildings stored for extrusion.
  • eGameObject carries surfaceId_; gPlayerWall/gNetPlayerWall carry it too; gPortalWall is a non-massive rim marking a seam.

Map format 2.0 (resource/proto/map-2.0.dtd)

  • <Floor> (level + material grid/light/glass), <Ramp> (from/to level + direction), <Building> (footprint + height + style). Declared <Map version="200"> to avoid colliding with the existing version="2" zones format. Parser builds surfaces + auto-stitched portals, gated on map>=200 AND the negotiated 3D feature. Two sample maps (test-2.0-ground, test-2.0-stacked with a real overhang + ramp + tower).

Portal physics (eGameObject::Move)

  • At a portal seam the cycle hands off to the neighbour surface's grid (swap grid, set surfaceId, derive z, re-find face, resume the same step). Portal walls are non-massive ⇒ you don't die on a slope. Ramp end-seams are portals (drawn into both the ramp grid and the floor grid); the ramp's long sides stay massive guard rails.

Feature negotiation (src/network/nFeatures.*, protocol v23)

  • Server advertises a named THREE_D capability over nVersion/nVersionFeature. It is active only when every peer supports it; otherwise the session runs the 2D protocol and never serializes Z/surface data. Mandatory features would reject the join.

Network sync (feature-gated)

  • CycleSync.surface_id and PlayerWallSync.surface_id carry the authoritative surface (never derived from x,y on the client). Trail-wall collision is already per-surface (each wall is drawn into the cycle's current grid).

Rendering

  • Walls/trails draw at their surface's height in the legacy immediate-mode path (se_SurfaceBaseZ). Buildings stored with footprint+height+style for extrusion.

Verification

  • ✅ Full autotools build links cleanly (armagetronad_main + dedicated).
  • ✅ Unit test src/test/eSurfaceTest.cpp (14 checks) passes: footprint containment, per-level floor lookup at overlapping (x,y), ramp→floor portal stitching, z-interpolation, portal frame mapping, building storage.
  • ✅ Sample maps validate against map-2.0.dtd (xmllint --valid).

Boundary with the in-flight Vulkan rewrite

The GPU-side rendering of multi-height floors, glass floors (look-up effect), decals/road-signs, and building extrusion shaders lives in the modern renderer (src/render/gl/*, se_RenderModernFloor, the instanced WallInstance + GLSL) that the parallel graphics rewrite owns. This PR provides the data and engine hooks (surface base-z, material enum, building records, per-wall surface id) and applies height in the legacy wall path; the modern-renderer pieces are left as additive integration points (e.g. a base-z field on WallInstance) to avoid conflicting with that work and because they can't be visually verified in a headless build. An in-game map editor remains a separate follow-up; the data model and format are designed to support it.

https://claude.ai/code/session_01NGicPQEn7UpifzRD2ouSfZ

claude added 5 commits June 16, 2026 17:57
…ure negotiation

This is PR #1 of the map-system 3D rework. It introduces the surface-graph
(2.5D manifold) foundation without changing 2D gameplay: tCoord stays 2D and
all existing per-grid physics is untouched. Each drivable surface owns its own
planar eGrid, joined by portals, with z derived per-surface for rendering.

Engine data model:
- eSurface / ePortal (src/engine/eSurface.{h,cpp}): a chart wrapping an eGrid
  with a z-mapping (flat or fixed-slope ramp), footprint, and seam portals.
- eWorld (src/engine/eWorld.{h,cpp}): surface registry. Surface 0 wraps the
  arena's primary grid, so existing eGrid* code is unchanged; ramps auto-stitch
  portals to the floors they meet (StitchRamp).
- eGameObject gains surfaceId_ (default 0); gPlayerWall gains surfaceId_;
  gPortalWall (non-massive rim) marks portal seams.

Map format 2.0:
- resource/proto/map-2.0.dtd adds <Floor>, <Ramp>, <Building> primitives and a
  basement/ground/air level enum. Encoded as <Map version="200"> to avoid
  colliding with the existing version="2" zones format.
- gParser parses the new primitives into surfaces + portals, gated on
  map>=200 AND the negotiated 3D feature; legacy maps are byte-identical.
- Sample maps: test-2.0-ground (regression) and test-2.0-stacked (overhang).

Feature negotiation:
- New protocol version 23 ("0.5_3dmaps") and nFeatures (src/network), a named
  capability layer over nVersion/nVersionFeature. THREE_D is active only when
  every peer supports it; otherwise the session runs the 2D protocol and never
  serializes Z/surface data.

Tests: src/test/eSurfaceTest.cpp exercises footprint containment, per-level
floor lookup, ramp portal stitching, z-interpolation, and portal mapping.

https://claude.ai/code/session_01NGicPQEn7UpifzRD2ouSfZ
Cycles now hand off between surfaces at portal seams. Because surface frames
are identity (local == world), a crossing keeps (x,y) and only switches the
grid, surfaceId, and derived z:

- eWall gains a virtual Portal() (NULL by default); gPortalWall returns its
  ePortal. eGameObject::Move detects a portal at the crossed edge and, since
  portal walls are non-massive (no death -> "don't die on a slope"), swaps to
  the neighbour surface's grid, re-finds the face, and resumes the same step.
- Parser draws ramp end-seams as non-massive portal walls and mirrors each seam
  into the adjacent floor's grid, so a cycle can drive on/off the ramp at the
  shared world seam; the ramp's long sides stay massive guard rails.
- eWorld::Connect (public) creates+registers a portal on both surfaces.

Legacy 2D maps have no portal walls, so Move() behaviour is unchanged.
Cycles and walls now carry their surface across the wire, but only when the
THREE_D feature is negotiated on for everyone; otherwise the new fields are
never serialized and the session stays byte-identical to the 2D protocol.

- gCycle.proto/CycleSync: optional surface_id (12). Written from
  eGameObject::SurfaceId() in WriteSync, read back authoritatively in ReadSync
  (never derived from x,y -- overhangs make that ambiguous).
- gWall.proto/PlayerWallSync: optional surface_id (12). gNetPlayerWall carries
  surfaceId_, set from its cycle at creation and propagated to gPlayerWall, so
  the renderer can draw each wall at the right height.

Trail-wall collision is already per-surface because each wall is drawn into the
cycle's current grid; the synced id is the authoritative correction/render hint.
Walls and trails now draw at the height of the surface they belong to:
se_SurfaceBaseZ(surfaceId) looks up the surface's BaseZ via the arena's eWorld,
and gNetPlayerWall::RenderNormal offsets its vertices by it. 2D walls
(surfaceId 0) get z=0, so legacy maps are unchanged.

This is applied in the legacy immediate-mode wall path. The modern instanced
path (render/gl/gl_wall_instanced) needs a base-z added to WallInstance and its
GLSL attribute layout; that, plus multi-height floor rendering, glass floors and
building extrusion, is the integration surface for the in-flight Vulkan rewrite
(it owns those files and shaders) and is left as a documented hook.
Buildings now carry their footprint, height, level, and style into the world
(eWorld::Buildings) so the renderer can extrude them into Tron-Legacy blocks;
their footprint is still drawn as a solid collision wall so they block cycles
immediately. The stacked sample map gains a block tower, and the unit test
covers building storage.
@OddlyDoddly OddlyDoddly changed the title 3D map foundation: surface-graph data model, map-2.0 format, feature negotiation (PR #1) 3D maps: surface-graph (z-levels, ramps, overhangs), network sync, feature negotiation Jun 16, 2026
claude added 2 commits June 16, 2026 22:55
… extrusion

Now that the Vulkan backend is merged into this branch, complete the 3D render
integration:

- gl_wall_instanced: add a per-instance zbase (surface base height) to
  WallInstance, the GLSL vertex shader (location 6), and the attribute setup, so
  the default modern-GL instanced wall path draws walls/trails at their surface
  height. gWall.cpp sets inst.zbase from se_SurfaceBaseZ(). The legacy immediate
  path (used by the Vulkan renderer via the rRenderer Vertex interface) already
  carries the offset, so multi-level walls render under both backends.
- parseBuilding extrudes the footprint side walls to the building's height
  (scaled by sizeMultiplier) instead of the default rim height, so Buildings
  render as Tron-Legacy blocks while still acting as solid obstacles.

Verified: full autotools build links both with GL (default) and with
--with-vulkan (vk_renderer/vk_context compile, SPIR-V generated, binary linked);
surface unit test passes against the full client libs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants