refactor(world): own live environment state
Move the DAT sky, selected day group, world clock, weather, AdminEnvirons bridge, and debug cycles into a one-shot WorldEnvironmentController while preserving GameWindow's public aliases and accepted startup/session/render order. Correct the named retail citations and register the remaining environment audio and fog/radar gaps. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
557eb7ef6b
commit
d09e246d3a
19 changed files with 606 additions and 368 deletions
|
|
@ -27,10 +27,11 @@ What does NOT go here:
|
|||
## Current queue — 2026-07-22
|
||||
|
||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 are
|
||||
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
|
||||
complete; Slice 8 checkpoints A–D now own native callbacks, live-session
|
||||
composition, and world-environment state outside the shell.
|
||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
||||
handoffs and the class is 4,330 lines / 192 fields / 67 methods. Remaining
|
||||
Slice 8 input/settings/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.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ after Slice 4 10,301 lines / 267 fields / 163 method
|
|||
after Slice 5 closeout 8,811 lines / 247 fields / 153 methods
|
||||
after Slice 6 closeout 7,026 lines / 241 fields / 108 methods
|
||||
after Slice 7 draw-frame cutover 4,666 lines / 196 fields / 70 methods
|
||||
after Slice 8 checkpoint D 4,330 lines / 192 fields / 67 methods
|
||||
```
|
||||
|
||||
`GameWindow` is the single object that:
|
||||
|
|
@ -256,6 +257,7 @@ src/AcDream.App/
|
|||
│ ├── RetailInboundEventDispatcher.cs # update-thread packet/frame FIFO barrier
|
||||
│ ├── RetailLiveFrameCoordinator.cs # shipped: object/network/command/reconcile phase order
|
||||
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
|
||||
│ ├── WorldEnvironmentController.cs # one-shot clock/DAT-sky/day/weather/AdminEnvirons owner
|
||||
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
|
||||
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
|
||||
├── Interaction/
|
||||
|
|
@ -439,13 +441,14 @@ useful ordering seam, but its ownership status is **partial**.
|
|||
|---|---|---|
|
||||
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
|
||||
| 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. |
|
||||
| World environment | **Complete ownership** | `WorldEnvironmentController` solely owns WorldTime, the loaded DAT sky, current day group, Weather, server synchronization, AdminEnvirons bridging, and debug time/weather cycles. `GameWindow` keeps ABI-compatible readonly aliases to the same clock/weather instances and composes one render source plus the two live-session callbacks. TS-54/TS-55 register the remaining centered UI sound and full fog/ambient/radar behavior gaps. |
|
||||
| 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. |
|
||||
| Retained gameplay UI | **Mostly complete feature ownership** | `RetailUiRuntime` and focused panel/controllers own layout and behavior. `GameWindow.OnLoad` still performs substantial service composition, which is allowed until the final composition cleanup. |
|
||||
| Selection/interaction | **Complete** | `WorldSelectionQuery` owns read-only picking/classification/range queries; `SelectionInteractionController` owns selection intent, Use/PickUp transport, exact-incarnation queues, and auto-walk deferral; `ItemInteractionController` owns ItemHolder policy plus the shared retail inventory-request transaction. `GameWindow` retains construction and narrow lifecycle forwarding only. |
|
||||
| Landblock presentation | **Complete** | `LandblockBuildFactory` owns the captured-origin DAT transaction; concrete render/physics/DAT-static publishers and `LandblockPresentationPipeline` own publication and exact retryable retirement. `StreamingOriginRecenterCoordinator` serializes old-window retirement with teleport/session origin lifetimes. `GameWindow` retains construction and one pipeline field only (`c79d0a49`, closeout `4a205a3e`). |
|
||||
| Render-frame orchestration | **Not extracted** | `OnRender` plus its portal/PView/alpha/particle helpers still own the draw graph and frame-local scratch state. There is no `RenderFrameOrchestrator` yet. |
|
||||
| Render-frame orchestration | **Complete** | `RenderFrameOrchestrator` owns the GPU-flight, resource, world/PView/shared-alpha, private-presentation, diagnostics, screenshot, and recovery graph. `GameWindow.OnRender` takes one logical window-size snapshot and performs one immutable handoff (`9d7df1bf`). |
|
||||
| Unified `GameEntity` | **Deferred target** | `LiveEntityRuntime` already supplies the critical canonical owner. Full type aggregation is a separate high-risk migration and is not required to make `GameWindow` thin. |
|
||||
|
||||
### 4.3 Revised extraction sequence
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-119 | Equal-generation CreateObject refresh applies the packet's complete `PhysicsDesc` to the existing `EntityEffectProfile`, including replacing its network sound/PES-table/default-script description. Retail's equal-`INSTANCE_TS` branch applies the individual ObjDesc/Parent-or-Position/Movement/State/Vector/WeenieDesc tail and does not call `CPhysicsObj::set_description` again. | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`ILiveEntitySameGenerationUpdateSink.OnDescription`); `LiveEntitySameGenerationUpdateRouter.cs` | Existing spell/projectile/portal VFX tests and connected behavior were accepted with this refresh. Slice 4 records and isolates it rather than silently changing DAT-effect ownership during an architecture extraction. | A same-generation CreateObject whose PeTable/sound/default-script fields differ from the original can replace effect lookup state where retail would retain the original table, producing a different later typed effect. | `SmartBox::HandleCreateObject @ 0x00454C80`; `CPhysicsObj::set_description @ 0x00514F40`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
|
||||
| AP-120 | `ObjectTableWiring.ApplyEntitySpawn` publishes the CreateObject's WeenieDesc/item state before the same-generation physics update tail. Retail applies WeenieDesc after ObjDesc, Parent-or-Position/Pickup, Movement, State, and Vector. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (`ApplyEntitySpawn`); `src/AcDream.App/World/LiveEntitySameGenerationUpdateRouter.cs` | Core.Net owns item-table ingestion before App callbacks and the current single-thread FIFO prevents a second network packet from interleaving; changing publication order crosses the Core.Net/App ownership boundary and requires a separately tested event transaction. | A synchronous item-table observer can see the refreshed WeenieDesc while the same object's physics/parent/state still reflects the prior snapshot; retail observers see the completed physics tail first. | `SmartBox::HandleCreateObject @ 0x00454C80`; `ACCObjectMaint::CreateObject @ 0x00558870` |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 35 active rows + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
|
||||
## 4. Temporary stopgap (TS) — 37 active rows + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -261,10 +261,12 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each ordinary object's ParticleManager then ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` instead advances that static owner's ScriptManager then ParticleManager and only then `process_hooks`, using its whole admitted elapsed interval. acdream's shared tail is Particle → Script after static hook capture. | `src/AcDream.App/Update/LiveObjectFrameController.cs` (`LiveObjectFrameController` + `LiveEffectFrameController` shared `_particles.Tick` / `_scripts.Tick` tail); `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs` | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and ordinary manager order faithful without pretending the shared tails have per-owner timing or static-tail order. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static hooks can route before their script/particle managers and static default scripts/particles use render elapsed in Particle → Script order rather than `animate_static_object` elapsed/discard and Script → Particle → hooks timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum/order |
|
||||
| TS-52 | The terrain shader applies retail-authored base/overlay/road `TerrainTex.TexTiling` but omits the separate Environment Detail Textures pass and its viewer-distance fade (**#226**). | `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs`; `src/AcDream.App/Rendering/Shaders/terrain_modern.frag` | `bb5acab9` fixed the user-visible stretched/blurry regression by porting the distinct base-tiling contract. An earlier experimental detail array darkened the whole ground because its source/neutral blend contract was wrong, so it was correctly reverted rather than guessed into production. | With retail's Environment Detail Textures preference enabled, close terrain lacks the extra high-frequency modulation/fade even though authored base texture scale is correct. | `LScape::GenerateDetailSurfaces` / `SetDetailTexturing @ 0x00506B40`; `ACRender::landPolyDraw @ 0x006B6450..0x006B6525`; issue #226 |
|
||||
| TS-53 | acdream advances retained UI time on the draw seam and local teleport/UI-camera presentation after its SmartBox-shaped object → inbound network → CommandInterpreter barrier. Retail `Client::UseTime` calls `UIElementManager::UseTime` first, whose global time message reaches `gmSmartBoxUI::UseTime`, and publishes player-camera work from the physics/player callback rather than one post-network camera tail. Slices 6–7 preserve the accepted host order as ownership-only extractions. | `src/AcDream.App/Update/UpdateFrameOrchestrator.cs` (post-live-frame teleport/camera phases); `src/AcDream.App/Rendering/PrivatePresentationRenderer.cs` (`RetainedGameplayUiFrame.Render`); `docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`; `docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md` | Current retained UI, portal transit, reveal, camera, and connected movement traces are accepted; changing cross-subsystem host order while extracting ownership would combine a behavior change with the structural cutover. | Retained UI, teleport, and camera presentation can observe same-frame object/inbound/player state one host update earlier or later than retail at transition boundaries; a future exact host-order port must prove UI, input, reveal, and camera consequences together. | `Client::UseTime @ 0x00411C40`; `UIElementManager::UseTime`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; `CPhysics::UseTime @ 0x00509950`; retire only with a focused host-order port and connected portal/camera comparison |
|
||||
| TS-54 | AdminEnvirons sound values `0x65..0x7B` are diagnosed by retail enum name but do not play audio. Retail checks that the local player physics object and UI sound table exist, then calls `SoundManager::PlaySoundFromCenter(Sound_UI_*, table)` for Roar through Thunder6. | `src/AcDream.App/World/WorldEnvironmentController.cs` (`ApplyAdminEnvirons`) | The current audio owner has no typed retail UI-sound-table binding; logging preserves the inbound evidence without inventing wave DIDs or routing the sounds through positional world audio. | Server-authored ambience/thunder packets are silent in acdream while retail plays the centered UI sound. | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20` (`0x0055E07F..0x0055E2C7`); `SoundManager::PlaySoundFromCenter @ 0x00550950` |
|
||||
| TS-55 | AdminEnvirons fog values remain a color-only `WeatherSystem.Override` approximation. Retail values 1..5 install authored ambient color/level plus fog color/max; value 6 also forces transition/min/max and blanks radar; Clear restores all override fields and radar; `0x270F` installs a separate authored override. | `src/AcDream.App/World/WorldEnvironmentController.cs` (`ApplyAdminEnvirons`); `src/AcDream.Core/World/WeatherState.cs` (`EnvironOverrideColor`) | Preserves the already accepted enum bridge while Slice 8 moves ownership; porting the complete environment/radar presentation is a separate behavior change requiring focused visual gates. | Forced-fog hue, density, scene ambient, and radar blanking differ from retail; `0x270F` is ignored. | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20` (`0x0055DE2B..0x0055E344`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Unclear (UN) — 5 rows
|
||||
## 5. Unclear (UN) — 4 rows
|
||||
|
||||
These rows have a missing, contradictory, or never-argued justification.
|
||||
They are the highest-priority audits: each needs either a recorded
|
||||
|
|
@ -273,7 +275,6 @@ equivalence argument (promote to AD/AP) or a fix.
|
|||
| # | Divergence | Where (file:line) | Recorded justification (deficient) | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
| UN-1 | `CheckOtherCells` iterates the overlap set SORTED by cell id; retail walks the CELLARRAY in build order — and the loop halts on the first non-OK result, so order is behavior-bearing | `src/AcDream.Core/Physics/CellTransit.cs:1718` | Justified only as "deterministic order for greppable probe logs" — no equivalence argument vs retail's array order recorded | A sphere straddling two cells that would each return a different non-OK result halts on a different cell than retail — different collision normal / slide direction at multi-cell straddles | `CTransition::check_other_cells` pc:272717-272798 |
|
||||
| UN-3 | AdminEnvirons fog-override RGB tints hardcoded with no retail constant cited (RedFog 0.60/0.05/0.05 etc.); Snapshot replaces fog COLOR only, keeping keyframe distances on an unverified assumption | `src/AcDream.Core/World/WeatherState.cs:350` | Enum semantics cite ACE EnvironChangeType + r12 §5.2; no source for the RGB values or the color-only override scope | A server-forced fog event renders the wrong hue and/or wrong density vs what retail clients showed for the same packet | AdminEnvirons 0xEA60; ACE EnvironChangeType.cs |
|
||||
| UN-4 | GfxObj double-sided/negative-surface handling keeps WB's legacy logic (cull-mode double-siding, no reversed-winding duplicate, different neg-surface predicate) while the CellStruct path follows the retail-cited `ConstructMesh` reading | `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1059` (CellStruct contrast :1396-1410) | No recorded justification on the GfxObj side — it is the unmodified WB extraction; the retail citation was added only to the CellStruct path | GfxObj models retail draws via duplicated-reversed-winding get wrong back-face lighting (normals not inverted) or missing/extra negative faces — dark or absent faces from behind | `D3DPolyRender::ConstructMesh` 0x0059dfa0 |
|
||||
| UN-6 | Fixed 200 ms sleep between ConnectRequest and ConnectResponse; retail inserts no delay. Annotated only as "with 200ms race delay"; the 2026-06-04 audit flagged it, the follow-up refuted "forbidden workaround" but wrote no fuller rationale back | `src/AcDream.Core.Net/WorldSession.cs:484` | Presumed ACE port+1 listener race guard — four words, no citation | Every login eats a flat 200 ms; if the race needs longer on a loaded server, the handshake fails intermittently (ConnectResponse ignored → CharacterList never arrives, exit-29 shape) with no retry — a timing constant masking an unconfirmed root cause | (none recorded) |
|
||||
| UN-7 | Outdoor OBJECT point lighting uses `calc_point_light` (wrap/norm + per-channel cap, `~1/d²`) for ALL meshes including static buildings, but retail's object path is unconfirmed — `config_hardware_light` (0x0059ad30) sets D3D-FF point lights (`Diffuse=color×intensity`, `Attenuation=(0,1,0)`⇒`1/d`, `Range=falloff×1.5`, `material.diffuse=white`) yet that math would blow walls WHITE while retail stays DIM, so static buildings may instead use the `SetStaticLightingVertexColors` bake. Model + the brightness-scaling factor both UNRESOLVED (issue #140 / Fix D) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution`); `src/AcDream.Core/Lighting/LightManager.cs` (`SelectForObject`) | Fix A/B ported calc_point_light + per-object selection for objects without confirming retail uses that model for static buildings; cdb captured the D3D-FF path but it contradicts the observed dim result | Outdoor buildings blow out warm near torches (the #140 meeting-hall symptom); whichever model is wrong, the object torch contribution is too strong | `config_hardware_light` 0x0059ad30; `SetStaticLightingVertexColors` 0x0059cfe0; `rangeAdjust=1.5` 0x00820cc4 — see docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md |
|
||||
|
|
@ -294,7 +295,7 @@ WITH that phase, not before.
|
|||
5. **UN-6 — 200 ms ConnectResponse sleep** — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check.
|
||||
6. **UN-4 — GfxObj sides/negative-surface logic** — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it.
|
||||
7. **TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12)** — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
|
||||
8. **UN-3 — AdminEnvirons tints** — invented RGB constants + unverified color-only scope; one decomp lookup against the 0xEA60 handler.
|
||||
8. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together.
|
||||
9. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging.
|
||||
|
||||
**Phase-gated (do WITH the phase, flagged here so they aren't forgotten):**
|
||||
|
|
|
|||
|
|
@ -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 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.
|
||||
**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–D 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,12 +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 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.
|
||||
Final Slice 8 composition/shutdown cleanup is active. Checkpoints A–D now own
|
||||
the frozen shell boundary, all nine native callback edges, live-session
|
||||
reset/selection/entry/route composition, and world clock/sky/day/weather/
|
||||
AdminEnvirons state. `GameWindow` is 4,330 lines / 192 fields / 67 methods;
|
||||
the Checkpoint D Release suite passes 7,419 tests / 5 skips. Input, settings,
|
||||
startup/resource transfer, and shutdown checkpoints E–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,10 +39,12 @@ 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 checkpoints A–C are complete:
|
||||
preserve Slice 6 presentation. Final Slice 8 checkpoints A–D 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
|
||||
owners. Checkpoint D adds the sole world-environment owner for clock, DAT sky,
|
||||
day group, weather, and AdminEnvirons state. `GameWindow` is 4,330 lines / 192
|
||||
fields / 67 methods; 7,419 Release tests pass / 5 skip. Checkpoints E–L remain
|
||||
active. Issue #232 tracks process-residency variance in the soak without
|
||||
loosening its leak threshold.
|
||||
|
||||
Carried:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ audit.
|
|||
define host-quiescence failure semantics.
|
||||
- [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.
|
||||
- [x] D — extract retail world-environment/day/weather behavior.
|
||||
- [ ] E — extract two-phase raw pointer/camera input, focus, and framebuffer
|
||||
resize behind symmetric named subscriptions.
|
||||
- [ ] F — extract the sole gameplay input-action router plus focused combat and
|
||||
|
|
@ -471,6 +471,21 @@ checkpoint; the final connected lifecycle gate remains Checkpoint L.
|
|||
- Preserve existing named-retail citations and registered sound adaptation;
|
||||
diagnostics call typed commands rather than owning time/weather state.
|
||||
|
||||
Result: `WorldEnvironmentController` is now the sole owner of the clock,
|
||||
loaded sky descriptor, selected day group, Weather state, server time sync,
|
||||
AdminEnvirons bridge, and time/weather debug cycles. `GameWindow` preserves its
|
||||
public readonly clock/weather aliases but only composes the owner into render
|
||||
and session seams. Initialization is explicitly one-shot, and missing GameTime
|
||||
restores the documented fallback origin instead of inheriting process-global
|
||||
state. Named-oracle review corrected the day picker to
|
||||
`SkyDesc::CalcPresentDayGroup @ 0x00500E10` and AdminEnvirons to
|
||||
`CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20`; TS-54/TS-55 now register
|
||||
the carried centered-audio and complete fog/ambient/radar gaps. Three
|
||||
corrected-diff reviews are clean; 17 focused tests, the App suite (3,059 pass /
|
||||
3 intentional skips), the warning-free Release build, and the full suite
|
||||
(7,419 pass / 5 intentional skips) pass. No connected gate was required for
|
||||
this behavior-preserving ownership checkpoint.
|
||||
|
||||
### E — pointer, focus, and framebuffer resize
|
||||
|
||||
- Make SilkKeyboard/Mouse sources and dispatcher links reversible, then attach
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@
|
|||
**Status:** RESEARCH COMPLETE — algorithm recovered, ready for C# port.
|
||||
**Companion docs:** `2026-04-23-sky-retail-verbatim.md` (full sky pipeline map).
|
||||
|
||||
> **Named-oracle correction (2026-07-22):** the Sept 2013 PDB identifies this
|
||||
> body as `SkyDesc::CalcPresentDayGroup @ 0x00500E10`. References below to
|
||||
> `FUN_00501990 @ 0x00501990` describe the older fallback binary/chunk only;
|
||||
> that address is inside `SkyDesc::Pack` in the named 2013 build.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
Retail's DayGroup picker is a **simple integer LCG mixed with `(YEAR, DAY_OF_YEAR)` from the live `TimeOfDay` struct**, scaled by `DayGroupCount`. It is **not** a weighted walk over `ChanceOfOccur` — it's uniform over all DayGroups. Dereth's 20 DayGroups all carry `ChanceOfOccur = 5.0f` anyway, so uniform matches the intent. The algorithm lives in `FUN_00501990` (previously mis-labeled "deterministic PES roll" in the sky-verbatim doc; it is actually **SkyDesc::PickCurrentDayGroup**).
|
||||
Retail's DayGroup picker is a **simple integer LCG mixed with `(YEAR, DAY_OF_YEAR)` from the live `TimeOfDay` struct**, scaled by `DayGroupCount`. It is **not** a weighted walk over `ChanceOfOccur` — it's uniform over all DayGroups. Dereth's 20 DayGroups all carry `ChanceOfOccur = 5.0f` anyway, so uniform matches the intent. The named algorithm is **`SkyDesc::CalcPresentDayGroup @ 0x00500E10`**; `FUN_00501990` is its older-build fallback identity.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -41,7 +46,7 @@ Note: `FUN_004ff4b0` (the OTHER mapped trampoline → `FUN_00502a10`) is called
|
|||
|
||||
```c
|
||||
// chunk_00500000.c:1274 FUN_00501990 @ 0x00501990 size=138
|
||||
// SkyDesc::PickCurrentDayGroup(this)
|
||||
// Older build of SkyDesc::CalcPresentDayGroup(this)
|
||||
void __fastcall FUN_00501990(uint *param_1)
|
||||
{
|
||||
float fVar1;
|
||||
|
|
@ -127,7 +132,7 @@ Global: `DAT_008ee9c8` (type `void *`). Populated by `FUN_005a7fd0` (TimeOfDay::
|
|||
| 0x00 | double | `EpochBase` (dat-declared) | `FUN_005a7fd0:6045` |
|
||||
| 0x08 | int | `BaseYear` (dat-declared) | `FUN_005a7fd0:6051` |
|
||||
| 0x0C | float | `SecondsPerDay` | `FUN_005a7fd0:6056` |
|
||||
| **0x10** | int | **`SecondsPerDay` (int copy — source of `iVar6`)** | `FUN_005a7fd0:6061` |
|
||||
| **0x10** | int | **`DaysPerYear` (source of `iVar6`; live-probed as 360)** | `FUN_005a7fd0:6061`; `2026-04-23-retail-memory-probe.md` |
|
||||
| 0x40 | double | `SecondsPerYear = DaysPerYear × SecondsPerDay` | `FUN_005a7fd0:6068` |
|
||||
| 0x48 | float | `DayFraction` (0..1) | `FUN_005a7800:5497` |
|
||||
| 0x50 | double | CurrentDay startTick | `FUN_005a7510` (via `FUN_005a75b0`) |
|
||||
|
|
@ -136,24 +141,28 @@ Global: `DAT_008ee9c8` (type `void *`). Populated by `FUN_005a7fd0` (TimeOfDay::
|
|||
| **0x68** | int | **`DayOfYear`** | `FUN_005a7510:5304`: `= floor(withinYearSec / secsPerDay)` |
|
||||
| 0x6c | int | `SeasonIndex` | `FUN_005a7510:5313` |
|
||||
|
||||
The `iVar6 = *(int *)(DAT_008ee9c8 + 0x10)` read in `FUN_00501990` picks up `SecondsPerDay` as a multiplier — acting as a spread-factor to guarantee different days in different years yield different hash inputs. (If both sides of the multiplication were small — Year in [0, ~200], DayOfYear in [0, 365] — and `SecondsPerDay` were 1, the seed range would be tiny. Using the full `SecondsPerDay` integer makes the seed uniform over a ~31-bit range before the LCG step.)
|
||||
The `days_per_year = GameTime::current_game_time->days_per_year` read in
|
||||
`SkyDesc::CalcPresentDayGroup` is the year-to-day multiplier. The live memory
|
||||
probe measured 360 at the older build's corresponding `TimeOfDay+0x10` field,
|
||||
so `Year × DaysPerYear + DayOfYear` is a flat total-day index.
|
||||
|
||||
Retail Dereth values (from dat — confirm when we next parse a Region): `SecondsPerDay = 1800` (30 real-minutes = one Dereth-day per r12 §11, times some scale factor baked into the dat). Any value works — the algorithm is agnostic.
|
||||
Retail Dereth's DAT-backed `DaysPerYear` value is 360.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pseudocode (ready for C# port)
|
||||
|
||||
```csharp
|
||||
// SkyDesc.PickCurrentDayGroup — ports acclient.exe FUN_00501990 @ 0x00501990
|
||||
// SkyDesc.CalcPresentDayGroup — named retail @ 0x00500E10
|
||||
// Older-build cross-reference: FUN_00501990 @ 0x00501990
|
||||
// Decompile: docs/research/decompiled/chunk_00500000.c:1276
|
||||
// Must be called every frame before SkyDesc.UpdateSkyObjectTable; value is
|
||||
// stable across frames within one Dereth-day so repeated calls are cheap.
|
||||
public static int PickCurrentDayGroup(int year, int secondsPerDay, int dayOfYear, int dayGroupCount)
|
||||
public static int CalcPresentDayGroup(int year, int daysPerYear, int dayOfYear, int dayGroupCount)
|
||||
{
|
||||
// Step 1: deterministic per-day seed.
|
||||
// (This mirrors retail's 3-int read from TimeOfDay+0x64/0x10/0x68.)
|
||||
int seed = year * secondsPerDay + dayOfYear;
|
||||
int seed = year * daysPerYear + dayOfYear;
|
||||
|
||||
// Step 2: 32-bit signed LCG (x86 wraps silently; force it in C#).
|
||||
int mixed = unchecked(seed * 0x6A42FDB2 + (int)0x8ABE1652);
|
||||
|
|
@ -180,14 +189,15 @@ Call from the per-frame sky render hook, **before** the keyframe-bracket interpo
|
|||
|
||||
```csharp
|
||||
// once per frame, before sky object/light interp
|
||||
skyDesc.CurrentDayGroupIndex = SkyDesc.PickCurrentDayGroup(
|
||||
skyDesc.CurrentDayGroupIndex = SkyDesc.CalcPresentDayGroup(
|
||||
year: world.TimeOfDay.Year,
|
||||
secondsPerDay: world.TimeOfDay.SecondsPerDayInt, // the int copy at TimeOfDay+0x10
|
||||
daysPerYear: world.TimeOfDay.DaysPerYear,
|
||||
dayOfYear: world.TimeOfDay.DayOfYear,
|
||||
dayGroupCount: skyDesc.DayGroups.Count);
|
||||
```
|
||||
|
||||
If ACE and acdream agree on `(Year, SecondsPerDay, DayOfYear)` (trivially true — ACE computes these server-side from the same epoch), both clients will converge to the same index every Dereth-day. This replaces the current SplitMix64 path.
|
||||
Retail and acdream converge whenever their `(Year, DaysPerYear, DayOfYear)`
|
||||
inputs agree. ACE does not select a day group; each client computes it locally.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -204,12 +214,10 @@ So retail's decompile IS our only source, and we have it.
|
|||
|
||||
## 7. Gaps
|
||||
|
||||
None critical. Two minor open items:
|
||||
None critical. One minor open item:
|
||||
|
||||
1. **`_DAT_007c6f10` exact bit value.** I inferred `1.0/2^32` from usage pattern but did not find a data-section dump that prints the literal. If the live client ever shows off-by-one drift on a boundary day, re-verify by dumping the .rdata section at 0x007c6f10. (ACDREAM_DUMP_SKY already logs picked index + inputs; we can correlate against a retail client session to confirm.)
|
||||
|
||||
2. **`TimeOfDay+0x10` value for live Dereth.** Retail `SecondsPerDay` has been quoted as both 1800 and 3600 in various reverse-engineering docs depending on whether "seconds" means realtime seconds or Dereth-clock seconds. The algorithm itself is insensitive — whatever integer the dat provides is what both retail and acdream must use. If our Region parser stores it in a differently-named field, make sure the call site passes the dat-raw value (the int at TimeOfDay+0x10), not a re-derived one.
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary table
|
||||
|
|
@ -217,8 +225,8 @@ None critical. Two minor open items:
|
|||
| Question | Answer | Evidence |
|
||||
|---|---|---|
|
||||
| Caller of `FUN_00502a10` | `FUN_004ff4b0` → called from `FUN_00508010:7560` | `chunk_004F0000.c:10732`, `chunk_00500000.c:7560` |
|
||||
| DayGroup selection function | `FUN_00501990` | `chunk_00500000.c:1276` |
|
||||
| Hash formula | `(Year × SecondsPerDay + DayOfYear) × 0x6A42FDB2 + 0x8ABE1652` | `chunk_00500000.c:1296` |
|
||||
| DayGroup selection function | `SkyDesc::CalcPresentDayGroup @ 0x00500E10` | named Sept 2013 PDB/pseudo-C; older fallback `FUN_00501990`, `chunk_00500000.c:1276` |
|
||||
| Hash formula | `(Year × DaysPerYear + DayOfYear) × 0x6A42FDB2 + 0x8ABE1652` | `SkyDesc::CalcPresentDayGroup @ 0x00500E10`; older `chunk_00500000.c:1296` |
|
||||
| Weighted by ChanceOfOccur? | **No.** Uniform over DayGroupCount. | No CDF loop in FUN_00501990 |
|
||||
| Selected index stored at | `SkyDesc + 0x00` (via `*param_1` write) | `chunk_00500000.c:1307,1309` |
|
||||
| Read back by | `FUN_00502a10:2429`, `FUN_00501600`, `FUN_00501860` | `chunk_00500000.c:2429` |
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ DayOfYear = 47 ✓
|
|||
The decompile agent's C trail
|
||||
(`docs/research/2026-04-23-sky-decompile-hunt-C.md` §1 + §4) labeled
|
||||
`TimeOfDay+0x10` as *"SecondsPerDay (int copy — source of iVar6)"*
|
||||
for the `FUN_00501990` (`SkyDesc::PickCurrentDayGroup`) LCG seed.
|
||||
for the named `SkyDesc::CalcPresentDayGroup @ 0x00500E10` LCG seed
|
||||
(older-build cross-reference `FUN_00501990 @ 0x00501990`).
|
||||
|
||||
The live probe disproves that. The value is **360** — the same as
|
||||
`GameTime.DaysPerYear` from the dat.
|
||||
|
|
@ -43,7 +44,8 @@ The live probe disproves that. The value is **360** — the same as
|
|||
Implication for the retail LCG seed formula:
|
||||
|
||||
```c
|
||||
// FUN_00501990 line 1296 of chunk_00500000.c:
|
||||
// SkyDesc::CalcPresentDayGroup @ 0x00500E10
|
||||
// (older build: FUN_00501990 line 1296 of chunk_00500000.c):
|
||||
iVar4 = (iVar3 * iVar6 + iVar4) * 0x6a42fdb2 + -0x7541e9ae;
|
||||
// ^Year ^x0x10 ^DayOfYear
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue