refactor(net): own live session composition
Extract reset, selection, entered-world, and route construction behind LiveSessionHost while preserving the sole LiveSessionController authority. Retain partial route and subscription cleanup for retry, and replace the embedded ACE-only shortcut with the exact named-retail unsigned skill formula. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
18d4b999de
commit
557eb7ef6b
22 changed files with 1430 additions and 236 deletions
|
|
@ -27,9 +27,11 @@ What does NOT go here:
|
|||
## Current queue — 2026-07-22
|
||||
|
||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 are
|
||||
complete; `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed
|
||||
orchestration handoffs and the class is 4,666 lines / 196 fields / 70
|
||||
methods. Final Slice 8 composition/shutdown cleanup is active in
|
||||
complete; Slice 8 checkpoints A–C now own native callbacks and live-session
|
||||
composition outside the shell. `GameWindow.OnUpdate` and
|
||||
`GameWindow.OnRender` are typed orchestration handoffs and the class is 4,589
|
||||
lines / 196 fields / 69 methods. Remaining Slice 8 composition/shutdown
|
||||
cleanup is active in
|
||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
||||
- **Connected-gate fidelity:** `#232` adds canonical managed/GPU/cache-owner
|
||||
|
|
@ -73,6 +75,39 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #233 — Live skill-credit resolver used an ACE shortcut, not retail's formula
|
||||
|
||||
**Status:** DONE (2026-07-22, GameWindow Slice 8 Checkpoint C)
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-07-22
|
||||
**Component:** live character state / DAT skill formulas
|
||||
|
||||
**Description:** The live skill-update route calculated a skill's primary-
|
||||
attribute contribution with an ACE-oriented shortcut. It rejected every
|
||||
formula whose first multiplier was zero, truncated division, and added `W`
|
||||
after division. Custom DAT formulas—and any stock boundary landing on a half—
|
||||
could therefore display the wrong skill value.
|
||||
|
||||
**Root cause / status:** The binding cited ACE without first checking named
|
||||
retail. `SkillFormula::Calculate @ 0x00591960` treats all four arithmetic words
|
||||
as unsigned, fails only for `Z == 0`, forms the wrapping numerator
|
||||
`X*a + Y*b + W`, and rounds `numerator/Z` half-up. Checkpoint C moved that
|
||||
algorithm into `RetailSkillFormula`, added a named `LiveSkillCreditResolver`,
|
||||
and pinned zero-X, W placement, half rounding, unsigned reinterpretation, and
|
||||
32-bit wrap with conformance tests.
|
||||
|
||||
**Files:** `src/AcDream.App/Net/RetailSkillFormula.cs`;
|
||||
`tests/AcDream.App.Tests/Net/RetailSkillFormulaTests.cs`.
|
||||
|
||||
**Research:**
|
||||
[`docs/research/skill_formula_calculate_pseudocode.md`](research/skill_formula_calculate_pseudocode.md).
|
||||
|
||||
**Acceptance:** Live skill updates resolve the DAT entry and reproduce the
|
||||
named retail function for all unsigned field values; missing skills or
|
||||
attributes remain safely projected as zero.
|
||||
|
||||
---
|
||||
|
||||
## #232 — Nine-stop soak process-memory gate lacks canonical owner snapshots
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
|
|||
|
|
@ -232,7 +232,9 @@ src/AcDream.App/
|
|||
│ └── Vfx/ # (already exists)
|
||||
├── Net/
|
||||
│ ├── LiveSessionController.cs # owns complete WorldSession connect/enter/logout/reconnect lifecycle
|
||||
│ ├── LiveSessionLifecycleHost.cs # narrow App composition + exact borrowed-session attachment
|
||||
│ ├── LiveSessionHost.cs # App reset/selection/entry/route-factory composition over the canonical controller
|
||||
│ ├── LiveSessionLifecycleHost.cs # exact borrowed-session attachment guard
|
||||
│ ├── RetailSkillFormula.cs # named-retail unsigned skill-credit calculation + DAT lookup
|
||||
│ └── LiveSessionEventRouter.cs # typed subscriptions → focused domain handlers
|
||||
├── Physics/
|
||||
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
|
||||
|
|
@ -436,7 +438,7 @@ useful ordering seam, but its ownership status is **partial**.
|
|||
| Area | Status | Current truth |
|
||||
|---|---|---|
|
||||
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
|
||||
| Network session | **Complete** | `LiveSessionController` owns resolve/create/bind/Connect/selection/EnterWorld/Tick/stop/reconnect/disposal; `LiveSessionLifecycleHost` is the narrow App composition boundary; `LiveSessionEventRouter` and `LiveSessionCommandRouter` own exact inbound/outbound lifetimes. `GameWindow` retains one controller field and focused domain sink factories only (`d9ccf8a6`, `6a5d9e2e`). |
|
||||
| Network session | **Complete** | `LiveSessionController` remains the sole resolve/create/Connect/selection/EnterWorld/Tick/stop/reconnect/disposal and current-session owner. `LiveSessionHost` owns App reset/selection/entry ordering plus create→attach route factories; `LiveSessionLifecycleHost` guards exact borrowed-session attachment. `LiveSessionEventRouter` and `LiveSessionCommandRouter` own exact inbound/outbound lifetimes, with retryable per-edge teardown before reset or replacement. `GameWindow` retains the controller, focused host, and typed domain binding factories—no mirrored session or reset plan. |
|
||||
| Live identity/lifetime | **Complete App integration** | `LiveEntityRuntime` owns incarnation identity, accepted snapshots/timestamps, runtime components, and logical/spatial lifetime. `LiveEntityHydrationController`, `LiveEntityRuntimeTeardownController`, and `LiveEntityNetworkUpdateController` own DAT-backed hydration, retryable cleanup, and accepted Position/Vector/State/Movement presentation without a second GUID owner (`aa90c646`). |
|
||||
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
|
||||
| World reveal | **Complete** | `WorldRevealCoordinator` owns login/portal readiness and reveal lifetime (`a4ef5788`). The accepted deterministic lifecycle trace did not change after extraction. |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# acdream — strategic roadmap
|
||||
|
||||
**Status:** Living document. Updated 2026-07-22. **M3 landed; M4 is next after the active `GameWindow` structural prerequisite.** M3's retail casting/UI, R6 locomotion/collision/projectile/teleport/radar rebaseline, deterministic fresh-login/portal world lifecycle, and final two-client portal observer flow are user-gated. The current program is the behavior-preserving ownership campaign in [`docs/architecture/code-structure.md`](../architecture/code-structure.md); Slices 1–7 are complete and final Slice 8 composition/shutdown cleanup is active. New M4 quest/emote/character-creation feature bodies wait until that campaign is complete. Issue #225's lifestone/particle alpha comparison remains a separate rendering visual gate.
|
||||
**Status:** Living document. Updated 2026-07-22. **M3 landed; M4 is next after the active `GameWindow` structural prerequisite.** M3's retail casting/UI, R6 locomotion/collision/projectile/teleport/radar rebaseline, deterministic fresh-login/portal world lifecycle, and final two-client portal observer flow are user-gated. The current program is the behavior-preserving ownership campaign in [`docs/architecture/code-structure.md`](../architecture/code-structure.md); Slices 1–7 and Slice 8 checkpoints A–C are complete, with the remaining composition/shutdown checkpoints active. New M4 quest/emote/character-creation feature bodies wait until that campaign is complete. Issue #225's lifestone/particle alpha comparison remains a separate rendering visual gate.
|
||||
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
|
||||
|
||||
---
|
||||
|
|
@ -79,7 +79,12 @@ second synchronized soak pass with graceful exits, and the six stable PNG
|
|||
checkpoints preserve Slice 6 presentation. Issue #232 tracks the coarse
|
||||
process-residency gate's diagnostic blind spot; no tolerance was loosened.
|
||||
|
||||
Final Slice 8 composition/shutdown cleanup is now active.
|
||||
Final Slice 8 composition/shutdown cleanup is active. Checkpoints A–C now own
|
||||
the frozen shell boundary, all nine native callback edges, and live-session
|
||||
reset/selection/entry/route composition. `GameWindow` is 4,589 lines / 196
|
||||
fields / 69 methods; the Checkpoint C Release suite passes 7,408 tests / 5
|
||||
skips. Environment, input, settings, startup/resource transfer, and shutdown
|
||||
checkpoints D–L remain.
|
||||
|
||||
This is a behavior-preserving structural program. Severe regressions still get
|
||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ diagnostic owners preserve the accepted draw/failure graph. `GameWindow` is
|
|||
campaign baseline. The full Release suite passes 7,341 tests / 5 skips; the
|
||||
315.6-second capped/reconnect lifecycle gate and 395.2-second synchronized
|
||||
nine-destination soak pass with graceful exits, and six stable PNG checkpoints
|
||||
preserve Slice 6 presentation. Final Slice 8 composition/shutdown cleanup is
|
||||
active. Issue #232 tracks process-residency variance in the soak without
|
||||
preserve Slice 6 presentation. Final Slice 8 checkpoints A–C are complete:
|
||||
native callbacks and live-session composition now have explicit retryable
|
||||
owners, and `GameWindow` is 4,589 lines / 196 fields / 69 methods. Checkpoints
|
||||
D–L remain active. Issue #232 tracks process-residency variance in the soak without
|
||||
loosening its leak threshold.
|
||||
|
||||
Carried:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ audit.
|
|||
test-facade residue.
|
||||
- [x] B — make native-window callback intake an explicit reversible owner and
|
||||
define host-quiescence failure semantics.
|
||||
- [ ] C — extract live-session host/reset/binding callbacks and verify the
|
||||
- [x] C — extract live-session host/reset/binding callbacks and verify the
|
||||
embedded skill formula against named retail.
|
||||
- [ ] D — extract retail world-environment/day/weather behavior.
|
||||
- [ ] E — extract two-phase raw pointer/camera input, focus, and framebuffer
|
||||
|
|
@ -450,6 +450,19 @@ frozen callback behavior.
|
|||
- Keep `LiveSessionController.Start` last; both disabled-live and
|
||||
missing-credential starts must execute the reset path.
|
||||
|
||||
Result: `LiveSessionHost` now owns reset-plan construction, exact selection and
|
||||
entered-world ordering, and create→attach route factories while resolving all
|
||||
session/command/in-world state through the sole `LiveSessionController`.
|
||||
Partially attached routes and every individual subscription edge retain failed
|
||||
cleanup work; successful edges are never replayed, and reset/new generations
|
||||
remain blocked until teardown converges. Named-retail research corrected the
|
||||
former ACE-only skill shortcut to exact unsigned `SkillFormula::Calculate @
|
||||
0x00591960` semantics. Three corrected-diff review loops are clean; focused
|
||||
session/formula/ledger tests, the App suite (3,048 pass / 3 intentional skips),
|
||||
Core.Net (548 pass), the Release build, and the full suite (7,408 pass / 5
|
||||
intentional skips) pass. No connected gate was required for this ownership
|
||||
checkpoint; the final connected lifecycle gate remains Checkpoint L.
|
||||
|
||||
### D — world environment
|
||||
|
||||
- Move loaded sky/day state, `RefreshSkyForCurrentDay`, `AdminEnvirons`,
|
||||
|
|
|
|||
65
docs/research/skill_formula_calculate_pseudocode.md
Normal file
65
docs/research/skill_formula_calculate_pseudocode.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Retail skill-formula calculation
|
||||
|
||||
## Oracle
|
||||
|
||||
- Named retail: `SkillFormula::Calculate @ 0x00591960` in
|
||||
`named-retail/acclient_2013_pseudo_c.txt`.
|
||||
- Retail layout: `SkillFormula` in `named-retail/acclient.h:40199` stores six
|
||||
unsigned 32-bit fields in wire order: `W`, `X`, `Y`, `Z`, `Attr1`, `Attr2`.
|
||||
|
||||
The signed-looking branches in the recovered x87 code are not signed formula
|
||||
semantics. They are the compiler's unsigned-32-bit-to-floating-point fix-up:
|
||||
when the high bit is set, it adds `2^32` after the initial signed conversion.
|
||||
|
||||
## Pseudocode
|
||||
|
||||
```text
|
||||
bool SkillFormula.Calculate(uint attribute1, uint attribute2, out uint result)
|
||||
{
|
||||
uint divisor = Z;
|
||||
if (divisor == 0)
|
||||
return false;
|
||||
|
||||
// Every operation on this line is uint32 and wraps modulo 2^32.
|
||||
uint numerator = X * attribute1 + Y * attribute2 + W;
|
||||
|
||||
// Formula values are non-negative. Retail rounds to nearest integer,
|
||||
// with exact halves rounding upward.
|
||||
result = uint(floor(double(numerator) / double(divisor) + 0.5));
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Consequences pinned by the port:
|
||||
|
||||
- `Z == 0` is the only failure gate. `X == 0` remains a valid formula.
|
||||
- `W` participates in the numerator before division; it is not added after
|
||||
division.
|
||||
- multiplication and addition wrap as unsigned 32-bit arithmetic before the
|
||||
floating-point division.
|
||||
- the division rounds half-up; it does not truncate.
|
||||
|
||||
## Reference cross-check
|
||||
|
||||
ACE's [`SkillFormula`](https://github.com/ACEmulator/ACE/blob/master/Source/ACE.DatLoader/Entity/SkillFormula.cs)
|
||||
confirms the unsigned DAT order `W, X, Y, Z, Attr1, Attr2`. ACE's
|
||||
[`AttributeFormula.GetFormula`](https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Server/Entity/AttributeFormula.cs)
|
||||
is only an interpretation aid: it rejects `X == 0`, assumes unit multipliers,
|
||||
omits `W`, and rounds a summed-attribute quotient. That is sufficient for
|
||||
ordinary stock ACE data but is not the general retail algorithm.
|
||||
|
||||
Chorizite DatReaderWriter's generated
|
||||
[`SkillFormula`](https://github.com/Chorizite/DatReaderWriter/blob/master/DatReaderWriter/Generated/Types/SkillFormula.generated.cs)
|
||||
independently confirms the same six-field order and maps its public names to
|
||||
`W`, `X`, `Y`, and `Z`. It exposes the four arithmetic words as signed `int`,
|
||||
so the retail port must reinterpret their bits as `uint`. Its summary places
|
||||
the additive value after division; the named retail function proves that the
|
||||
actual client adds `W` to the numerator instead.
|
||||
|
||||
## Port decision
|
||||
|
||||
`RetailSkillFormula` is the sole App-layer calculation seam. It reinterprets
|
||||
the DAT words as unsigned, performs the exact unchecked 32-bit numerator, and
|
||||
uses the retail half-up division. `LiveSkillCreditResolver` performs only the
|
||||
skill-table lookup and treats a missing attribute as zero, matching the prior
|
||||
live-property projection behavior.
|
||||
Loading…
Add table
Add a link
Reference in a new issue