Four independent decomp studies (Opus 4.8 x2, Sonnet 4.6, external Codex) converge: retail carries the cell through the collision sweep (validate_transition advances curr_cell only on an accepted move, reverts on a block) and commits it in SetPositionInternal — it never re-derives membership from a static resting position. acdream already ports the sweep machinery (sp.CurCellId/CheckCellId, ValidateTransition, CheckOtherCells) but ResolveWithTransition discards the swept cell and re-derives statically via ResolveCellId (PhysicsEngine.cs:909/928) — the root of the 0170<->0031 doorway/cellar ping-pong. The do_not_load_cells prune is secondary (static/cross-cell lists), not the anti-flicker; W2b was doubly misplaced and is reverted. Render: one PView::ConstructView portal traversal over the same cell graph, rooted at the physics current cell; seen_outside (not a dungeon flag) gates landscape; the outside draws through exit portals clipped to the doorway (no blue-hole, no stencil split). Dungeons/interiors share the machinery; "underground" is emergent. Design doc lays out the staged, evidence-first rewrite (Stage 0 diagnostic -> Stage 1 transition-owned membership [visual gate] -> Stage 2 CELLARRAY/prune parity -> Stages 3-5 render root + PView seal + entity clip). Adds the shared research prompt and all four study reports as the grounding record. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
11 KiB
Markdown
129 lines
11 KiB
Markdown
# Research task — Retail AC: cell transitions, underground/dungeons, and seamless inside/outside rendering
|
||
|
||
> **Shared prompt for a multi-model study (2026-06-02).** The same prompt is run on
|
||
> several models (Opus 4.6/4.7/4.8, Sonnet 4.6, and an external model) so we can compare
|
||
> independent reads before committing to an architecture. Study the **source** (retail
|
||
> decomp + reference repos) and **cite everything** — do not guess. Depth + citations
|
||
> matter far more than brevity.
|
||
|
||
## 0. Why this study exists (context)
|
||
|
||
**acdream** is a modern C# .NET port of the retail Asheron's Call client (Sept 2013 EoR
|
||
build). The rule: *the code is modern, the behavior is retail.* Every AC-specific algorithm
|
||
is ported faithfully from the named retail decomp.
|
||
|
||
We are at an architecture decision and want ground truth before choosing. Two coupled
|
||
problems:
|
||
|
||
1. **Cell-membership flicker (physics).** The player's "current cell" ping-pongs at
|
||
boundaries — at a near-static position the cell flips e.g. `0xA9B40170` (indoor cottage
|
||
vestibule) ↔ `0xA9B40031` (outdoor landcell) every physics tick, and also
|
||
`vestibule ↔ room` and inside the cellar. Root finding so far: acdream runs retail's
|
||
collision *sweep* but then **discards the swept cell** and **re-derives the cell from the
|
||
final static position every tick** (`PhysicsEngine.ResolveCellId`), which flips as the
|
||
collision push-back jitters the end position ±~8 cm across the boundary.
|
||
|
||
2. **Non-seamless indoor render.** Standing inside a cottage/cellar the interior does not
|
||
seal: the ceiling isn't capped, the doorway opening shows the blue clear-color instead of
|
||
the real outside (no sky / no rain visible through the door), entities/particles bleed
|
||
through walls, and at the threshold the view strobes between "indoor (incomplete)" and
|
||
"outdoor." acdream's render maintains its **own** cell/visibility system separate from
|
||
physics; we believe retail renders inside+outside seamlessly through a single
|
||
portal-visibility traversal.
|
||
|
||
We have a candidate fix direction ("track the cell through the transition sweep like
|
||
retail's `validate_transition` + `change_cell`, drop the static re-derive, add the
|
||
`do_not_load_cells` prune, and make the render obey one portal-visibility traversal"), but
|
||
we want a **solid, decomp-grounded understanding of how retail ACTUALLY does all of this**
|
||
before we commit — patches/guesses in this exact area have failed ~10× historically.
|
||
|
||
## 1. Your deliverable
|
||
|
||
A comprehensive, decomp-cited markdown report. For **every** non-trivial claim, cite the
|
||
retail function **name + address** (from the decomp) or the reference **file:line** you
|
||
verified it from. Include short pseudocode where it clarifies control flow. End with a
|
||
concrete "Recommended acdream architecture" section (questions D14–16).
|
||
|
||
Write your report to the output path given to you (e.g.
|
||
`docs/research/2026-06-02-retail-cell-render-study-<model>.md`).
|
||
|
||
## 2. Sources to study
|
||
|
||
**Primary oracle — the retail decomp (study this first and most):**
|
||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — 1.4 M lines of named pseudo-C.
|
||
Grep by `Class::method` (e.g. `CTransition::validate_transition`).
|
||
- `docs/research/named-retail/acclient.h` — verbatim retail struct definitions.
|
||
- `docs/research/named-retail/symbols.json` — name ↔ address index (grep by name or addr).
|
||
|
||
**Reference repos (cross-check at least two per topic; the intersection is usually truth):**
|
||
- `references/ACE/` — server-side C# physics port. `Source/ACE.Server/Physics/` has
|
||
`Common/` (ObjCell, EnvCell, LandCell, Position), `Animation/` and the transition/
|
||
sphere-path/cell logic. Authoritative C# reading of `find_cell_list`, `change_cell`,
|
||
`Transition`, `SpherePath`.
|
||
- `references/ACViewer/` — MonoGame client that renders world + dungeons. `Physics/Common/`
|
||
(EnvCell, ObjCell, CellArray) and `Render/` (how cells/portals are drawn).
|
||
- `references/WorldBuilder/` — the render base acdream extracted from. EnvCell/portal/
|
||
visibility/scenery managers; how it draws interiors + the (flat-stencil) inside/outside
|
||
split it uses.
|
||
- `references/Chorizite.ACProtocol/`, `references/AC2D/`, `references/holtburger/` — use as
|
||
relevant (struct field order, simpler client confirmations).
|
||
|
||
**acdream's current code (so your synthesis is actionable, not abstract):**
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs` — `ResolveCellId` (~:272), `ResolveWithTransition` (~:651, see the two `ResolveCellId(sp.GlobalSphere[0].Origin,…)` calls at ~:909/:928 that discard the swept cell).
|
||
- `src/AcDream.Core/Physics/CellTransit.cs` — `FindCellList`/`FindCellSet`/`BuildCellSetAndPickContaining`, `FindTransitCellsSphere`, `AddAllOutsideCells`, `CheckBuildingTransit` (note: NO `do_not_load_cells` prune today).
|
||
- `src/AcDream.Core/Physics/TransitionTypes.cs` — `SpherePath` (`CheckCellId`/`CheckPos`, only set at `InitPath`/reset — NOT advanced through the sweep), `Transition` (`FindEnvCollisions`, `CheckOtherCells`).
|
||
- `src/AcDream.Core/World/Cells/` — the W1 unified cell graph (`ObjCell`/`EnvCell`/`LandCell`/`CellGraph`/`CellPortal`).
|
||
- `src/AcDream.App/Rendering/CellVisibility.cs` + `PortalVisibilityBuilder.cs` + `GameWindow.cs` (terrain/shell/entity draw gates ~:7150–7420).
|
||
|
||
## 3. Decomp anchors (verified starting points — confirm and expand; find more yourself)
|
||
|
||
Physics / cell tracking:
|
||
- `CPhysicsObj::change_cell` @ `0x00513390` (pseudo_c ~:281192) — the leave/enter setter.
|
||
- `CPhysicsObj::SetPositionInternal` @ `0x00515330` (~:283399) — reads `arg2->sphere_path.curr_cell`, calls `change_cell` only when it differs.
|
||
- `CTransition::validate_transition` @ `0x0050aa70` (~:272547) — advances `sphere_path.curr_cell = check_cell` on an accepted move (~:272608-272619); resets to `curr_cell` on a block/slide (~:272593).
|
||
- `CTransition::check_collisions` @ `0x0050aa00` (~:272530); `CTransition::check_other_cells` @ `0x0050ae50` (~:272717) — calls `find_cell_list`.
|
||
- `CTransition::transitional_insert` @ `0x0050b6f0` (~:273137) — the sweep stepper.
|
||
- `CObjCell::find_cell_list` @ `0x0052b4e0` (~:308742) — builds the cell array, picks the containing cell (`*arg5`), applies the `do_not_load_cells` prune (~:308829-308867).
|
||
- `CObjCell::GetVisible` (pseudo_c ~:308209) magnitude dispatch; `CEnvCell::GetVisible`, `CLandCell::GetVisible`, `CLandCell::add_all_outside_cells`, `CObjCell::point_in_cell` (vtable +0x84), `CEnvCell::find_transit_cells`.
|
||
|
||
Cell structs (acclient.h): `CObjCell` (:30915), `CEnvCell` (:32072), `CLandCell` (:31886),
|
||
`CSortCell` (:31880), `CCellPortal` (:32300), `CBldPortal` (:32094), `CellStruct` (:32275),
|
||
plus `SPHEREPATH`, `CELLARRAY`, `Position`.
|
||
|
||
Rendering / visibility (verify addresses; these are from prior notes):
|
||
- `ConstructView` (~:433750 / :433827), `InitCell` (~:432896),
|
||
`CObjCell::find_visible_child_cell` (~:311397). Find the full PVS/PView traversal,
|
||
the EnvCell draw path, how exit portals / `seen_outside` feed render, terrain/sky gating.
|
||
|
||
## 4. Questions to answer (be comprehensive + cited)
|
||
|
||
### A. Cell membership & transitions (physics)
|
||
1. How does retail represent and store "the cell I'm in" (`curr_cell`)? When/where is it updated? Trace the full chain: per-step sweep → `find_cell_list` → `validate_transition` advance → `SetPositionInternal`/`change_cell`.
|
||
2. Exactly how does `find_cell_list` build the candidate cell array, and how does it pick the single containing cell (`*arg5`)? What is the `do_not_load_cells` prune — when is it set, what does it remove, and what stability does it buy?
|
||
3. **Precisely how does retail avoid cell flicker** at a doorway / indoor↔outdoor / room↔room? Is it directed portal-crossing, swept-path containment with accept-on-move, the prune, `point_in_cell` semantics, or a combination? What guarantees a blocked/standing-still step does NOT change the cell?
|
||
4. How does a player transition indoor→outdoor (exit) and outdoor→indoor (enter)? Between interior cells? What do `CCellPortal` vs `CBldPortal` do, and how does the exit portal / outdoor landcell get added and chosen?
|
||
5. Is the cell ARRAY (for collision) the same mechanism as `curr_cell` (membership), or two? How do they relate within one transition?
|
||
|
||
### B. Underground / dungeons
|
||
6. How are dungeons represented in the dats and at runtime — EnvCell graph, portals, the absence of terrain? How does this differ from building interiors (cottage/inn) which sit ON a landblock with terrain?
|
||
7. How does the player move through a dungeon: cell tracking, dungeon cell loading/streaming, and how the engine knows there is no sky/terrain to draw?
|
||
8. Is there an explicit "underground" flag/state (e.g. on `Position`/landblock/cell), or is "underground" simply "current cell is an EnvCell with no outdoor reachability"? Cite the flag/field if it exists.
|
||
|
||
### C. Rendering inside and outside (the seamless seal)
|
||
9. Trace retail's render visibility: how does it build the visible set in ONE pass (`ConstructView`/`InitCell`/`find_visible_child_cell`/PVS/PView)? What does it output (visible cell list, per-portal clip regions/frustum)?
|
||
10. How does retail draw the OUTSIDE seen through a doorway/window from inside (sky, rain, terrain, exterior buildings) so there is **no blue clear-color hole**? How are exit portals / `seen_outside` handled in the render traversal?
|
||
11. How does retail seal interiors — cap ceilings, prevent the outdoor world from bleeding in, and clip entities/particles to the visible cells?
|
||
12. How does retail decide to draw terrain + sky vs not, as a function of the current cell (indoor / outdoor / underground)?
|
||
13. **Is the render cell/visibility the SAME `curr_cell`/cell-graph as physics, or separate?** Trace whether render reads the physics `curr_cell` and traverses the shared cell graph, or maintains its own. (Central to acdream's decision.)
|
||
|
||
### D. Synthesis for acdream (be concrete)
|
||
14. Given acdream re-derives membership statically per tick (instead of tracking it through the sweep) and renders with a separate cell system, what is the **retail-faithful target architecture** we should port?
|
||
15. Specifically: should membership be advanced inside the transition sweep (port `validate_transition`'s `curr_cell` advance + the `do_not_load_cells` prune + drop the static `ResolveCellId`, reading `sphere_path.curr_cell` like `SetPositionInternal`)? Should the render obey the physics `curr_cell` + a single portal-visibility traversal? Justify from the decomp.
|
||
16. List the **must-port functions** (with decomp addresses), the integration order, the main risks (esp. anything touching the collision sweep where acdream has a history of bugs), and what conformance tests would prove faithfulness.
|
||
|
||
## 5. Method reminders
|
||
- Grep the named decomp by `Class::method` FIRST; confirm addresses in `symbols.json`.
|
||
- The decomp is ground truth; ACE/ACViewer/WorldBuilder are interpretation aids — when they
|
||
disagree, the decomp wins, but note the disagreement.
|
||
- Distinguish what you VERIFIED in source from what you INFER. Flag inferences.
|
||
- Cite: `function_name @ 0xADDR (pseudo_c:LINE)` or `repo/path/File.cs:LINE`.
|