refactor(runtime): own local movement and outbound cadence
Move the canonical local movement controller, body/motion managers, object clock, movement wire data, and MTS/jump/AP sender into AcDream.Runtime. Replace process skill defaults with typed Runtime character options, make graphical and direct commands borrow one autorun owner, retain the construction-time PartArray seam, and include movement in terminal ownership convergence. Preserve the accepted pre-inbound movement/jump and post-inbound autonomous-position order while moving the exact packet/cadence fixtures into Runtime tests. Add graphical/direct parity, two-instance isolation, teardown, allocation, architecture, and divergence-path coverage. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
3456dff038
commit
aa3f4a60f8
36 changed files with 878 additions and 276 deletions
|
|
@ -99,8 +99,8 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AD-36 | `IMotionDoneSink.MotionDone` consumed for CREATURE-class entities only: R3-W2 binds the seam to the entity's `MotionInterpreter.MotionDone` (player via `PlayerMovementController.Motion`, remotes via `RemoteMotion.Motion`, resolved at fire time); interp-less entities (statics that never receive a UM/UP and so never get a `RemoteMotion`) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02; R5-V5 gave every `RemoteMotion`/player ONE literal `MovementManager` facade, so the residue is only the no-RemoteMotion class) | `src/AcDream.App/Rendering/GameWindow.cs` (TickAnimations MotionDoneTarget bind) | Motion for entities without a `RemoteMotion` (never UM/UP-touched) completes via the manager queue alone; nothing consumes their MotionDone until every sequencer-owning entity gets a host/`RemoteMotion` (doors DO have one since the R4-V5 door fix — first UM creates it) | An entity behavior depending on pending_motions bookkeeping in that no-RemoteMotion class (none known) would silently no-op | `CPhysicsObj::MotionDone` 0x0050fdb0; retire when every sequencer-owning entity constructs a `RemoteMotion`/host (post-M1.5 entity-class unification; R5-V5 closed the facade half) |
|
| AD-36 | `IMotionDoneSink.MotionDone` consumed for CREATURE-class entities only: R3-W2 binds the seam to the entity's `MotionInterpreter.MotionDone` (player via `PlayerMovementController.Motion`, remotes via `RemoteMotion.Motion`, resolved at fire time); interp-less entities (statics that never receive a UM/UP and so never get a `RemoteMotion`) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02; R5-V5 gave every `RemoteMotion`/player ONE literal `MovementManager` facade, so the residue is only the no-RemoteMotion class) | `src/AcDream.App/Rendering/GameWindow.cs` (TickAnimations MotionDoneTarget bind) | Motion for entities without a `RemoteMotion` (never UM/UP-touched) completes via the manager queue alone; nothing consumes their MotionDone until every sequencer-owning entity gets a host/`RemoteMotion` (doors DO have one since the R4-V5 door fix — first UM creates it) | An entity behavior depending on pending_motions bookkeeping in that no-RemoteMotion class (none known) would silently no-op | `CPhysicsObj::MotionDone` 0x0050fdb0; retire when every sequencer-owning entity constructs a `RemoteMotion`/host (post-M1.5 entity-class unification; R5-V5 closed the facade half) |
|
||||||
| AD-37 | Camera rotation state is a forward VECTOR (nlerp + normalize; roll always 0, up = world Z); retail's sought carries a full Frame and slerps quaternions (`Frame::interpolate_rotation` shortest-path slerp with 2e-4 nlerp fallback). The dead-band compares forward-vector distance against the same 2e-4 epsilon retail applies per quaternion component | `src/AcDream.App/Rendering/RetailChaseCamera.cs` (`_dampedForward`, `ApplyConvergenceSnap`) | The chase camera never rolls (heading frames are Z-up by construction), so a forward vector spans the reachable rotation space; identified (not introduced) during the #180 UpdateCamera tail reading | If a future camera mode needs roll (death cam, cutscene) the vector state can't represent it; large-angle per-frame turns nlerp (chord) vs slerp (arc) — imperceptible at 0.45-stiffness step sizes | `Frame::interpolate_rotation` 0x00535390, `Frame::close_rotation` 0x00455d70; pseudocode doc 2026-07-06-camera-sought-position |
|
| AD-37 | Camera rotation state is a forward VECTOR (nlerp + normalize; roll always 0, up = world Z); retail's sought carries a full Frame and slerps quaternions (`Frame::interpolate_rotation` shortest-path slerp with 2e-4 nlerp fallback). The dead-band compares forward-vector distance against the same 2e-4 epsilon retail applies per quaternion component | `src/AcDream.App/Rendering/RetailChaseCamera.cs` (`_dampedForward`, `ApplyConvergenceSnap`) | The chase camera never rolls (heading frames are Z-up by construction), so a forward vector spans the reachable rotation space; identified (not introduced) during the #180 UpdateCamera tail reading | If a future camera mode needs roll (death cam, cutscene) the vector state can't represent it; large-angle per-frame turns nlerp (chord) vs slerp (arc) — imperceptible at 0.45-stiffness step sizes | `Frame::interpolate_rotation` 0x00535390, `Frame::close_rotation` 0x00455d70; pseudocode doc 2026-07-06-camera-sought-position |
|
||||||
| AD-39 | The `frames_stationary_fall` ladder + fsf≥3 UP-contact-plane manufacture runs AFTER acdream's fused LKCP-restore/contact-marking block, deriving retail's `_redo` as `cleanAdvance \|\| OnWalkable`; retail (ACE Transition.cs:1029-1061) interleaves the fsf block BETWEEN the LKCP-restore (sets `_redo`) and the contact-marking (reads the manufactured plane) (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ValidateTransition` fsf tail) | acdream deliberately fused ACE's separate LKCP-restore + contact-mark blocks (the L.2.3c/L.2.4/A6.P3 contact-retention divergences); running the ladder after them and re-marking grounding inside the manufacture branch is semantically equal (a grounded wall-slide is not a stuck-fall in either arrangement) without disturbing those hard-won fixes | If a future contact-retention change alters when OnWalkable is set relative to the ladder, `_redo` could misclassify a frame (grounded-jam mistaken for stuck-fall → spurious velocity zero, or vice-versa) — the fsf conformance tests pin the current arrangement | `CTransition::validate_transition` 0x0050aa70 pc:272625-656; ACE Transition.cs:1029-1061 |
|
| AD-39 | The `frames_stationary_fall` ladder + fsf≥3 UP-contact-plane manufacture runs AFTER acdream's fused LKCP-restore/contact-marking block, deriving retail's `_redo` as `cleanAdvance \|\| OnWalkable`; retail (ACE Transition.cs:1029-1061) interleaves the fsf block BETWEEN the LKCP-restore (sets `_redo`) and the contact-marking (reads the manufactured plane) (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ValidateTransition` fsf tail) | acdream deliberately fused ACE's separate LKCP-restore + contact-mark blocks (the L.2.3c/L.2.4/A6.P3 contact-retention divergences); running the ladder after them and re-marking grounding inside the manufacture branch is semantically equal (a grounded wall-slide is not a stuck-fall in either arrangement) without disturbing those hard-won fixes | If a future contact-retention change alters when OnWalkable is set relative to the ladder, `_redo` could misclassify a frame (grounded-jam mistaken for stuck-fall → spurious velocity zero, or vice-versa) — the fsf conformance tests pin the current arrangement | `CTransition::validate_transition` 0x0050aa70 pc:272625-656; ACE Transition.cs:1029-1061 |
|
||||||
| AD-40 | The fsf `Stationary*` transient-bit encode (fsf→0x10/0x20/0x40) lives in the Core resolve writeback (`PhysicsEngine.ResolveWithTransition`), co-located with the fsf computation; retail encodes it in `handle_all_collisions` (pc:282737-758). Also: `PhysicsBody.CachedVelocity` is computed at the player chokepoint but not yet consumed — outbound wire velocity still uses the existing `get_state_velocity` path, not retail's cached_velocity source (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/PhysicsEngine.cs` (writeback); `src/AcDream.App/Input/PlayerMovementController.cs` (`CachedVelocity`) | Encoding in the writeback keeps the seed→ladder→writeback→seed round-trip self-contained in Core (testable without the App loop); the bit values + timing are identical to retail's (set after fsf is final, before the next resolve). CachedVelocity is faithful to carry now; routing the wire through it is a separate, unmeasured change | If a future consumer reads the Stationary* bits expecting retail's handle_all_collisions to have set them (it doesn't run in Core), the Core writeback is the source of truth; a wire-reporting change that assumes CachedVelocity is live would send the wrong velocity until it's wired | `handle_all_collisions` bit encode pc:282737-758; `get_velocity` 0x005113c0 (cached_velocity reader) |
|
| AD-40 | The fsf `Stationary*` transient-bit encode (fsf→0x10/0x20/0x40) lives in the Core resolve writeback (`PhysicsEngine.ResolveWithTransition`), co-located with the fsf computation; retail encodes it in `handle_all_collisions` (pc:282737-758). Also: `PhysicsBody.CachedVelocity` is computed at the player chokepoint but not yet consumed — outbound wire velocity still uses the existing `get_state_velocity` path, not retail's cached_velocity source (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/PhysicsEngine.cs` (writeback); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (`CachedVelocity`) | Encoding in the writeback keeps the seed→ladder→writeback→seed round-trip self-contained in Core (testable without the App loop); the bit values + timing are identical to retail's (set after fsf is final, before the next resolve). CachedVelocity is faithful to carry now; routing the wire through it is a separate, unmeasured change | If a future consumer reads the Stationary* bits expecting retail's handle_all_collisions to have set them (it doesn't run in Core), the Core writeback is the source of truth; a wire-reporting change that assumes CachedVelocity is live would send the wrong velocity until it's wired | `handle_all_collisions` bit encode pc:282737-758; `get_velocity` 0x005113c0 (cached_velocity reader) |
|
||||||
| AD-41 | The `candidateMoved` gate (retail UpdateObjectInternal pc:283657 `candidate != m_position`) suppresses ONLY `handle_all_collisions` + `cached_velocity` on a no-move frame; acdream still runs `ResolveWithTransition` (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) | `src/AcDream.App/Input/PlayerMovementController.cs` (`candidateMoved` guard) | The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh | If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 pc:283657 (candidate-moved gate) |
|
| AD-41 | The `candidateMoved` gate (retail UpdateObjectInternal pc:283657 `candidate != m_position`) suppresses ONLY `handle_all_collisions` + `cached_velocity` on a no-move frame; acdream still runs `ResolveWithTransition` (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) | `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (`candidateMoved` guard) | The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh | If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 pc:283657 (candidate-moved gate) |
|
||||||
| AD-42 | Enter-world placement is split across two Core calls: legacy `Resolve` performs retail `AdjustPosition` + the host's established floor snap, then `ResolvePlacement` runs the verbatim object-aware `find_placement_pos` ring search. Retail runs initial environment placement, ring search, and final step-down inside one `find_placement_position` transition | `src/AcDream.App/Rendering/GameWindow.cs` (`EnterPlayerModeNow`); `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolvePlacement`) | The first call has already committed the same validated cell/floor point that feeds the ring search; the second call uses the same sphere dimensions, collision registry, and cell id. Keeping the split preserves the proven indoor-login snap while adding the missing occupied-position behavior | A spawn that requires retail's final placement step-down after a ring candidate (rather than the existing floor snap before it) could settle at a slightly different Z on a ledge/water boundary; the overlap is still cleared | `CPhysicsObj::enter_world` 0x00516170; `CTransition::find_placement_position` 0x0050C170; `CTransition::find_placement_pos` 0x0050BA50 |
|
| AD-42 | Enter-world placement is split across two Core calls: legacy `Resolve` performs retail `AdjustPosition` + the host's established floor snap, then `ResolvePlacement` runs the verbatim object-aware `find_placement_pos` ring search. Retail runs initial environment placement, ring search, and final step-down inside one `find_placement_position` transition | `src/AcDream.App/Rendering/GameWindow.cs` (`EnterPlayerModeNow`); `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolvePlacement`) | The first call has already committed the same validated cell/floor point that feeds the ring search; the second call uses the same sphere dimensions, collision registry, and cell id. Keeping the split preserves the proven indoor-login snap while adding the missing occupied-position behavior | A spawn that requires retail's final placement step-down after a ring candidate (rather than the existing floor snap before it) could settle at a slightly different Z on a ledge/water boundary; the overlap is still cleared | `CPhysicsObj::enter_world` 0x00516170; `CTransition::find_placement_position` 0x0050C170; `CTransition::find_placement_pos` 0x0050BA50 |
|
||||||
| AD-43 | A malformed/custom PhysicsScript `CallPES` cycle whose script timeline never advances is rejected with a diagnostic; retail's linked scheduler would continue draining that zero-time tail indefinitely | `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` (timeline-progress ancestry guard) | Prevents corrupt DAT content from hanging the single update/render thread. Installed-DAT audit plus conformance tests prove the real rolling-weather cycles advance 2.8 seconds per edge and continue unchanged; only a no-progress strongly connected cycle is rejected | A custom DAT that deliberately relies on an infinite zero-time loop observes a rejected play instead of freezing the client | `ScriptManager::AddScriptInternal` 0x0051B310; `ScriptManager::UpdateScripts` 0x0051B480; `CPhysicsObj::CallPES` 0x00511AF0 |
|
| AD-43 | A malformed/custom PhysicsScript `CallPES` cycle whose script timeline never advances is rejected with a diagnostic; retail's linked scheduler would continue draining that zero-time tail indefinitely | `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` (timeline-progress ancestry guard) | Prevents corrupt DAT content from hanging the single update/render thread. Installed-DAT audit plus conformance tests prove the real rolling-weather cycles advance 2.8 seconds per edge and continue unchanged; only a no-progress strongly connected cycle is rejected | A custom DAT that deliberately relies on an infinite zero-time loop observes a rejected play instead of freezing the client | `ScriptManager::AddScriptInternal` 0x0051B310; `ScriptManager::UpdateScripts` 0x0051B480; `CPhysicsObj::CallPES` 0x00511AF0 |
|
||||||
| AD-44 | acdream has no retained character-management screen: startup deterministically selects the first active, non-greyed CharacterList identity, and native-window close performs retail's complete character-logoff handshake plus transport disconnect before exiting instead of returning to character selection. One active `ReceiverData` equivalent means `ClientNet::LogOffServer`'s per-receiver loop sends one header. | `src/AcDream.Core.Net/Messages/CharacterList.cs` (`TrySelectFirstAvailable`); `src/AcDream.App/Rendering/GameWindow.cs` (live-session bootstrap, moving to `LiveSessionController` in Slice 3); `src/AcDream.Core.Net/WorldSession.cs` (`SelectCharacterForEnterWorld`, `Dispose`); `src/AcDream.Core.Net/Packets/TransportDisconnect.cs` | This preserves unattended startup and immediate ACE endpoint release while validating that the chosen identity is active/non-greyed and using the server's canonical account. A future retained character-management owner is separate UI/session work. | An account with multiple playable characters enters the first wire-order identity without retail's explicit choice. An eventual in-client "log off character" action cannot reuse the process-exit path; it must retain the authenticated socket after server `0xF653` and return to character management. | `gmCharacterManagementUI::SelectCharacter @ 0x004EC160`; `gmCharacterManagementUI::EnterGame @ 0x004ED440`; `gmCharGenMainUI::Update @ 0x004E8460`; `Proto_UI::LogOffCharacter @ 0x00546A20`; `CPlayerSystem::RequestLogOff @ 0x00562DD0`; `CPlayerSystem::ExecuteLogOff @ 0x0055D780`; `ClientNet::LogOffServer @ 0x00543EF0`; `SharedNet::SendOptionalHeader @ 0x00543160` |
|
| AD-44 | acdream has no retained character-management screen: startup deterministically selects the first active, non-greyed CharacterList identity, and native-window close performs retail's complete character-logoff handshake plus transport disconnect before exiting instead of returning to character selection. One active `ReceiverData` equivalent means `ClientNet::LogOffServer`'s per-receiver loop sends one header. | `src/AcDream.Core.Net/Messages/CharacterList.cs` (`TrySelectFirstAvailable`); `src/AcDream.App/Rendering/GameWindow.cs` (live-session bootstrap, moving to `LiveSessionController` in Slice 3); `src/AcDream.Core.Net/WorldSession.cs` (`SelectCharacterForEnterWorld`, `Dispose`); `src/AcDream.Core.Net/Packets/TransportDisconnect.cs` | This preserves unattended startup and immediate ACE endpoint release while validating that the chosen identity is active/non-greyed and using the server's canonical account. A future retained character-management owner is separate UI/session work. | An account with multiple playable characters enters the first wire-order identity without retail's explicit choice. An eventual in-client "log off character" action cannot reuse the process-exit path; it must retain the authenticated socket after server `0xF653` and return to character management. | `gmCharacterManagementUI::SelectCharacter @ 0x004EC160`; `gmCharacterManagementUI::EnterGame @ 0x004ED440`; `gmCharGenMainUI::Update @ 0x004E8460`; `Proto_UI::LogOffCharacter @ 0x00546A20`; `CPlayerSystem::RequestLogOff @ 0x00562DD0`; `CPlayerSystem::ExecuteLogOff @ 0x0055D780`; `ClientNet::LogOffServer @ 0x00543EF0`; `SharedNet::SendOptionalHeader @ 0x00543160` |
|
||||||
|
|
@ -135,12 +135,12 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| AP-21 | Entity translucency retains the invented α<0.05 fragment discard. World GfxObj/Setup instances now apply their DAT AlphaBlend/Additive/InvAlpha factors through the retail shared alpha queue, but sealed off-screen WbDrawDispatcher consumers (paperdoll/UI Studio) retain the old immediate normal-alpha pass for all three kinds | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`DrawDeferredAlphaBatch` versus immediate Phase 8) | World presentation needed exact per-surface blend for spell/particle density and translucent-object intersections. Off-screen object previews are isolated render targets and have not shown an authored additive entity surface that justifies splitting their compact immediate pass | A faint world fringe below 5% alpha is discarded; a hypothetical additive/inverse-alpha paperdoll or UI Studio entity composites darker than retail inside that private viewport | `D3DPolyRender::SetSurface`; `D3DPolyRender::RenderMeshSubset`; SurfaceType.Additive → D3DBLEND_ONE |
|
| AP-21 | Entity translucency retains the invented α<0.05 fragment discard. World GfxObj/Setup instances now apply their DAT AlphaBlend/Additive/InvAlpha factors through the retail shared alpha queue, but sealed off-screen WbDrawDispatcher consumers (paperdoll/UI Studio) retain the old immediate normal-alpha pass for all three kinds | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`DrawDeferredAlphaBatch` versus immediate Phase 8) | World presentation needed exact per-surface blend for spell/particle density and translucent-object intersections. Off-screen object previews are isolated render targets and have not shown an authored additive entity surface that justifies splitting their compact immediate pass | A faint world fringe below 5% alpha is discarded; a hypothetical additive/inverse-alpha paperdoll or UI Studio entity composites darker than retail inside that private viewport | `D3DPolyRender::SetSurface`; `D3DPolyRender::RenderMeshSubset`; SurfaceType.Additive → D3DBLEND_ONE |
|
||||||
| AP-22 | Invented `setup.Radius` cylinder (height = Height or Radius×2) for shapeless live entities; shape + height formula not from the retail shape walk | `src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs`; `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` | ShadowShapeBuilder (faithful walk) only emits CylSphere/Sphere/Part-BSP; the legacy cylinder preserves prior behavior so rare decorative props don't lose collision | Those props collide with an invented footprint (especially the Radius×2 height guess) — slides/blocks at non-retail distances | `find_obj_collisions` → `CPartArray::FindObjCollisions` pc:286236 |
|
| AP-22 | Invented `setup.Radius` cylinder (height = Height or Radius×2) for shapeless live entities; shape + height formula not from the retail shape walk | `src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs`; `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` | ShadowShapeBuilder (faithful walk) only emits CylSphere/Sphere/Part-BSP; the legacy cylinder preserves prior behavior so rare decorative props don't lose collision | Those props collide with an invented footprint (especially the Radius×2 height guess) — slides/blocks at non-retail distances | `find_obj_collisions` → `CPartArray::FindObjCollisions` pc:286236 |
|
||||||
| AP-23 | Invented per-type pickup-radius heuristic (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m rest) for close-range gating plus the speculative local TurnToObject/MoveToObject install through the player's MoveToManager. **R5-V3 narrowed it:** the install threads the target's real Setup radius/height (`GetSetupCylinder`, same as wire mt-6) and the player's real radius; only the radius buckets remain invented. **Use retired from this seam 2026-07-25** and now sends immediately. | `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`TryGetApproach`/`GetUseRadius`); `src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs` (`BeginApproach`) | The retained pickup presentation reserves a destination slot before the authoritative transfer; its close branch still needs an arrival boundary | A target whose real UseRadius differs from the bucket misjudges the pickup gate — pickup waits forever or fires early into a server "too far" | ACE Player_Move.cs:66; wire MoveToObject (type 6) carries the true radius; `CPhysicsObj::TurnToObject/MoveToObject` callers §9a/§9b |
|
| AP-23 | Invented per-type pickup-radius heuristic (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m rest) for close-range gating plus the speculative local TurnToObject/MoveToObject install through the player's MoveToManager. **R5-V3 narrowed it:** the install threads the target's real Setup radius/height (`GetSetupCylinder`, same as wire mt-6) and the player's real radius; only the radius buckets remain invented. **Use retired from this seam 2026-07-25** and now sends immediately. | `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`TryGetApproach`/`GetUseRadius`); `src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs` (`BeginApproach`) | The retained pickup presentation reserves a destination slot before the authoritative transfer; its close branch still needs an arrival boundary | A target whose real UseRadius differs from the bucket misjudges the pickup gate — pickup waits forever or fires early into a server "too far" | ACE Player_Move.cs:66; wire MoveToObject (type 6) carries the true radius; `CPhysicsObj::TurnToObject/MoveToObject` callers §9a/§9b |
|
||||||
| ~~AP-24~~ | **RETIRED 2026-07-11** — matching v11.4186 x86 disassembly recovered `ATTACK_POWERUP_TIME=1.0` seconds and `DUAL_WIELD_POWERUP_TIME=0.8` seconds from the operands loaded by `GetPowerBarLevel`; jump and combat now share those constants. | `src/AcDream.Core/Combat/CombatModel.cs`; `src/AcDream.App/Input/PlayerMovementController.cs`; `src/AcDream.Runtime/Gameplay/RuntimeCombatAttackState.cs` | — | — | `ClientCombatSystem::GetPowerBarLevel @ 0x0056ADE0`; static data `0x007CEFC8/0x007CEFD0` |
|
| ~~AP-24~~ | **RETIRED 2026-07-11** — matching v11.4186 x86 disassembly recovered `ATTACK_POWERUP_TIME=1.0` seconds and `DUAL_WIELD_POWERUP_TIME=0.8` seconds from the operands loaded by `GetPowerBarLevel`; jump and combat now share those constants. | `src/AcDream.Core/Combat/CombatModel.cs`; `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`; `src/AcDream.Runtime/Gameplay/RuntimeCombatAttackState.cs` | — | — | `ClientCombatSystem::GetPowerBarLevel @ 0x0056ADE0`; static data `0x007CEFC8/0x007CEFD0` |
|
||||||
| AP-25 | Run/Jump skill pushed to movement = attributeBonus + Init + Ranks — no augmentations, multipliers, or vitae | `src/AcDream.Core.Net/GameEventWiring.cs:346` | Closest to ACE's CreatureSkill.Current short of porting the full Aug/Multiplier/Vitae chain (K-fix7/13) | A character with augs or post-death vitae predicts wrong local run speed / jump arc — dying would NOT slow the local player though the server moves them slower: drift + snap-back | ACE CreatureSkill.Current; ACE Skill.cs (Jump=22, Run=24) |
|
| AP-25 | Run/Jump skill pushed to movement = attributeBonus + Init + Ranks — no augmentations, multipliers, or vitae | `src/AcDream.Core.Net/GameEventWiring.cs:346` | Closest to ACE's CreatureSkill.Current short of porting the full Aug/Multiplier/Vitae chain (K-fix7/13) | A character with augs or post-death vitae predicts wrong local run speed / jump arc — dying would NOT slow the local player though the server moves them slower: drift + snap-back | ACE CreatureSkill.Current; ACE Skill.cs (Jump=22, Run=24) |
|
||||||
| AP-26 | DDD interrogation answered with an empty dat-version list (count=0); retail reports actual dat iteration state | `src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs:18` | ACE is satisfied by the empty ack; pattern from holtburger | A dat-patching-enabled server could push a full patch or reject on version mismatch — the lie is harmless only while the server never acts on it | DDD flow 0xF7E5/0xF7E6 |
|
| AP-26 | DDD interrogation answered with an empty dat-version list (count=0); retail reports actual dat iteration state | `src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs:18` | ACE is satisfied by the empty ack; pattern from holtburger | A dat-patching-enabled server could push a full patch or reject on version mismatch — the lie is harmless only while the server never acts on it | DDD flow 0xF7E5/0xF7E6 |
|
||||||
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
|
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
|
||||||
| AP-28 | 3D audio falloff via OpenAL InverseDistanceClamped with picked constants (ref 2 m, max 1000 m, rolloff 1); voice pool/eviction IS cited to retail | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:146` | Stands in for retail's DirectSound-era attenuation; r05 §5.3 documents inverse-square behavior but the three AL params were picked, not ported | Sounds attenuate at a different rate — too loud/quiet at range side-by-side; gain-driven eviction comparisons inherit the skew | FUN_00550ad0 (voice pool only); r05 §5.3 |
|
| AP-28 | 3D audio falloff via OpenAL InverseDistanceClamped with picked constants (ref 2 m, max 1000 m, rolloff 1); voice pool/eviction IS cited to retail | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:146` | Stands in for retail's DirectSound-era attenuation; r05 §5.3 documents inverse-square behavior but the three AL params were picked, not ported | Sounds attenuate at a different rate — too loud/quiet at range side-by-side; gain-driven eviction comparisons inherit the skew | FUN_00550ad0 (voice pool only); r05 §5.3 |
|
||||||
| AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.App/Input/PlayerMovementController.cs:1110` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 |
|
| AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1110` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 |
|
||||||
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
||||||
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` (`ShellDrawLiftZ`); `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`DrawPortalDepthWrite`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` (`ShellDrawLiftZ`); `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`DrawPortalDepthWrite`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
||||||
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
||||||
|
|
@ -180,7 +180,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
---
|
---
|
||||||
|
|
||||||
| AP-75 | **NARROWED 2026-07-19 — adapter-boundary `adjust_motion` only.** `SetCycle` remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in `CMotionInterp`; GameWindow's local-player adapter can still pass raw ids directly | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap) | Preserves raw local callers until every caller enters through `MotionInterpreter`; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | `CMotionInterp::adjust_motion` @305343; retire with the remaining local caller unification |
|
| AP-75 | **NARROWED 2026-07-19 — adapter-boundary `adjust_motion` only.** `SetCycle` remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in `CMotionInterp`; GameWindow's local-player adapter can still pass raw ids directly | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap) | Preserves raw local callers until every caller enters through `MotionInterpreter`; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | `CMotionInterp::adjust_motion` @305343; retire with the remaining local caller unification |
|
||||||
| AP-77 | **NARROWED 2026-07-19 — animation-less/headless movement fallback only.** When `MotionInterpreter.DefaultSink` or the local PartArray callback is absent, acdream writes grounded command-derived body velocity and applies the DAT-pinned Humanoid `TurnRight` rate (1.5 radians/second) directly to the body Frame. Production animated players/remotes bind `MotionTableDispatchSink` plus CSequence and instead consume the complete DAT-authored root Frame; that path preserves airborne orientation while suppressing only origin exactly like retail | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`); `src/AcDream.App/Input/PlayerMovementController.cs` (no-PartArray object-quantum fallback) | Keeps isolated/headless physics tests and a deliberately animation-less entity controllable without fabricating a PartArray | A future production entity missing its animation binding uses Humanoid-only yaw/velocity, can foot-slide, and can rotate through an airborne quantum differently from a real creature's DAT Frame | `CMotionInterp::apply_interpreted_movement` 0x00528600; `CPhysicsObj::UpdatePositionInternal` 0x00512C30; retire when animation-less production objects have an explicit motion owner |
|
| AP-77 | **NARROWED 2026-07-19 — animation-less/headless movement fallback only.** When `MotionInterpreter.DefaultSink` or the local PartArray callback is absent, acdream writes grounded command-derived body velocity and applies the DAT-pinned Humanoid `TurnRight` rate (1.5 radians/second) directly to the body Frame. Production animated players/remotes bind `MotionTableDispatchSink` plus CSequence and instead consume the complete DAT-authored root Frame; that path preserves airborne orientation while suppressing only origin exactly like retail | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (no-PartArray object-quantum fallback) | Keeps isolated/headless physics tests and a deliberately animation-less entity controllable without fabricating a PartArray | A future production entity missing its animation binding uses Humanoid-only yaw/velocity, can foot-slide, and can rotate through an airborne quantum differently from a real creature's DAT Frame | `CMotionInterp::apply_interpreted_movement` 0x00528600; `CPhysicsObj::UpdatePositionInternal` 0x00512C30; retire when animation-less production objects have an explicit motion owner |
|
||||||
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables. The adaptation is now structurally limited to replacing Ready/Walk/Run-family states, so authoritative actions/substates (especially Dead) always win. | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`, `CanApplyVelocityCycle`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
|
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables. The adaptation is now structurally limited to replacing Ready/Walk/Run-family states, so authoritative actions/substates (especially Dead) always win. | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`, `CanApplyVelocityCycle`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
|
||||||
| AP-81 | **Remote-DR VectorUpdate adds airborne/contact state beyond retail and toggles gravity via the Gravity STATE bit**: the handler writes velocity/omega, then may set Airborne, set `Body.State \|= Gravity`, clear contact, and call `LeaveGround`; both landing blocks clear Gravity after `HitGround()`. Retail `DoVectorUpdate` only writes velocity and omega, keeps GRAVITY set for the object's whole life, and gates gravity ACCELERATION on the Contact transient (`calc_acceleration`) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so `Motion.HitGround()`'s verbatim `state&0x400` gate runs BEFORE the clear). | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (VectorUpdate jump handler); `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (landing blocks) | The extra branch makes an inbound nonzero vertical velocity start the current remote airborne integration even without the complete retail contact-gated acceleration chain; the flag dance delivers gravity only while airborne and the #161 ordering fix keeps the retail HitGround contract satisfied. Slice 4 isolates it rather than changing accepted remote motion during extraction. | A VectorUpdate sent while contact state should remain authoritative can make the remote leave ground earlier than retail; any new call into `Motion.HitGround`/`LeaveGround` placed after the clear silently no-ops on the gravity gate; grounded remotes carry a non-retail state word. | `SmartBox::DoVectorUpdate @ 0x004521C0`; `CPhysicsObj::calc_acceleration`; `set_on_walkable @ 0x00511310`; retire when the complete contact-gated acceleration path owns remote motion. |
|
| AP-81 | **Remote-DR VectorUpdate adds airborne/contact state beyond retail and toggles gravity via the Gravity STATE bit**: the handler writes velocity/omega, then may set Airborne, set `Body.State \|= Gravity`, clear contact, and call `LeaveGround`; both landing blocks clear Gravity after `HitGround()`. Retail `DoVectorUpdate` only writes velocity and omega, keeps GRAVITY set for the object's whole life, and gates gravity ACCELERATION on the Contact transient (`calc_acceleration`) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so `Motion.HitGround()`'s verbatim `state&0x400` gate runs BEFORE the clear). | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (VectorUpdate jump handler); `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (landing blocks) | The extra branch makes an inbound nonzero vertical velocity start the current remote airborne integration even without the complete retail contact-gated acceleration chain; the flag dance delivers gravity only while airborne and the #161 ordering fix keeps the retail HitGround contract satisfied. Slice 4 isolates it rather than changing accepted remote motion during extraction. | A VectorUpdate sent while contact state should remain authoritative can make the remote leave ground earlier than retail; any new call into `Motion.HitGround`/`LeaveGround` placed after the clear silently no-ops on the gravity gate; grounded remotes carry a non-retail state word. | `SmartBox::DoVectorUpdate @ 0x004521C0`; `CPhysicsObj::calc_acceleration`; `set_on_walkable @ 0x00511310`; retire when the complete contact-gated acceleration path owns remote motion. |
|
||||||
| AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = −(speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) |
|
| AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = −(speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) |
|
||||||
|
|
@ -239,8 +239,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| TS-18 | `LandCell.BuildingCellId` (CSortCell building bridge) declared but never populated — always null in Stage 1 | `src/AcDream.Core/World/Cells/LandCell.cs:19` | Cell graph shipped in stages; population is explicitly membership Stage 2 (the outdoor→indoor entry path the physics digest flags as unvalidated) | Cell-graph paths that should discover a building's EnvCells from the outdoor cell silently find nothing — the doorway-entry bug class | CSortCell (acclient.h:31880) |
|
| TS-18 | `LandCell.BuildingCellId` (CSortCell building bridge) declared but never populated — always null in Stage 1 | `src/AcDream.Core/World/Cells/LandCell.cs:19` | Cell graph shipped in stages; population is explicitly membership Stage 2 (the outdoor→indoor entry path the physics digest flags as unvalidated) | Cell-graph paths that should discover a building's EnvCells from the outdoor cell silently find nothing — the doorway-entry bug class | CSortCell (acclient.h:31880) |
|
||||||
| TS-19 | Legacy non-retail ChaseCamera (invented pitch/distance, K-fix12 airborne Z-pin) retained behind `ACDREAM_RETAIL_CHASE=0` / DebugPanel toggle; both update every frame | `src/AcDream.App/Rendering/ChaseCamera.cs:49` | Diagnostic before/after comparison path, "pending the follow-up deletion commit" | When toggled on, the eye diverges from retail's spring-arm — and the render roots at the VIEWER cell, so a non-retail eye changes the render root near doorways, masking or manufacturing flap symptoms during debugging | `CameraManager::UpdateCamera` (retail path in RetailChaseCamera.cs) |
|
| TS-19 | Legacy non-retail ChaseCamera (invented pitch/distance, K-fix12 airborne Z-pin) retained behind `ACDREAM_RETAIL_CHASE=0` / DebugPanel toggle; both update every frame | `src/AcDream.App/Rendering/ChaseCamera.cs:49` | Diagnostic before/after comparison path, "pending the follow-up deletion commit" | When toggled on, the eye diverges from retail's spring-arm — and the render roots at the VIEWER cell, so a non-retail eye changes the render root near doorways, masking or manufacturing flap symptoms during debugging | `CameraManager::UpdateCamera` (retail path in RetailChaseCamera.cs) |
|
||||||
| ~~TS-20~~ | **RETIRED AS A FALSE ATTRIBUTION 2026-07-16** — `CGfxObj::InitLoad` passes the complete polygon array to `D3DPolyRender::ConstructMesh`; ordinary GfxObj rendering does not filter it through DrawingBSP. Building DrawingBSP traversal discovers and orders portal apertures after `RemoveNonPortalNodes`; it is not a global visible-polygon selector. The alleged building-shell "orphans" are `DrawingBSPNode.Portals`, omitted by the old diagnostic collector; the corrected node-polygons ∪ portal-polygons audit finds no true orphans. Applying the proposed filter would repeat the door disappearance regression from `e46d3d9`. | `docs/research/2026-06-11-holistic-map/wf1-gfxobj-draw.md`; `docs/research/2026-06-11-holistic-map/wf1-building-shells.md`; `tests/AcDream.Core.Tests/Rendering/Wb/Issue113DoorVanishDiagnosticTests.cs` | — | — | `CGfxObj::InitLoad @ 0x005346B0`; `D3DPolyRender::ConstructMesh @ 0x0059DFA0`; `BSPTREE::build_draw_portals_only @ 0x00539860` |
|
| ~~TS-20~~ | **RETIRED AS A FALSE ATTRIBUTION 2026-07-16** — `CGfxObj::InitLoad` passes the complete polygon array to `D3DPolyRender::ConstructMesh`; ordinary GfxObj rendering does not filter it through DrawingBSP. Building DrawingBSP traversal discovers and orders portal apertures after `RemoveNonPortalNodes`; it is not a global visible-polygon selector. The alleged building-shell "orphans" are `DrawingBSPNode.Portals`, omitted by the old diagnostic collector; the corrected node-polygons ∪ portal-polygons audit finds no true orphans. Applying the proposed filter would repeat the door disappearance regression from `e46d3d9`. | `docs/research/2026-06-11-holistic-map/wf1-gfxobj-draw.md`; `docs/research/2026-06-11-holistic-map/wf1-building-shells.md`; `tests/AcDream.Core.Tests/Rendering/Wb/Issue113DoorVanishDiagnosticTests.cs` | — | — | `CGfxObj::InitLoad @ 0x005346B0`; `D3DPolyRender::ConstructMesh @ 0x0059DFA0`; `BSPTREE::build_draw_portals_only @ 0x00539860` |
|
||||||
| TS-21 | Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands (the stale "we don't parse yet" comment was FIXED in R4-V5; K-fix7 parses PD → SetCharacterSkills) | `src/AcDream.App/Input/PlayerMovementController.cs:311` | Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) | Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs | retail height = (skill/(skill+1300))×22.2 + 0.05 |
|
| TS-21 | Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands (the stale "we don't parse yet" comment was FIXED in R4-V5; K-fix7 parses PD → SetCharacterSkills) | `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:311` | Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) | Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs | retail height = (skill/(skill+1300))×22.2 + 0.05 |
|
||||||
| TS-23 | PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always `IsPlayer ∣ EdgeSlide` — for BOTH the LOCAL player mover and, as of **#184 Slice 2b**, every remote-PLAYER dead-reckoning mover | `src/AcDream.App/Input/PlayerMovementController.cs:1177`; `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (`Tick` sweep, `IsPlayerGuid` branch) | Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults. Slice 2b gave the remote-player mover `IsPlayer` (was bare `EdgeSlide`) so remote-vs-remote non-PK players WALK THROUGH exactly like the local player and like retail (they still collide with monsters + terrain + walls); without it Slice 2b would have de-overlapped players (MORE solid than retail) | On a PK/PKLite character the client lets players walk through where retail collides — now for the local player AND remote-vs-remote — the moment PvP statuses enter play (M2+) | PWD._bitfield acclient.h:6431-6463; pc:406898-406918; FindObjCollisions PvP block pc:276812 (mover IsPlayer via OBJECTINFO::init 0x0050cf30 `state\|=0x100`) |
|
| TS-23 | PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always `IsPlayer ∣ EdgeSlide` — for BOTH the LOCAL player mover and, as of **#184 Slice 2b**, every remote-PLAYER dead-reckoning mover | `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1177`; `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (`Tick` sweep, `IsPlayerGuid` branch) | Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults. Slice 2b gave the remote-player mover `IsPlayer` (was bare `EdgeSlide`) so remote-vs-remote non-PK players WALK THROUGH exactly like the local player and like retail (they still collide with monsters + terrain + walls); without it Slice 2b would have de-overlapped players (MORE solid than retail) | On a PK/PKLite character the client lets players walk through where retail collides — now for the local player AND remote-vs-remote — the moment PvP statuses enter play (M2+) | PWD._bitfield acclient.h:6431-6463; pc:406898-406918; FindObjCollisions PvP block pc:276812 (mover IsPlayer via OBJECTINFO::init 0x0050cf30 `state\|=0x100`) |
|
||||||
| TS-24 | RawMotionState action list always empty at runtime — the packer emits `num_actions` (bits 11–15) + per-action u16 pairs (L.2b, `RawMotionState::Pack` 0x0051ed10), and R3-W1 gives `RawMotionState`/`InterpretedMotionState` the retail-faithful action FIFO (`AddAction`/`RemoveAction`/`ApplyMotion`/`RemoveMotion`, `src/AcDream.Core/Physics/RawMotionState.cs` + `MotionInterpreter.cs`), but nothing calls `AddAction` yet — the outbound caller still builds an empty `Actions` list, so discrete motion events (emotes, one-shots) are still never broadcast | `src/AcDream.App/Rendering/GameWindow.cs:8297` (empty Actions); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91`; FIFO capability `src/AcDream.Core/Physics/RawMotionState.cs` | Discrete client-initiated motions (D2) not wired yet; packer-ready, state-ready (W1), runtime emission lands with R3-W2's `add_to_queue`/`DoInterpretedMotion` population | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | `RawMotionState::Pack` 0x0051ed10; num_actions `PackBitfield` acclient.h:46487 |
|
| TS-24 | RawMotionState action list always empty at runtime — the packer emits `num_actions` (bits 11–15) + per-action u16 pairs (L.2b, `RawMotionState::Pack` 0x0051ed10), and R3-W1 gives `RawMotionState`/`InterpretedMotionState` the retail-faithful action FIFO (`AddAction`/`RemoveAction`/`ApplyMotion`/`RemoveMotion`, `src/AcDream.Core/Physics/RawMotionState.cs` + `MotionInterpreter.cs`), but nothing calls `AddAction` yet — the outbound caller still builds an empty `Actions` list, so discrete motion events (emotes, one-shots) are still never broadcast | `src/AcDream.App/Rendering/GameWindow.cs:8297` (empty Actions); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91`; FIFO capability `src/AcDream.Core/Physics/RawMotionState.cs` | Discrete client-initiated motions (D2) not wired yet; packer-ready, state-ready (W1), runtime emission lands with R3-W2's `add_to_queue`/`DoInterpretedMotion` population | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | `RawMotionState::Pack` 0x0051ed10; num_actions `PackBitfield` acclient.h:46487 |
|
||||||
| TS-25 | `current_style` (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves `CurrentStyle` at default (stance not tracked here) | `src/AcDream.App/Rendering/GameWindow.cs:8286` (CurrentStyle left default); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80` | Stance switching is M2 combat scope | Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement | `RawMotionState::Pack` current_style 0x0051ed10 |
|
| TS-25 | `current_style` (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves `CurrentStyle` at default (stance not tracked here) | `src/AcDream.App/Rendering/GameWindow.cs:8286` (CurrentStyle left default); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80` | Stance switching is M2 combat scope | Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement | `RawMotionState::Pack` current_style 0x0051ed10 |
|
||||||
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
||||||
|
|
@ -249,14 +249,14 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| TS-30 | Chat DAT elements `0x10000522`–`0x10000525` render but have no controller semantics; the older claim that they are numbered in-window filter tabs is **unproven** | `src/AcDream.App/UI/Layout/ChatWindowController.cs` | Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; correction in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
| TS-30 | Chat DAT elements `0x10000522`–`0x10000525` render but have no controller semantics; the older claim that they are numbered in-window filter tabs is **unproven** | `src/AcDream.App/UI/Layout/ChatWindowController.cs` | Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; correction in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
||||||
| TS-31 | **NARROWED 2026-07-13** — `/squelch`, `/unsquelch`, `/filter`, `/unfilter`, and `/messagetypes` send the exact modification events and consume the authoritative retail `SquelchDB`; incoming `ChatLog` lines are not yet filtered through that database, and clickable name-tag social actions remain absent | `src/AcDream.Core/Social/SquelchState.cs`; `src/AcDream.Core.Net/Messages/SocialStateMessages.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core/Chat/ChatLog.cs` | Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical | A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable | `SquelchDB::UnPack @ 0x006B1900`; `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
| TS-31 | **NARROWED 2026-07-13** — `/squelch`, `/unsquelch`, `/filter`, `/unfilter`, and `/messagetypes` send the exact modification events and consume the authoritative retail `SquelchDB`; incoming `ChatLog` lines are not yet filtered through that database, and clickable name-tag social actions remain absent | `src/AcDream.Core/Social/SquelchState.cs`; `src/AcDream.Core.Net/Messages/SocialStateMessages.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core/Chat/ChatLog.cs` | Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical | A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable | `SquelchDB::UnPack @ 0x006B1900`; `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
||||||
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
||||||
| TS-33 | **NARROWED 2026-07-15** — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; `ShouldSendPositionEvent` compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves `UseTime` performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced | `src/AcDream.App/Rendering/GameWindow.cs` (outbound MTS/AP blocks); `src/AcDream.App/Input/PlayerMovementController.cs` (ported tracker) | Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus `UseTime`; do not infer it from `UseTime` alone | In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes | `CommandInterpreter::UseTime` 0x006B3BF0; `SendMovementEvent` 0x006B4680; `SendPositionEvent` 0x006B4770; `ShouldSendPositionEvent` 0x006B45E0; `Frame::is_equal` 0x00424C30 |
|
| TS-33 | **NARROWED 2026-07-15** — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; `ShouldSendPositionEvent` compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves `UseTime` performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced | `src/AcDream.Runtime/Gameplay/LocalPlayerOutboundController.cs` (pre/post network slots); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (ported tracker) | Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus `UseTime`; do not infer it from `UseTime` alone | In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes | `CommandInterpreter::UseTime` 0x006B3BF0; `SendMovementEvent` 0x006B4680; `SendPositionEvent` 0x006B4770; `ShouldSendPositionEvent` 0x006B45E0; `Frame::is_equal` 0x00424C30 |
|
||||||
| TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` |
|
| TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` |
|
||||||
| TS-35 | `PhysicsBody.IsFullyConstrained` is a stub property (default `false`, never set by any physics code), read by `jump_is_allowed`'s verbatim `IsFullyConstrained` gate (raw 305524-305525) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`IsFullyConstrained`) | R3-W3 needed the read site to port `jump_is_allowed`'s full chain. **R5-V1 CORRECTED the mechanism** (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the **ConstraintManager server-position rubber-band leash** — armed by `SmartBox::HandleReceivedPosition` on every inbound server position, `IsFullyConstrained` = `max*0.9 < offset`. R5-V1 ported `ConstraintManager` (`src/AcDream.Core/Physics/Motion/ConstraintManager.cs`) but does NOT arm it (no acdream `SmartBox` + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 | A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) | `CPhysicsObj::IsFullyConstrained` 0x0050ec60 → `ConstraintManager::IsFullyConstrained` 0x005560d0; `jump_is_allowed` 0x005282b0; arming `SmartBox::HandleReceivedPosition` 0x00453fd0 (issue #167) |
|
| TS-35 | `PhysicsBody.IsFullyConstrained` is a stub property (default `false`, never set by any physics code), read by `jump_is_allowed`'s verbatim `IsFullyConstrained` gate (raw 305524-305525) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`IsFullyConstrained`) | R3-W3 needed the read site to port `jump_is_allowed`'s full chain. **R5-V1 CORRECTED the mechanism** (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the **ConstraintManager server-position rubber-band leash** — armed by `SmartBox::HandleReceivedPosition` on every inbound server position, `IsFullyConstrained` = `max*0.9 < offset`. R5-V1 ported `ConstraintManager` (`src/AcDream.Core/Physics/Motion/ConstraintManager.cs`) but does NOT arm it (no acdream `SmartBox` + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 | A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) | `CPhysicsObj::IsFullyConstrained` 0x0050ec60 → `ConstraintManager::IsFullyConstrained` 0x005560d0; `jump_is_allowed` 0x005282b0; arming `SmartBox::HandleReceivedPosition` 0x00453fd0 (issue #167) |
|
||||||
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 |
|
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 |
|
||||||
| TS-38 | `MotionInterpreter.Initted` defaults to `true` in both constructors, not retail's `false` — retail's `CMotionInterp` is never observed pre-`enter_default_state` (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`Initted` property + both constructors) | Defaulting `true` is the C# equivalent of "the constructor already did what `enter_default_state` would have done to this flag" — `EnterDefaultState()` remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, `LeaveGround` tail) when a caller wants them | None observed: no code path needs `apply_current_movement`/`ReportExhaustion` to no-op before an explicit `EnterDefaultState()` call, since nothing constructs a `MotionInterpreter` and defers initialization today. If a future caller DOES need staged construction (build now, `EnterDefaultState()` later), it must explicitly set `Initted = false` first | `CMotionInterp::enter_default_state` 0x00528c80 @306124 sets `initted = 1`; retire if/when construction is staged through `EnterDefaultState()` uniformly |
|
| TS-38 | `MotionInterpreter.Initted` defaults to `true` in both constructors, not retail's `false` — retail's `CMotionInterp` is never observed pre-`enter_default_state` (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`Initted` property + both constructors) | Defaulting `true` is the C# equivalent of "the constructor already did what `enter_default_state` would have done to this flag" — `EnterDefaultState()` remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, `LeaveGround` tail) when a caller wants them | None observed: no code path needs `apply_current_movement`/`ReportExhaustion` to no-op before an explicit `EnterDefaultState()` call, since nothing constructs a `MotionInterpreter` and defers initialization today. If a future caller DOES need staged construction (build now, `EnterDefaultState()` later), it must explicitly set `Initted = false` first | `CMotionInterp::enter_default_state` 0x00528c80 @306124 sets `initted = 1`; retire if/when construction is staged through `EnterDefaultState()` uniformly |
|
||||||
| ~~TS-41~~ | **RETIRED 2026-07-07 (remote-creature de-overlap #184)** — the SERVERVEL synth-velocity body-drive (`Body.Velocity = ServerVelocity` / `get_state_velocity()` leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (`RemoteMotionCombiner.ComputeOffset` → `InterpolationManager::adjust_offset` toward the MoveOrTeleport-queued server waypoint) and `MovementManager::UseTime` (`TickRemoteMoveTo`) runs UNCONDITIONALLY per tick — the retail `UpdateObjectInternal` shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (`ApplyServerControlledVelocityCycle(Zero)` on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by **AP-80**; it no longer drives the body. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (grounded NPC branch) | — | — | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (`MovementManager::UseTime` @0x00515998, unconditional); `MoveOrTeleport` 0x00516330; `InterpolationManager::adjust_offset` 0x00555d30 |
|
| ~~TS-41~~ | **RETIRED 2026-07-07 (remote-creature de-overlap #184)** — the SERVERVEL synth-velocity body-drive (`Body.Velocity = ServerVelocity` / `get_state_velocity()` leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (`RemoteMotionCombiner.ComputeOffset` → `InterpolationManager::adjust_offset` toward the MoveOrTeleport-queued server waypoint) and `MovementManager::UseTime` (`TickRemoteMoveTo`) runs UNCONDITIONALLY per tick — the retail `UpdateObjectInternal` shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (`ApplyServerControlledVelocityCycle(Zero)` on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by **AP-80**; it no longer drives the body. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (grounded NPC branch) | — | — | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (`MovementManager::UseTime` @0x00515998, unconditional); `MoveOrTeleport` 0x00516330; `InterpolationManager::adjust_offset` 0x00555d30 |
|
||||||
| TS-44 | NPC UpdatePosition **enqueue is suppressed while StickyManager is armed** (`PositionManager.GetStickyObjectId() != 0`). Position and complete orientation otherwise share the ported `InterpolateTo → Position::subtract2 → PositionManager::adjust_offset` Frame, so the former orientation hard-snap residual is retired. Retail would still enqueue the server Position and let Sticky overwrite that Frame each tick; acdream retains the gate so no queued waypoint survives the stick | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (NPC `snapSuppressedByStick` gate) | Avoids replaying an old ACE waypoint immediately after a stick lease ends; all live during-stick pose ownership is now otherwise retail-shaped | After unstick the body waits for the next UP instead of consuming the latest waypoint already in the queue; at low packet cadence this can pause correction for one update interval | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330; retire by allowing enqueue while Sticky overwrites the shared complete Frame |
|
| TS-44 | NPC UpdatePosition **enqueue is suppressed while StickyManager is armed** (`PositionManager.GetStickyObjectId() != 0`). Position and complete orientation otherwise share the ported `InterpolateTo → Position::subtract2 → PositionManager::adjust_offset` Frame, so the former orientation hard-snap residual is retired. Retail would still enqueue the server Position and let Sticky overwrite that Frame each tick; acdream retains the gate so no queued waypoint survives the stick | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (NPC `snapSuppressedByStick` gate) | Avoids replaying an old ACE waypoint immediately after a stick lease ends; all live during-stick pose ownership is now otherwise retail-shaped | After unstick the body waits for the next UP instead of consuming the latest waypoint already in the queue; at low packet cadence this can pause correction for one update interval | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330; retire by allowing enqueue while Sticky overwrites the shared complete Frame |
|
||||||
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height − radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.App/Input/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`GetSetupCylinder`); `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (remote sweep fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal r−ε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
|
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height − radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`GetSetupCylinder`); `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (remote sweep fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal r−ε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
|
||||||
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-91` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
|
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-91` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
|
||||||
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
|
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
|
||||||
| TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` |
|
| TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` |
|
||||||
|
|
|
||||||
146
docs/research/2026-07-26-slice-j5-4-local-movement-ownership.md
Normal file
146
docs/research/2026-07-26-slice-j5-4-local-movement-ownership.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# Slice J5.4 — canonical local movement ownership
|
||||||
|
|
||||||
|
**Date:** 2026-07-26
|
||||||
|
**Parent:** `docs/plans/2026-07-26-modern-runtime-slice-j5.md`
|
||||||
|
**Scope:** ownership relocation only; no movement, collision, animation, or
|
||||||
|
wire behavior changes
|
||||||
|
|
||||||
|
## Retail oracle
|
||||||
|
|
||||||
|
The named September 2013 client remains the oracle:
|
||||||
|
|
||||||
|
| Mechanism | Named symbol | Address / named pseudo-C |
|
||||||
|
|---|---|---|
|
||||||
|
| take local control | `CommandInterpreter::TakeControlFromServer` | `0x006B32D0`, lines 699046–699145 |
|
||||||
|
| apply current raw movement | `CommandInterpreter::ApplyCurrentMovement` | `0x006B3430`, lines 699146–699530 |
|
||||||
|
| stop decision | `CommandInterpreter::MaybeStopCompletely` | `0x006B3B90`, lines 699531–699563 |
|
||||||
|
| post-inbound position slot | `CommandInterpreter::UseTime` | `0x006B3BF0`, lines 699564–699645 |
|
||||||
|
| input command boundary | `CommandInterpreter::MovePlayer` | `0x006B3F40`, lines 699803–700232 |
|
||||||
|
| AP predicate | `CommandInterpreter::ShouldSendPositionEvent` | `0x006B45E0`, lines 700233–700273 |
|
||||||
|
| MTS serialization | `CommandInterpreter::SendMovementEvent` | `0x006B4680`, lines 700274–700315 |
|
||||||
|
| AP serialization | `CommandInterpreter::SendPositionEvent` | `0x006B4770`, lines 700316 onward |
|
||||||
|
| object/root-motion step | `CPhysicsObj::UpdatePositionInternal` | `0x00512C30`, lines 280817 onward |
|
||||||
|
| object update envelope | `CPhysicsObj::update_object` | `0x00515D10`, lines 283950 onward |
|
||||||
|
|
||||||
|
The detailed wire unpack/pack interpretation and prior ACE/Holtburger
|
||||||
|
cross-checks are retained in:
|
||||||
|
|
||||||
|
- `docs/research/2026-05-04-l3-port/02-um-handling.md`;
|
||||||
|
- `docs/research/2026-05-04-l3-port/14-local-player-audit.md`;
|
||||||
|
- `docs/research/2026-05-01-retail-motion-trace/findings.md`;
|
||||||
|
- `docs/research/2026-07-26-slice-j5-owner-and-retail-order-inventory.md`.
|
||||||
|
|
||||||
|
The reference source trees are not present in this worktree, so J5.4 reuses
|
||||||
|
those already-recorded cross-checks rather than inventing a new interpretation.
|
||||||
|
No behavioral algorithm is changed by this slice.
|
||||||
|
|
||||||
|
## Consolidated retail pseudocode
|
||||||
|
|
||||||
|
### Input / object phase
|
||||||
|
|
||||||
|
```text
|
||||||
|
MovePlayer(command, speed, hold, ...)
|
||||||
|
if server currently controls movement
|
||||||
|
TakeControlFromServer()
|
||||||
|
|
||||||
|
mutate the player's one RawMotionState / MotionInterpreter
|
||||||
|
apply the current movement to the same CPhysicsObj
|
||||||
|
if the movement boundary requires a report
|
||||||
|
SendMovementEvent()
|
||||||
|
|
||||||
|
SendMovementEvent()
|
||||||
|
require player + SmartBox + raw motion state + autonomous movement
|
||||||
|
snapshot complete raw motion state
|
||||||
|
snapshot the player's canonical cell-local Position and contact
|
||||||
|
snapshot instance/server-control/teleport/force-position sequences
|
||||||
|
serialize and send MoveToState
|
||||||
|
stamp only last_sent_position_time after successful send
|
||||||
|
```
|
||||||
|
|
||||||
|
Jump uses the same pre-inbound input/object phase. The charged extent and
|
||||||
|
body-local launch velocity are read from the canonical local controller and
|
||||||
|
serialized exactly once on release.
|
||||||
|
|
||||||
|
### Object simulation
|
||||||
|
|
||||||
|
```text
|
||||||
|
UpdatePositionInternal(dt, rootFrame)
|
||||||
|
apply the animation-authored root frame to the canonical physics body
|
||||||
|
integrate/transition through the existing retail physics pipeline
|
||||||
|
commit complete Position, velocity, contact, and cell membership
|
||||||
|
|
||||||
|
update_object()
|
||||||
|
advance the retail object clock in fixed quanta
|
||||||
|
update movement and physics in retail order
|
||||||
|
leave one committed canonical body for every later borrower
|
||||||
|
```
|
||||||
|
|
||||||
|
App animation may supply the authored root frame and consume the committed
|
||||||
|
pose. It does not own a second body, movement state, or object clock.
|
||||||
|
|
||||||
|
### Post-inbound autonomous position phase
|
||||||
|
|
||||||
|
```text
|
||||||
|
UseTime()
|
||||||
|
after inbound messages may have corrected the canonical body:
|
||||||
|
if ShouldSendPositionEvent()
|
||||||
|
SendPositionEvent()
|
||||||
|
|
||||||
|
ShouldSendPositionEvent()
|
||||||
|
compare current cell/contact against the last AP baseline
|
||||||
|
after the retail interval, compare the complete current Frame
|
||||||
|
|
||||||
|
SendPositionEvent()
|
||||||
|
snapshot canonical cell-local Position, orientation, contact, timestamps
|
||||||
|
serialize and send AutonomousPosition
|
||||||
|
after successful send:
|
||||||
|
stamp last_sent_position_time
|
||||||
|
copy complete Position and contact plane into the AP baseline
|
||||||
|
```
|
||||||
|
|
||||||
|
This preserves the accepted acdream frame order:
|
||||||
|
|
||||||
|
1. graphical or no-window input becomes a typed `MovementInput`;
|
||||||
|
2. Runtime mutates the canonical controller/body;
|
||||||
|
3. Runtime emits MTS/jump before inbound dispatch;
|
||||||
|
4. inbound corrections mutate that same body;
|
||||||
|
5. Runtime evaluates/emits AP after inbound dispatch;
|
||||||
|
6. App projects the final committed body to animation, camera, and UI.
|
||||||
|
|
||||||
|
## Ownership result
|
||||||
|
|
||||||
|
- `PlayerMovementController`, its movement data types, and
|
||||||
|
`LocalPlayerOutboundController` now live in `AcDream.Runtime.Gameplay`.
|
||||||
|
- `RuntimeLocalPlayerMovementState` is the sole controller, construction
|
||||||
|
motion seam, autorun latch, typed view, and terminal ownership ledger.
|
||||||
|
- `DispatcherMovementInputSource` samples physical held state only and maps
|
||||||
|
configured edges onto the Runtime owner.
|
||||||
|
- direct Runtime movement commands and graphical input mutate that same
|
||||||
|
autorun state.
|
||||||
|
- `PlayerMovementConstructionOptions` replaces process environment reads and
|
||||||
|
is populated from `RuntimeMovementSkillState`.
|
||||||
|
- the App diagnostic implementation crosses only
|
||||||
|
`IMovementTruthDiagnosticSink`.
|
||||||
|
- shutdown retires graphical/session borrowers before disposing the Runtime
|
||||||
|
movement owner.
|
||||||
|
|
||||||
|
## Preserved boundaries
|
||||||
|
|
||||||
|
- Silk input, mouse filtering, camera, CSequence/PartArray presentation,
|
||||||
|
retained jump bar, sounds, and effects remain in App.
|
||||||
|
- `PhysicsEngine` remains App-owned until J5.5.
|
||||||
|
- remote movement and projectiles remain App-owned until J5.5/J5.6.
|
||||||
|
- TS-33's already-recorded possible same-update MTS/AP ordering difference is
|
||||||
|
preserved; this ownership move neither widens nor hides it.
|
||||||
|
|
||||||
|
## Automated evidence required before closeout
|
||||||
|
|
||||||
|
- Runtime state/view identity, two-instance isolation, construction lease,
|
||||||
|
terminal convergence, typed movement skills, and zero-allocation snapshots;
|
||||||
|
- graphical/direct autorun parity and one-production-owner source guards;
|
||||||
|
- existing bit-exact RawMotionState, MTS, jump, AP, quaternion, contact, and
|
||||||
|
strict cadence tests;
|
||||||
|
- existing run/walk/back/strafe/turn/mouse-look/jump/land, short-tap,
|
||||||
|
portal/reset, target-facing, and repeat-abort tests;
|
||||||
|
- Release build, complete solution tests, exact-binary connected lifecycle,
|
||||||
|
and canonical movement route.
|
||||||
|
|
@ -883,8 +883,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
d.UpdateClock,
|
d.UpdateClock,
|
||||||
live.SelectionInteractions,
|
live.SelectionInteractions);
|
||||||
gameplayInput);
|
|
||||||
bindings.Adopt("current game runtime adapter", gameRuntime);
|
bindings.Adopt("current game runtime adapter", gameRuntime);
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
"retained-UI game runtime commands",
|
"retained-UI game runtime commands",
|
||||||
|
|
|
||||||
7
src/AcDream.App/GlobalUsings.cs
Normal file
7
src/AcDream.App/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
global using AcDream.Runtime.Gameplay;
|
||||||
|
global using LocalPlayerControllerSlot =
|
||||||
|
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
|
||||||
|
global using ILocalPlayerControllerSource =
|
||||||
|
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource;
|
||||||
|
global using ILocalPlayerMotionSource =
|
||||||
|
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;
|
||||||
|
|
@ -8,18 +8,25 @@ internal interface IMovementInputSource
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owns held movement sampling and the retail autorun latch. The input
|
/// Samples held graphical input and maps press edges onto the Runtime-owned
|
||||||
/// dispatcher remains the only keyboard/mouse-button state source.
|
/// retail autorun latch. The dispatcher remains the only physical
|
||||||
|
/// keyboard/mouse-button state source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
||||||
{
|
{
|
||||||
|
private readonly RuntimeLocalPlayerMovementState _movement;
|
||||||
private readonly IInputCaptureSource? _capture;
|
private readonly IInputCaptureSource? _capture;
|
||||||
private InputDispatcher? _dispatcher;
|
private InputDispatcher? _dispatcher;
|
||||||
|
|
||||||
public DispatcherMovementInputSource(IInputCaptureSource? capture = null) =>
|
public DispatcherMovementInputSource(
|
||||||
|
RuntimeLocalPlayerMovementState movement,
|
||||||
|
IInputCaptureSource? capture = null)
|
||||||
|
{
|
||||||
|
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
|
||||||
_capture = capture;
|
_capture = capture;
|
||||||
|
}
|
||||||
|
|
||||||
public bool AutoRunActive { get; private set; }
|
public bool AutoRunActive => _movement.AutoRunActive;
|
||||||
public bool IsAvailable => _dispatcher is not null;
|
public bool IsAvailable => _dispatcher is not null;
|
||||||
|
|
||||||
public void Bind(InputDispatcher dispatcher)
|
public void Bind(InputDispatcher dispatcher)
|
||||||
|
|
@ -70,10 +77,8 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
||||||
public bool HandlePressedAction(InputAction action)
|
public bool HandlePressedAction(InputAction action)
|
||||||
{
|
{
|
||||||
if (action == InputAction.MovementRunLock)
|
if (action == InputAction.MovementRunLock)
|
||||||
{
|
return _movement.Execute(
|
||||||
AutoRunActive = !AutoRunActive;
|
AcDream.Runtime.RuntimeMovementCommand.ToggleRunLock);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AutoRunActive && action is (
|
if (AutoRunActive && action is (
|
||||||
InputAction.MovementBackup
|
InputAction.MovementBackup
|
||||||
|
|
@ -81,11 +86,11 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
||||||
or InputAction.MovementStrafeLeft
|
or InputAction.MovementStrafeLeft
|
||||||
or InputAction.MovementStrafeRight))
|
or InputAction.MovementStrafeRight))
|
||||||
{
|
{
|
||||||
AutoRunActive = false;
|
_movement.CancelAutoRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSession() => AutoRunActive = false;
|
public void ResetSession() => _movement.ResetInputIntent();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,89 +16,6 @@ internal sealed class LocalPlayerIdentityState : ILocalPlayerIdentitySource
|
||||||
public uint ServerGuid { get; set; }
|
public uint ServerGuid { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface ILocalPlayerControllerSource
|
|
||||||
{
|
|
||||||
PlayerMovementController? Controller { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface ILocalPlayerMotionSource
|
|
||||||
{
|
|
||||||
MotionInterpreter? Motion { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The one mutable local movement-controller slot. Player-mode lifecycle owns
|
|
||||||
/// assignment; update and presentation owners receive the read-only seam.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class LocalPlayerControllerSlot
|
|
||||||
: ILocalPlayerControllerSource,
|
|
||||||
ILocalPlayerMotionSource
|
|
||||||
{
|
|
||||||
private PlayerMovementController? _preparingMotionOwner;
|
|
||||||
|
|
||||||
public PlayerMovementController? Controller { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The motion owner visible to the local PartArray completion relay.
|
|
||||||
/// During player-mode construction this is the fully animation-bound
|
|
||||||
/// candidate controller; all other consumers continue to see only the
|
|
||||||
/// committed <see cref="Controller"/>.
|
|
||||||
/// </summary>
|
|
||||||
MotionInterpreter? ILocalPlayerMotionSource.Motion =>
|
|
||||||
_preparingMotionOwner?.Motion ?? Controller?.Motion;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the narrow construction-time ownership seam required by retail's
|
|
||||||
/// CPhysicsObj/PartArray lifetime. Initial placement synchronously queues
|
|
||||||
/// and completes StopCompletely, so its MotionDone relay must reach the
|
|
||||||
/// candidate interpreter before the complete controller is published.
|
|
||||||
/// </summary>
|
|
||||||
public IDisposable BeginMotionPreparation(
|
|
||||||
PlayerMovementController controller,
|
|
||||||
Action? drainPriorAnimationQueue = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(controller);
|
|
||||||
if (_preparingMotionOwner is not null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"A local player motion owner is already being prepared.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The live PartArray exists before the local player controller. Drain
|
|
||||||
// any animation completions produced under that prior ownership while
|
|
||||||
// they still resolve to the prior (or null) interpreter. Publishing
|
|
||||||
// the fresh interpreter first would let those old callbacks pop its
|
|
||||||
// new pending_motions queue and leave an unmatched Ready sentinel.
|
|
||||||
drainPriorAnimationQueue?.Invoke();
|
|
||||||
_preparingMotionOwner = controller;
|
|
||||||
return new MotionPreparation(this, controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndMotionPreparation(PlayerMovementController controller)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_preparingMotionOwner, controller))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"The local player motion preparation owner changed unexpectedly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_preparingMotionOwner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MotionPreparation(
|
|
||||||
LocalPlayerControllerSlot owner,
|
|
||||||
PlayerMovementController controller) : IDisposable
|
|
||||||
{
|
|
||||||
private LocalPlayerControllerSlot? _owner = owner;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
LocalPlayerControllerSlot? current = Interlocked.Exchange(ref _owner, null);
|
|
||||||
current?.EndMotionPreparation(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface ILocalPlayerPhysicsHostSource
|
internal interface ILocalPlayerPhysicsHostSource
|
||||||
{
|
{
|
||||||
EntityPhysicsHost? Host { get; }
|
EntityPhysicsHost? Host { get; }
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,6 @@ using AcDream.Core.Net;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
internal interface IMovementTruthDiagnosticSink
|
|
||||||
{
|
|
||||||
void OnOutbound(
|
|
||||||
string kind,
|
|
||||||
uint sequence,
|
|
||||||
MovementResult result,
|
|
||||||
Vector3 wirePosition,
|
|
||||||
uint wireCellId,
|
|
||||||
byte contactByte);
|
|
||||||
|
|
||||||
void OnServerEcho(
|
|
||||||
WorldSession.EntityPositionUpdate update,
|
|
||||||
Vector3 serverWorldPosition);
|
|
||||||
|
|
||||||
void ResetSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class MovementTruthDiagnosticController
|
internal sealed class MovementTruthDiagnosticController
|
||||||
: IMovementTruthDiagnosticSink
|
: IMovementTruthDiagnosticSink
|
||||||
{
|
{
|
||||||
|
|
@ -129,33 +112,3 @@ internal sealed class MovementTruthDiagnosticController
|
||||||
? FormattableString.Invariant($"0x{command.Value:X8}")
|
? FormattableString.Invariant($"0x{command.Value:X8}")
|
||||||
: "-";
|
: "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class DelegateMovementTruthDiagnosticSink
|
|
||||||
: IMovementTruthDiagnosticSink
|
|
||||||
{
|
|
||||||
private readonly Action<string, uint, MovementResult, Vector3, uint, byte>
|
|
||||||
_outbound;
|
|
||||||
|
|
||||||
public DelegateMovementTruthDiagnosticSink(
|
|
||||||
Action<string, uint, MovementResult, Vector3, uint, byte> outbound) =>
|
|
||||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
|
||||||
|
|
||||||
public void OnOutbound(
|
|
||||||
string kind,
|
|
||||||
uint sequence,
|
|
||||||
MovementResult result,
|
|
||||||
Vector3 wirePosition,
|
|
||||||
uint wireCellId,
|
|
||||||
byte contactByte) =>
|
|
||||||
_outbound(kind, sequence, result, wirePosition, wireCellId, contactByte);
|
|
||||||
|
|
||||||
public void OnServerEcho(
|
|
||||||
WorldSession.EntityPositionUpdate update,
|
|
||||||
Vector3 serverWorldPosition)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetSession()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,8 @@ internal sealed class PlayerModeController :
|
||||||
{
|
{
|
||||||
var controller = new PlayerMovementController(
|
var controller = new PlayerMovementController(
|
||||||
_physics,
|
_physics,
|
||||||
playerRecord.ObjectClock);
|
playerRecord.ObjectClock,
|
||||||
|
PlayerMovementConstructionOptions.From(_skills.Snapshot));
|
||||||
controller.ApplyPhysicsState(playerRecord.FinalPhysicsState);
|
controller.ApplyPhysicsState(playerRecord.FinalPhysicsState);
|
||||||
|
|
||||||
// Retail MovementManager::MakeMoveToManager @ 0x00524000 creates one
|
// Retail MovementManager::MakeMoveToManager @ 0x00524000 creates one
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ public sealed class GameWindow :
|
||||||
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
||||||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||||
_movementTruthDiagnostics;
|
_movementTruthDiagnostics;
|
||||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||||
// Step 7 projectile presentation. The controller owns no identity map;
|
// Step 7 projectile presentation. The controller owns no identity map;
|
||||||
|
|
@ -431,8 +431,8 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo;
|
private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo;
|
||||||
private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer;
|
private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer;
|
||||||
// Phase B.2: player movement mode.
|
// Phase B.2: player movement mode.
|
||||||
private readonly AcDream.App.Input.LocalPlayerControllerSlot _playerControllerSlot = new();
|
private readonly RuntimeLocalPlayerMovementState _playerControllerSlot = new();
|
||||||
private AcDream.App.Input.PlayerMovementController? _playerController
|
private PlayerMovementController? _playerController
|
||||||
=> _playerControllerSlot.Controller;
|
=> _playerControllerSlot.Controller;
|
||||||
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new();
|
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new();
|
||||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera
|
private AcDream.App.Rendering.ChaseCamera? _chaseCamera
|
||||||
|
|
@ -586,6 +586,7 @@ public sealed class GameWindow :
|
||||||
new AcDream.App.Input.DevToolsInputCaptureSource(options.DevTools),
|
new AcDream.App.Input.DevToolsInputCaptureSource(options.DevTools),
|
||||||
_retainedInputCapture);
|
_retainedInputCapture);
|
||||||
_movementInput = new AcDream.App.Input.DispatcherMovementInputSource(
|
_movementInput = new AcDream.App.Input.DispatcherMovementInputSource(
|
||||||
|
_playerControllerSlot,
|
||||||
_inputCapture);
|
_inputCapture);
|
||||||
_framebufferResize = new FramebufferResizeController(_viewportAspect);
|
_framebufferResize = new FramebufferResizeController(_viewportAspect);
|
||||||
_datDir = options.DatDir;
|
_datDir = options.DatDir;
|
||||||
|
|
@ -620,7 +621,7 @@ public sealed class GameWindow :
|
||||||
options.DumpMoveTruth,
|
options.DumpMoveTruth,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
_localPlayerIdentity);
|
_localPlayerIdentity);
|
||||||
_localPlayerOutbound = new AcDream.App.Input.LocalPlayerOutboundController(
|
_localPlayerOutbound = new LocalPlayerOutboundController(
|
||||||
_movementTruthDiagnostics);
|
_movementTruthDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1587,6 +1588,7 @@ public sealed class GameWindow :
|
||||||
_runtimeCharacter,
|
_runtimeCharacter,
|
||||||
_runtimeCommunication,
|
_runtimeCommunication,
|
||||||
_runtimeActions,
|
_runtimeActions,
|
||||||
|
_playerControllerSlot,
|
||||||
_renderSceneShadow,
|
_renderSceneShadow,
|
||||||
_livePresentationBindings,
|
_livePresentationBindings,
|
||||||
_entityEffectAdvance,
|
_entityEffectAdvance,
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ internal sealed record LiveShutdownRoots(
|
||||||
RuntimeCharacterState Character,
|
RuntimeCharacterState Character,
|
||||||
RuntimeCommunicationState Communication,
|
RuntimeCommunicationState Communication,
|
||||||
RuntimeActionState Actions,
|
RuntimeActionState Actions,
|
||||||
|
RuntimeLocalPlayerMovementState Movement,
|
||||||
RenderSceneShadowRuntime? RenderSceneShadow,
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LivePresentationRuntimeBindings? PresentationBindings,
|
LivePresentationRuntimeBindings? PresentationBindings,
|
||||||
DeferredEntityEffectAdvanceSource EffectAdvance,
|
DeferredEntityEffectAdvanceSource EffectAdvance,
|
||||||
|
|
@ -392,6 +393,9 @@ internal static class GameWindowShutdownManifest
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("runtime entity/object stream",
|
new ResourceShutdownStage("runtime entity/object stream",
|
||||||
[
|
[
|
||||||
|
Hard(
|
||||||
|
"runtime local movement state",
|
||||||
|
live.Movement.Dispose),
|
||||||
Hard(
|
Hard(
|
||||||
"runtime action state",
|
"runtime action state",
|
||||||
live.Actions.Dispose),
|
live.Actions.Dispose),
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,10 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
RuntimeCharacterState character,
|
RuntimeCharacterState character,
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
RuntimeActionState actions,
|
RuntimeActionState actions,
|
||||||
ILocalPlayerControllerSource playerController,
|
RuntimeLocalPlayerMovementState movement,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock,
|
IGameRuntimeClock clock,
|
||||||
SelectionInteractionController selection,
|
SelectionInteractionController selection)
|
||||||
GameplayInputFrameController gameplayInput)
|
|
||||||
{
|
{
|
||||||
_view = new CurrentGameRuntimeViewAdapter(
|
_view = new CurrentGameRuntimeViewAdapter(
|
||||||
session,
|
session,
|
||||||
|
|
@ -53,7 +52,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
character,
|
character,
|
||||||
communication,
|
communication,
|
||||||
actions,
|
actions,
|
||||||
playerController,
|
movement,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
clock);
|
clock);
|
||||||
entityObjects.BindEventContext(
|
entityObjects.BindEventContext(
|
||||||
|
|
@ -71,8 +70,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
inventory,
|
inventory,
|
||||||
character,
|
character,
|
||||||
actions,
|
actions,
|
||||||
|
movement,
|
||||||
selection,
|
selection,
|
||||||
gameplayInput,
|
|
||||||
_events);
|
_events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
private readonly RuntimeInventoryState _inventory;
|
private readonly RuntimeInventoryState _inventory;
|
||||||
private readonly RuntimeCharacterState _character;
|
private readonly RuntimeCharacterState _character;
|
||||||
private readonly RuntimeActionState _actions;
|
private readonly RuntimeActionState _actions;
|
||||||
|
private readonly RuntimeLocalPlayerMovementState _movement;
|
||||||
private readonly SelectionInteractionController _selection;
|
private readonly SelectionInteractionController _selection;
|
||||||
private readonly GameplayInputFrameController _gameplayInput;
|
|
||||||
private readonly ICurrentGameRuntimeEventSink _events;
|
private readonly ICurrentGameRuntimeEventSink _events;
|
||||||
|
|
||||||
public CurrentGameRuntimeCommandAdapter(
|
public CurrentGameRuntimeCommandAdapter(
|
||||||
|
|
@ -46,8 +46,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeInventoryState inventory,
|
RuntimeInventoryState inventory,
|
||||||
RuntimeCharacterState character,
|
RuntimeCharacterState character,
|
||||||
RuntimeActionState actions,
|
RuntimeActionState actions,
|
||||||
|
RuntimeLocalPlayerMovementState movement,
|
||||||
SelectionInteractionController selection,
|
SelectionInteractionController selection,
|
||||||
GameplayInputFrameController gameplayInput,
|
|
||||||
ICurrentGameRuntimeEventSink events)
|
ICurrentGameRuntimeEventSink events)
|
||||||
{
|
{
|
||||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
|
@ -58,9 +58,9 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
_character = character ?? throw new ArgumentNullException(nameof(character));
|
_character = character ?? throw new ArgumentNullException(nameof(character));
|
||||||
_actions = actions
|
_actions = actions
|
||||||
?? throw new ArgumentNullException(nameof(actions));
|
?? throw new ArgumentNullException(nameof(actions));
|
||||||
|
_movement = movement
|
||||||
|
?? throw new ArgumentNullException(nameof(movement));
|
||||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||||
_gameplayInput = gameplayInput
|
|
||||||
?? throw new ArgumentNullException(nameof(gameplayInput));
|
|
||||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,22 +246,9 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
|
|
||||||
InputAction action = command switch
|
RuntimeCommandStatus status = _movement.Execute(command)
|
||||||
{
|
? RuntimeCommandStatus.Accepted
|
||||||
RuntimeMovementCommand.ToggleRunLock => InputAction.MovementRunLock,
|
: RuntimeCommandStatus.Unsupported;
|
||||||
RuntimeMovementCommand.Stop => InputAction.MovementStop,
|
|
||||||
_ => InputAction.None,
|
|
||||||
};
|
|
||||||
RuntimeCommandStatus status;
|
|
||||||
if (action == InputAction.None)
|
|
||||||
{
|
|
||||||
status = RuntimeCommandStatus.Unsupported;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_gameplayInput.HandlePressedMovementAction(action);
|
|
||||||
status = RuntimeCommandStatus.Accepted;
|
|
||||||
}
|
|
||||||
|
|
||||||
_events.EmitCommand(RuntimeCommandDomain.Movement, (int)command, status);
|
_events.EmitCommand(RuntimeCommandDomain.Movement, (int)command, status);
|
||||||
return Result(status);
|
return Result(status);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
private readonly IRuntimeSocialView _socialView;
|
private readonly IRuntimeSocialView _socialView;
|
||||||
private readonly IRuntimeChatView _chatView;
|
private readonly IRuntimeChatView _chatView;
|
||||||
private readonly IRuntimeActionView _actionView;
|
private readonly IRuntimeActionView _actionView;
|
||||||
private readonly MovementView _movementView;
|
private readonly IRuntimeMovementView _movementView;
|
||||||
private readonly PortalView _portalView;
|
private readonly PortalView _portalView;
|
||||||
private bool _active = true;
|
private bool _active = true;
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
RuntimeCharacterState character,
|
RuntimeCharacterState character,
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
RuntimeActionState actions,
|
RuntimeActionState actions,
|
||||||
ILocalPlayerControllerSource playerController,
|
RuntimeLocalPlayerMovementState movement,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock)
|
IGameRuntimeClock clock)
|
||||||
{
|
{
|
||||||
|
|
@ -65,10 +65,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_socialView = communication.SocialView;
|
_socialView = communication.SocialView;
|
||||||
_actionView = (
|
_actionView = (
|
||||||
actions ?? throw new ArgumentNullException(nameof(actions))).View;
|
actions ?? throw new ArgumentNullException(nameof(actions))).View;
|
||||||
_movementView = new MovementView(
|
_movementView = (
|
||||||
playerController
|
movement ?? throw new ArgumentNullException(nameof(movement))).View;
|
||||||
?? throw new ArgumentNullException(nameof(playerController)),
|
|
||||||
_clock);
|
|
||||||
_portalView = new PortalView(
|
_portalView = new PortalView(
|
||||||
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)));
|
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)));
|
||||||
}
|
}
|
||||||
|
|
@ -132,35 +130,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
|
|
||||||
internal void Deactivate() => _active = false;
|
internal void Deactivate() => _active = false;
|
||||||
|
|
||||||
private sealed class MovementView(
|
|
||||||
ILocalPlayerControllerSource owner,
|
|
||||||
IGameRuntimeClock clock)
|
|
||||||
: IRuntimeMovementView
|
|
||||||
{
|
|
||||||
public RuntimeMovementSnapshot Snapshot
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
PlayerMovementController? controller = owner.Controller;
|
|
||||||
return controller is null
|
|
||||||
? new RuntimeMovementSnapshot(
|
|
||||||
false,
|
|
||||||
0u,
|
|
||||||
default,
|
|
||||||
default,
|
|
||||||
false,
|
|
||||||
clock.SimulationTimeSeconds)
|
|
||||||
: new RuntimeMovementSnapshot(
|
|
||||||
true,
|
|
||||||
controller.LocalEntityId,
|
|
||||||
controller.CellPosition,
|
|
||||||
controller.BodyVelocity,
|
|
||||||
controller.IsAirborne,
|
|
||||||
clock.SimulationTimeSeconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PortalView(WorldRevealCoordinator owner)
|
private sealed class PortalView(WorldRevealCoordinator owner)
|
||||||
: IRuntimePortalView
|
: IRuntimePortalView
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ namespace AcDream.Core.Physics;
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// L.3a (2026-04-30): added optional collision-normal fields so the
|
/// L.3a (2026-04-30): added optional collision-normal fields so the
|
||||||
/// caller (typically <see cref="AcDream.App.Input.PlayerMovementController"/>)
|
/// caller (typically
|
||||||
|
/// <see cref="AcDream.Runtime.Gameplay.PlayerMovementController"/>)
|
||||||
/// can apply retail's velocity-reflection bounce
|
/// can apply retail's velocity-reflection bounce
|
||||||
/// (<c>v_new = v - (1 + elasticity) * dot(v, n) * n</c>) to the
|
/// (<c>v_new = v - (1 + elasticity) * dot(v, n) * n</c>) to the
|
||||||
/// PhysicsBody after the geometric resolve completes. ACE port mirror:
|
/// PhysicsBody after the geometric resolve completes. ACE port mirror:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
||||||
|
<InternalsVisibleTo Include="AcDream.App" />
|
||||||
|
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||||
|
<InternalsVisibleTo Include="AcDream.Core.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,9 @@ public readonly record struct RuntimeMovementSnapshot(
|
||||||
Position Position,
|
Position Position,
|
||||||
System.Numerics.Vector3 Velocity,
|
System.Numerics.Vector3 Velocity,
|
||||||
bool IsAirborne,
|
bool IsAirborne,
|
||||||
double SimulationTimeSeconds);
|
double SimulationTimeSeconds,
|
||||||
|
long Revision = 0,
|
||||||
|
bool AutoRunActive = false);
|
||||||
|
|
||||||
public interface IRuntimeMovementView
|
public interface IRuntimeMovementView
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,24 @@ using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
public interface IMovementTruthDiagnosticSink
|
||||||
|
{
|
||||||
|
void OnOutbound(
|
||||||
|
string kind,
|
||||||
|
uint sequence,
|
||||||
|
MovementResult result,
|
||||||
|
Vector3 wirePosition,
|
||||||
|
uint wireCellId,
|
||||||
|
byte contactByte);
|
||||||
|
|
||||||
|
void OnServerEcho(
|
||||||
|
WorldSession.EntityPositionUpdate update,
|
||||||
|
Vector3 serverWorldPosition);
|
||||||
|
|
||||||
|
void ResetSession();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializes input-originated output from the object-phase result and the
|
/// Serializes input-originated output from the object-phase result and the
|
||||||
|
|
@ -250,3 +267,33 @@ public sealed class LocalPlayerOutboundController
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class DelegateMovementTruthDiagnosticSink
|
||||||
|
: IMovementTruthDiagnosticSink
|
||||||
|
{
|
||||||
|
private readonly Action<string, uint, MovementResult, Vector3, uint, byte>
|
||||||
|
_outbound;
|
||||||
|
|
||||||
|
public DelegateMovementTruthDiagnosticSink(
|
||||||
|
Action<string, uint, MovementResult, Vector3, uint, byte> outbound) =>
|
||||||
|
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||||
|
|
||||||
|
public void OnOutbound(
|
||||||
|
string kind,
|
||||||
|
uint sequence,
|
||||||
|
MovementResult result,
|
||||||
|
Vector3 wirePosition,
|
||||||
|
uint wireCellId,
|
||||||
|
byte contactByte) =>
|
||||||
|
_outbound(kind, sequence, result, wirePosition, wireCellId, contactByte);
|
||||||
|
|
||||||
|
public void OnServerEcho(
|
||||||
|
WorldSession.EntityPositionUpdate update,
|
||||||
|
Vector3 serverWorldPosition)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input state for a single frame of player movement.
|
/// Input state for a single frame of player movement.
|
||||||
|
|
@ -18,6 +18,29 @@ public readonly record struct MovementInput(
|
||||||
float MouseDeltaX = 0f,
|
float MouseDeltaX = 0f,
|
||||||
bool Jump = false);
|
bool Jump = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed construction policy for the local movement owner. Server-authoritative
|
||||||
|
/// values come from <see cref="RuntimeMovementSkillState"/>; the fallback only
|
||||||
|
/// preserves the pre-description test/login baseline and never reads process
|
||||||
|
/// environment state.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct PlayerMovementConstructionOptions(
|
||||||
|
int RunSkill,
|
||||||
|
int JumpSkill)
|
||||||
|
{
|
||||||
|
public const int FallbackRunSkill = 200;
|
||||||
|
public const int FallbackJumpSkill = 300;
|
||||||
|
|
||||||
|
public static PlayerMovementConstructionOptions Fallback =>
|
||||||
|
new(FallbackRunSkill, FallbackJumpSkill);
|
||||||
|
|
||||||
|
public static PlayerMovementConstructionOptions From(
|
||||||
|
RuntimeMovementSkillSnapshot skills) =>
|
||||||
|
new(
|
||||||
|
skills.RunSkill >= 0 ? skills.RunSkill : FallbackRunSkill,
|
||||||
|
skills.JumpSkill >= 0 ? skills.JumpSkill : FallbackJumpSkill);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-only presentation snapshot of retail's pending jump build. The movement
|
/// Read-only presentation snapshot of retail's pending jump build. The movement
|
||||||
/// controller remains the sole owner of charge timing; retained UI only projects
|
/// controller remains the sole owner of charge timing; retained UI only projects
|
||||||
|
|
@ -456,6 +479,14 @@ public sealed class PlayerMovementController
|
||||||
public PlayerMovementController(
|
public PlayerMovementController(
|
||||||
PhysicsEngine physics,
|
PhysicsEngine physics,
|
||||||
RetailObjectQuantumClock? objectClock = null)
|
RetailObjectQuantumClock? objectClock = null)
|
||||||
|
: this(physics, objectClock, PlayerMovementConstructionOptions.Fallback)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerMovementController(
|
||||||
|
PhysicsEngine physics,
|
||||||
|
RetailObjectQuantumClock? objectClock,
|
||||||
|
PlayerMovementConstructionOptions options)
|
||||||
{
|
{
|
||||||
_physics = physics;
|
_physics = physics;
|
||||||
_objectClock = objectClock ?? new RetailObjectQuantumClock();
|
_objectClock = objectClock ?? new RetailObjectQuantumClock();
|
||||||
|
|
@ -467,20 +498,20 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
// Default skills — tuned toward mid-retail feel. Real characters'
|
// Default skills — tuned toward mid-retail feel. Real characters'
|
||||||
// skills come from PlayerDescription (0xF7B0/0x0013) — GameWindow
|
// skills come from PlayerDescription (0xF7B0/0x0013) — GameWindow
|
||||||
// pushes them via SetCharacterSkills once the controller exists
|
// Runtime supplies them through typed construction options and
|
||||||
// (K-fix7; PD arrives at login before auto-entry). These env-var
|
// updates them via SetCharacterSkills when later authoritative
|
||||||
// defaults only cover tests / pre-PD frames:
|
// values arrive.
|
||||||
// ACDREAM_RUN_SKILL, ACDREAM_JUMP_SKILL
|
|
||||||
// K-fix6 (2026-04-26): bumped default jump skill from 200 → 300.
|
// K-fix6 (2026-04-26): bumped default jump skill from 200 → 300.
|
||||||
// Retail formula: height = (skill/(skill+1300))*22.2 + 0.05 (extent=1):
|
// Retail formula: height = (skill/(skill+1300))*22.2 + 0.05 (extent=1):
|
||||||
// skill=200 → 3.01m max (felt too low — user complaint)
|
// skill=200 → 3.01m max (felt too low — user complaint)
|
||||||
// skill=300 → 4.21m max (closer to a typical retail mid-tier
|
// skill=300 → 4.21m max (closer to a typical retail mid-tier
|
||||||
// character's "I can clear that fence" hop)
|
// character's "I can clear that fence" hop)
|
||||||
// Until #7 ships and PlayerDescription gives us the server's real
|
// Until PlayerDescription supplies both values, retain the typed
|
||||||
// skill, this default is the right "feels like retail" baseline.
|
// construction baseline. RuntimeMovementSkillState updates this same
|
||||||
int runSkill = int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_RUN_SKILL"), out var rs) ? rs : 200;
|
// PlayerWeenie after authoritative character data arrives.
|
||||||
int jumpSkill = int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_JUMP_SKILL"), out var jsv) ? jsv : 300;
|
_weenie = new PlayerWeenie(
|
||||||
_weenie = new PlayerWeenie(runSkill: runSkill, jumpSkill: jumpSkill);
|
runSkill: options.RunSkill,
|
||||||
|
jumpSkill: options.JumpSkill);
|
||||||
_motion = new MotionInterpreter(_body, _weenie);
|
_motion = new MotionInterpreter(_body, _weenie);
|
||||||
// R5-V5: the MovementManager facade owns the interp from birth
|
// R5-V5: the MovementManager facade owns the interp from birth
|
||||||
// (retail CPhysicsObj::movement_manager); the moveto child binds
|
// (retail CPhysicsObj::movement_manager); the moveto child binds
|
||||||
|
|
@ -2,20 +2,22 @@ namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One allocation-free ledger over the gameplay-state lifetime group through
|
/// One allocation-free ledger over the gameplay-state lifetime group through
|
||||||
/// J5.1. It contains no state of its own; graphical and no-window hosts capture
|
/// J5.4. It contains no state of its own; graphical and no-window hosts capture
|
||||||
/// the same four canonical owners.
|
/// the same canonical owners.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
||||||
RuntimeInventoryOwnershipSnapshot Inventory,
|
RuntimeInventoryOwnershipSnapshot Inventory,
|
||||||
RuntimeCharacterOwnershipSnapshot Character,
|
RuntimeCharacterOwnershipSnapshot Character,
|
||||||
RuntimeCommunicationOwnershipSnapshot Communication,
|
RuntimeCommunicationOwnershipSnapshot Communication,
|
||||||
RuntimeActionOwnershipSnapshot Actions)
|
RuntimeActionOwnershipSnapshot Actions,
|
||||||
|
RuntimeLocalMovementOwnershipSnapshot Movement)
|
||||||
{
|
{
|
||||||
public bool IsConverged =>
|
public bool IsConverged =>
|
||||||
Inventory.IsConverged
|
Inventory.IsConverged
|
||||||
&& Character.IsConverged
|
&& Character.IsConverged
|
||||||
&& Communication.IsConverged
|
&& Communication.IsConverged
|
||||||
&& Actions.IsConverged;
|
&& Actions.IsConverged
|
||||||
|
&& Movement.IsConverged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class RuntimeGameplayOwnership
|
public static class RuntimeGameplayOwnership
|
||||||
|
|
@ -24,16 +26,19 @@ public static class RuntimeGameplayOwnership
|
||||||
RuntimeInventoryState inventory,
|
RuntimeInventoryState inventory,
|
||||||
RuntimeCharacterState character,
|
RuntimeCharacterState character,
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
RuntimeActionState actions)
|
RuntimeActionState actions,
|
||||||
|
RuntimeLocalPlayerMovementState movement)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(inventory);
|
ArgumentNullException.ThrowIfNull(inventory);
|
||||||
ArgumentNullException.ThrowIfNull(character);
|
ArgumentNullException.ThrowIfNull(character);
|
||||||
ArgumentNullException.ThrowIfNull(communication);
|
ArgumentNullException.ThrowIfNull(communication);
|
||||||
ArgumentNullException.ThrowIfNull(actions);
|
ArgumentNullException.ThrowIfNull(actions);
|
||||||
|
ArgumentNullException.ThrowIfNull(movement);
|
||||||
return new RuntimeGameplayOwnershipSnapshot(
|
return new RuntimeGameplayOwnershipSnapshot(
|
||||||
inventory.CaptureOwnership(),
|
inventory.CaptureOwnership(),
|
||||||
character.CaptureOwnership(),
|
character.CaptureOwnership(),
|
||||||
communication.CaptureOwnership(),
|
communication.CaptureOwnership(),
|
||||||
actions.CaptureOwnership());
|
actions.CaptureOwnership(),
|
||||||
|
movement.CaptureOwnership());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
200
src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs
Normal file
200
src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
public interface IRuntimeLocalPlayerControllerSource
|
||||||
|
{
|
||||||
|
PlayerMovementController? Controller { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRuntimeLocalPlayerMotionSource
|
||||||
|
{
|
||||||
|
MotionInterpreter? Motion { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical local movement lifetime and intent owner. Graphical input,
|
||||||
|
/// presentation, diagnostics, and future no-window hosts borrow this exact
|
||||||
|
/// state; they never mirror the controller or the autorun latch.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeLocalPlayerMovementState
|
||||||
|
: IRuntimeLocalPlayerControllerSource,
|
||||||
|
IRuntimeLocalPlayerMotionSource,
|
||||||
|
IRuntimeMovementView,
|
||||||
|
IDisposable
|
||||||
|
{
|
||||||
|
private PlayerMovementController? _controller;
|
||||||
|
private PlayerMovementController? _preparingMotionOwner;
|
||||||
|
private bool _autoRunActive;
|
||||||
|
private bool _disposed;
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public PlayerMovementController? Controller
|
||||||
|
{
|
||||||
|
get => _controller;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if (ReferenceEquals(_controller, value))
|
||||||
|
return;
|
||||||
|
_controller = value;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutoRunActive => _autoRunActive;
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
public IRuntimeMovementView View => this;
|
||||||
|
|
||||||
|
MotionInterpreter? IRuntimeLocalPlayerMotionSource.Motion =>
|
||||||
|
_preparingMotionOwner?.Motion ?? _controller?.Motion;
|
||||||
|
|
||||||
|
public RuntimeMovementSnapshot Snapshot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
PlayerMovementController? controller = _controller;
|
||||||
|
return controller is null
|
||||||
|
? new RuntimeMovementSnapshot(
|
||||||
|
false,
|
||||||
|
0u,
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
false,
|
||||||
|
0d,
|
||||||
|
Revision,
|
||||||
|
_autoRunActive)
|
||||||
|
: new RuntimeMovementSnapshot(
|
||||||
|
true,
|
||||||
|
controller.LocalEntityId,
|
||||||
|
controller.CellPosition,
|
||||||
|
controller.BodyVelocity,
|
||||||
|
controller.IsAirborne,
|
||||||
|
controller.SimTimeSeconds,
|
||||||
|
Revision,
|
||||||
|
_autoRunActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the retail construction seam where a PartArray completion can
|
||||||
|
/// reach the candidate MotionInterpreter before the controller is
|
||||||
|
/// published to ordinary borrowers.
|
||||||
|
/// </summary>
|
||||||
|
public IDisposable BeginMotionPreparation(
|
||||||
|
PlayerMovementController controller,
|
||||||
|
Action? drainPriorAnimationQueue = null)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(controller);
|
||||||
|
if (_preparingMotionOwner is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A local player motion owner is already being prepared.");
|
||||||
|
}
|
||||||
|
|
||||||
|
drainPriorAnimationQueue?.Invoke();
|
||||||
|
_preparingMotionOwner = controller;
|
||||||
|
return new MotionPreparation(this, controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Execute(RuntimeMovementCommand command)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case RuntimeMovementCommand.ToggleRunLock:
|
||||||
|
_autoRunActive = !_autoRunActive;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
return true;
|
||||||
|
case RuntimeMovementCommand.Stop:
|
||||||
|
CancelAutoRun();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CancelAutoRun()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if (!_autoRunActive)
|
||||||
|
return false;
|
||||||
|
_autoRunActive = false;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetInputIntent()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if (!_autoRunActive)
|
||||||
|
return;
|
||||||
|
_autoRunActive = false;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeLocalMovementOwnershipSnapshot CaptureOwnership() =>
|
||||||
|
new(
|
||||||
|
_disposed,
|
||||||
|
_controller is not null,
|
||||||
|
_preparingMotionOwner is not null,
|
||||||
|
_autoRunActive,
|
||||||
|
Revision);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_autoRunActive = false;
|
||||||
|
_controller = null;
|
||||||
|
_preparingMotionOwner = null;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndMotionPreparation(PlayerMovementController controller)
|
||||||
|
{
|
||||||
|
// Terminal disposal clears the unpublished construction seam. A
|
||||||
|
// caller may still unwind the already-issued lease afterward; that
|
||||||
|
// unwind is idempotent rather than resurrecting or faulting the
|
||||||
|
// retired runtime owner.
|
||||||
|
if (_disposed && _preparingMotionOwner is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!ReferenceEquals(_preparingMotionOwner, controller))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The local player motion preparation owner changed unexpectedly.");
|
||||||
|
}
|
||||||
|
_preparingMotionOwner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MotionPreparation(
|
||||||
|
RuntimeLocalPlayerMovementState owner,
|
||||||
|
PlayerMovementController controller) : IDisposable
|
||||||
|
{
|
||||||
|
private RuntimeLocalPlayerMovementState? _owner = owner;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
RuntimeLocalPlayerMovementState? current =
|
||||||
|
Interlocked.Exchange(ref _owner, null);
|
||||||
|
current?.EndMotionPreparation(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeLocalMovementOwnershipSnapshot(
|
||||||
|
bool IsDisposed,
|
||||||
|
bool HasController,
|
||||||
|
bool HasPreparingMotionOwner,
|
||||||
|
bool AutoRunActive,
|
||||||
|
long Revision)
|
||||||
|
{
|
||||||
|
public bool IsConverged =>
|
||||||
|
IsDisposed
|
||||||
|
&& !HasController
|
||||||
|
&& !HasPreparingMotionOwner
|
||||||
|
&& !AutoRunActive;
|
||||||
|
}
|
||||||
|
|
@ -96,7 +96,8 @@ public sealed class HostInputCameraCompositionTests
|
||||||
ViewportAspect = new ViewportAspectState();
|
ViewportAspect = new ViewportAspectState();
|
||||||
Framebuffer = new FramebufferResizeController(ViewportAspect);
|
Framebuffer = new FramebufferResizeController(ViewportAspect);
|
||||||
Capture = new CaptureSource();
|
Capture = new CaptureSource();
|
||||||
Movement = new DispatcherMovementInputSource(Capture);
|
MovementState = new RuntimeLocalPlayerMovementState();
|
||||||
|
Movement = new DispatcherMovementInputSource(MovementState, Capture);
|
||||||
CameraInput = new DispatcherCameraInputSource();
|
CameraInput = new DispatcherCameraInputSource();
|
||||||
PlayerMode = new LocalPlayerModeState();
|
PlayerMode = new LocalPlayerModeState();
|
||||||
Chase = new ChaseCameraInputState();
|
Chase = new ChaseCameraInputState();
|
||||||
|
|
@ -111,6 +112,7 @@ public sealed class HostInputCameraCompositionTests
|
||||||
public ViewportAspectState ViewportAspect { get; }
|
public ViewportAspectState ViewportAspect { get; }
|
||||||
public FramebufferResizeController Framebuffer { get; }
|
public FramebufferResizeController Framebuffer { get; }
|
||||||
public CaptureSource Capture { get; }
|
public CaptureSource Capture { get; }
|
||||||
|
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||||
public DispatcherMovementInputSource Movement { get; }
|
public DispatcherMovementInputSource Movement { get; }
|
||||||
public DispatcherCameraInputSource CameraInput { get; }
|
public DispatcherCameraInputSource CameraInput { get; }
|
||||||
public LocalPlayerModeState PlayerMode { get; }
|
public LocalPlayerModeState PlayerMode { get; }
|
||||||
|
|
@ -141,7 +143,11 @@ public sealed class HostInputCameraCompositionTests
|
||||||
throw new InvalidOperationException($"fault at {point}");
|
throw new InvalidOperationException($"fault at {point}");
|
||||||
});
|
});
|
||||||
|
|
||||||
public void Dispose() => Publication.Dispose();
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Publication.Dispose();
|
||||||
|
MovementState.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Publication :
|
private sealed class Publication :
|
||||||
|
|
|
||||||
7
tests/AcDream.App.Tests/GlobalUsings.cs
Normal file
7
tests/AcDream.App.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
global using AcDream.Runtime.Gameplay;
|
||||||
|
global using LocalPlayerControllerSlot =
|
||||||
|
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
|
||||||
|
global using ILocalPlayerControllerSource =
|
||||||
|
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource;
|
||||||
|
global using ILocalPlayerMotionSource =
|
||||||
|
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;
|
||||||
|
|
@ -76,7 +76,7 @@ public sealed class CameraPointerInputControllerTests
|
||||||
var mouseLook = new MouseLook(active: true);
|
var mouseLook = new MouseLook(active: true);
|
||||||
var frame = new GameplayInputFrameController(
|
var frame = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook,
|
mouseLook,
|
||||||
new Combat());
|
new Combat());
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ public sealed class CameraPointerInputControllerTests
|
||||||
var mouseLook = new MouseLook(active: true);
|
var mouseLook = new MouseLook(active: true);
|
||||||
var frame = new GameplayInputFrameController(
|
var frame = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook,
|
mouseLook,
|
||||||
new Combat());
|
new Combat());
|
||||||
fixture.Owner.BindGameplayFrame(frame);
|
fixture.Owner.BindGameplayFrame(frame);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnboundSourceCapturesNeutralInput()
|
public void UnboundSourceCapturesNeutralInput()
|
||||||
{
|
{
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
|
|
||||||
Assert.Equal(default, source.Capture());
|
Assert.Equal(default, source.Capture());
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
public void CapturesDispatcherHeldStateAndRetailWalkModifier()
|
public void CapturesDispatcherHeldStateAndRetailWalkModifier()
|
||||||
{
|
{
|
||||||
var (dispatcher, _, _) = CreateDispatcher();
|
var (dispatcher, _, _) = CreateDispatcher();
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
source.Bind(dispatcher);
|
source.Bind(dispatcher);
|
||||||
|
|
||||||
Assert.True(dispatcher.TrySetAutomationActionHeld(
|
Assert.True(dispatcher.TrySetAutomationActionHeld(
|
||||||
|
|
@ -38,7 +38,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
public void RetainedKeyboardCaptureSilencesHeldKeysButDoesNotCancelAutorun()
|
public void RetainedKeyboardCaptureSilencesHeldKeysButDoesNotCancelAutorun()
|
||||||
{
|
{
|
||||||
var (dispatcher, _, mouse) = CreateDispatcher();
|
var (dispatcher, _, mouse) = CreateDispatcher();
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
source.Bind(dispatcher);
|
source.Bind(dispatcher);
|
||||||
dispatcher.TrySetAutomationActionHeld(InputAction.MovementForward, held: true);
|
dispatcher.TrySetAutomationActionHeld(InputAction.MovementForward, held: true);
|
||||||
Assert.True(source.HandlePressedAction(InputAction.MovementRunLock));
|
Assert.True(source.HandlePressedAction(InputAction.MovementRunLock));
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
{
|
{
|
||||||
var capture = new FakeCapture();
|
var capture = new FakeCapture();
|
||||||
var (dispatcher, _, _) = CreateDispatcher();
|
var (dispatcher, _, _) = CreateDispatcher();
|
||||||
var source = new DispatcherMovementInputSource(capture);
|
var source = CreateSource(capture);
|
||||||
source.Bind(dispatcher);
|
source.Bind(dispatcher);
|
||||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
[InlineData(InputAction.MovementStrafeRight)]
|
[InlineData(InputAction.MovementStrafeRight)]
|
||||||
public void RetailCancelActionsClearAutorun(InputAction action)
|
public void RetailCancelActionsClearAutorun(InputAction action)
|
||||||
{
|
{
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||||
|
|
||||||
Assert.False(source.HandlePressedAction(action));
|
Assert.False(source.HandlePressedAction(action));
|
||||||
|
|
@ -89,7 +89,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ForwardDoesNotCancelAutorunAndResetDoes()
|
public void ForwardDoesNotCancelAutorunAndResetDoes()
|
||||||
{
|
{
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||||
|
|
||||||
Assert.False(source.HandlePressedAction(InputAction.MovementForward));
|
Assert.False(source.HandlePressedAction(InputAction.MovementForward));
|
||||||
|
|
@ -102,7 +102,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BindingIsIdempotentOnlyForTheSameDispatcher()
|
public void BindingIsIdempotentOnlyForTheSameDispatcher()
|
||||||
{
|
{
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
var (first, _, _) = CreateDispatcher();
|
var (first, _, _) = CreateDispatcher();
|
||||||
var (second, _, _) = CreateDispatcher();
|
var (second, _, _) = CreateDispatcher();
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnbindRequiresExactDispatcherAndRestoresNeutralCapture()
|
public void UnbindRequiresExactDispatcherAndRestoresNeutralCapture()
|
||||||
{
|
{
|
||||||
var source = new DispatcherMovementInputSource();
|
var source = CreateSource();
|
||||||
var (first, _, _) = CreateDispatcher();
|
var (first, _, _) = CreateDispatcher();
|
||||||
var (other, _, _) = CreateDispatcher();
|
var (other, _, _) = CreateDispatcher();
|
||||||
source.Bind(first);
|
source.Bind(first);
|
||||||
|
|
@ -142,6 +142,10 @@ public sealed class DispatcherMovementInputSourceTests
|
||||||
return (dispatcher, keyboard, mouse);
|
return (dispatcher, keyboard, mouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DispatcherMovementInputSource CreateSource(
|
||||||
|
IInputCaptureSource? capture = null) =>
|
||||||
|
new(new RuntimeLocalPlayerMovementState(), capture);
|
||||||
|
|
||||||
private sealed class FakeKeyboard : IKeyboardSource
|
private sealed class FakeKeyboard : IKeyboardSource
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0067
|
#pragma warning disable CS0067
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public sealed class GameplayInputFrameControllerTests
|
||||||
var combat = new FakeCombat(calls);
|
var combat = new FakeCombat(calls);
|
||||||
var controller = new GameplayInputFrameController(
|
var controller = new GameplayInputFrameController(
|
||||||
dispatcher,
|
dispatcher,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook,
|
mouseLook,
|
||||||
combat);
|
combat);
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ public sealed class GameplayInputFrameControllerTests
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var controller = new GameplayInputFrameController(
|
var controller = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook: null,
|
mouseLook: null,
|
||||||
new FakeCombat(calls));
|
new FakeCombat(calls));
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ public sealed class GameplayInputFrameControllerTests
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var controller = new GameplayInputFrameController(
|
var controller = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook: null,
|
mouseLook: null,
|
||||||
new FakeCombat(calls, consumes: true));
|
new FakeCombat(calls, consumes: true));
|
||||||
|
|
||||||
|
|
@ -76,7 +76,8 @@ public sealed class GameplayInputFrameControllerTests
|
||||||
public void ResetSessionReleasesMouseLookAndAutorun()
|
public void ResetSessionReleasesMouseLookAndAutorun()
|
||||||
{
|
{
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var movement = new DispatcherMovementInputSource();
|
var movement = new DispatcherMovementInputSource(
|
||||||
|
new RuntimeLocalPlayerMovementState());
|
||||||
movement.HandlePressedAction(InputAction.MovementRunLock);
|
movement.HandlePressedAction(InputAction.MovementRunLock);
|
||||||
var mouseLook = new FakeMouseLook(calls);
|
var mouseLook = new FakeMouseLook(calls);
|
||||||
var controller = new GameplayInputFrameController(
|
var controller = new GameplayInputFrameController(
|
||||||
|
|
@ -98,7 +99,7 @@ public sealed class GameplayInputFrameControllerTests
|
||||||
var mouseLook = new FakeMouseLook(calls, active: true, consumes: true);
|
var mouseLook = new FakeMouseLook(calls, active: true, consumes: true);
|
||||||
var controller = new GameplayInputFrameController(
|
var controller = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
new DispatcherMovementInputSource(),
|
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||||
mouseLook,
|
mouseLook,
|
||||||
new FakeCombat(calls));
|
new FakeCombat(calls));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -782,7 +782,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
CombatMode = new RecordingCombatModeOperations();
|
CombatMode = new RecordingCombatModeOperations();
|
||||||
_combatModeBinding =
|
_combatModeBinding =
|
||||||
CombatModeOperations.BindOwned(CombatMode);
|
CombatModeOperations.BindOwned(CombatMode);
|
||||||
MovementInput = new DispatcherMovementInputSource();
|
MovementState = new RuntimeLocalPlayerMovementState();
|
||||||
|
MovementInput = new DispatcherMovementInputSource(MovementState);
|
||||||
GameplayInput = new GameplayInputFrameController(
|
GameplayInput = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
MovementInput,
|
MovementInput,
|
||||||
|
|
@ -832,11 +833,10 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Character,
|
Character,
|
||||||
Communication,
|
Communication,
|
||||||
Actions,
|
Actions,
|
||||||
new LocalPlayerControllerSlot(),
|
MovementState,
|
||||||
WorldReveal,
|
WorldReveal,
|
||||||
Clock,
|
Clock,
|
||||||
selectionController,
|
selectionController);
|
||||||
GameplayInput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeOptions Options { get; }
|
public RuntimeOptions Options { get; }
|
||||||
|
|
@ -853,6 +853,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public RuntimeSpellCastOperationsSlot SpellCastOperations { get; }
|
public RuntimeSpellCastOperationsSlot SpellCastOperations { get; }
|
||||||
public RuntimeActionState Actions { get; }
|
public RuntimeActionState Actions { get; }
|
||||||
public SelectionState Selection => Actions.Selection;
|
public SelectionState Selection => Actions.Selection;
|
||||||
|
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||||
public DispatcherMovementInputSource MovementInput { get; }
|
public DispatcherMovementInputSource MovementInput { get; }
|
||||||
public GameplayInputFrameController GameplayInput { get; }
|
public GameplayInputFrameController GameplayInput { get; }
|
||||||
public RecordingCombatModeOperations CombatMode { get; }
|
public RecordingCombatModeOperations CombatMode { get; }
|
||||||
|
|
@ -874,6 +875,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Actions.Dispose();
|
Actions.Dispose();
|
||||||
InventoryState.Dispose();
|
InventoryState.Dispose();
|
||||||
Entities.Clear();
|
Entities.Clear();
|
||||||
|
MovementState.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
123
tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
Normal file
123
tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Runtime;
|
||||||
|
|
||||||
|
public sealed class RuntimeMovementOwnershipTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProductionConstructsOneCanonicalRuntimeMovementOwner()
|
||||||
|
{
|
||||||
|
string root = FindRepositoryRoot();
|
||||||
|
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||||
|
string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime");
|
||||||
|
string app = string.Join(
|
||||||
|
"\n",
|
||||||
|
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
|
||||||
|
.Select(File.ReadAllText));
|
||||||
|
string runtime = string.Join(
|
||||||
|
"\n",
|
||||||
|
Directory.EnumerateFiles(runtimeRoot, "*.cs", SearchOption.AllDirectories)
|
||||||
|
.Select(File.ReadAllText));
|
||||||
|
|
||||||
|
Assert.False(File.Exists(Path.Combine(
|
||||||
|
appRoot,
|
||||||
|
"Input",
|
||||||
|
"PlayerMovementController.cs")));
|
||||||
|
Assert.False(File.Exists(Path.Combine(
|
||||||
|
appRoot,
|
||||||
|
"Input",
|
||||||
|
"LocalPlayerOutboundController.cs")));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"class PlayerMovementController",
|
||||||
|
app,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"class LocalPlayerOutboundController",
|
||||||
|
app,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b"));
|
||||||
|
Assert.Single(Regex.Matches(
|
||||||
|
app,
|
||||||
|
@"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(")
|
||||||
|
.Cast<Match>());
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"ACDREAM_RUN_SKILL",
|
||||||
|
runtime,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"ACDREAM_JUMP_SKILL",
|
||||||
|
runtime,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner()
|
||||||
|
{
|
||||||
|
string root = FindRepositoryRoot();
|
||||||
|
string gameWindow = ReadAppSource(root, "Rendering", "GameWindow.cs");
|
||||||
|
string input = ReadAppSource(
|
||||||
|
root,
|
||||||
|
"Input",
|
||||||
|
"DispatcherMovementInputSource.cs");
|
||||||
|
string view = ReadAppSource(
|
||||||
|
root,
|
||||||
|
"Runtime",
|
||||||
|
"CurrentGameRuntimeViewAdapter.cs");
|
||||||
|
string commands = ReadAppSource(
|
||||||
|
root,
|
||||||
|
"Runtime",
|
||||||
|
"CurrentGameRuntimeCommandAdapter.cs");
|
||||||
|
string lifetime = ReadAppSource(
|
||||||
|
root,
|
||||||
|
"Rendering",
|
||||||
|
"GameWindowLifetime.cs");
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
"RuntimeLocalPlayerMovementState _playerControllerSlot = new();",
|
||||||
|
gameWindow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"new AcDream.App.Input.DispatcherMovementInputSource(",
|
||||||
|
gameWindow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"_playerControllerSlot,",
|
||||||
|
gameWindow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"RuntimeLocalPlayerMovementState movement,",
|
||||||
|
input,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"movement ?? throw new ArgumentNullException(nameof(movement))).View",
|
||||||
|
view,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"RuntimeLocalPlayerMovementState Movement",
|
||||||
|
lifetime,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"\"runtime local movement state\"",
|
||||||
|
lifetime,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadAppSource(string root, params string[] relative) =>
|
||||||
|
File.ReadAllText(Path.Combine(
|
||||||
|
[root, "src", "AcDream.App", .. relative]));
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
|
||||||
|
return current.FullName;
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -625,11 +625,11 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
== typeof(AcDream.App.Interaction.SelectionInteractionController));
|
== typeof(AcDream.App.Interaction.SelectionInteractionController));
|
||||||
|
|
||||||
FieldInfo outboundDiagnostics = Assert.Single(
|
FieldInfo outboundDiagnostics = Assert.Single(
|
||||||
typeof(AcDream.App.Input.LocalPlayerOutboundController).GetFields(
|
typeof(LocalPlayerOutboundController).GetFields(
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic),
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
field => field.Name == "_diagnostic");
|
field => field.Name == "_diagnostic");
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
typeof(AcDream.App.Input.IMovementTruthDiagnosticSink),
|
typeof(IMovementTruthDiagnosticSink),
|
||||||
outboundDiagnostics.FieldType);
|
outboundDiagnostics.FieldType);
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
typeof(AcDream.App.Input.MovementTruthDiagnosticController).GetFields(
|
typeof(AcDream.App.Input.MovementTruthDiagnosticController).GetFields(
|
||||||
|
|
|
||||||
1
tests/AcDream.Core.Tests/GlobalUsings.cs
Normal file
1
tests/AcDream.Core.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
global using AcDream.Runtime.Gameplay;
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Input;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class LocalPlayerImmediatePositionTests
|
public sealed class LocalPlayerImmediatePositionTests
|
||||||
{
|
{
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using AcDream.App.Input;
|
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Net.Packets;
|
using AcDream.Core.Net.Packets;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Input;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class LocalPlayerOutboundCombatStyleTests
|
public sealed class LocalPlayerOutboundCombatStyleTests
|
||||||
{
|
{
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Physics.Motion;
|
using AcDream.Core.Physics.Motion;
|
||||||
using Xunit;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Input;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class PlayerMouseLookMovementTests
|
public sealed class PlayerMouseLookMovementTests
|
||||||
{
|
{
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using Xunit;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Core.Tests.Input;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public class PlayerMovementControllerTests
|
public class PlayerMovementControllerTests
|
||||||
{
|
{
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using Xunit;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Physics;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class PlayerOutboundPositionTests
|
public sealed class PlayerOutboundPositionTests
|
||||||
{
|
{
|
||||||
|
|
@ -9,13 +9,15 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
public sealed class RuntimeGameplayOwnershipTests
|
public sealed class RuntimeGameplayOwnershipTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CombinedLedgerConvergesEveryJ4OwnerAndSubscription()
|
public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription()
|
||||||
{
|
{
|
||||||
using var entities = new RuntimeEntityObjectLifetime();
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
var inventory = new RuntimeInventoryState(entities);
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
var character = new RuntimeCharacterState();
|
var character = new RuntimeCharacterState();
|
||||||
var communication = new RuntimeCommunicationState();
|
var communication = new RuntimeCommunicationState();
|
||||||
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
||||||
|
var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
|
||||||
inventory.Shortcuts.Changed += static () => { };
|
inventory.Shortcuts.Changed += static () => { };
|
||||||
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
||||||
|
|
@ -73,7 +75,8 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
inventory,
|
inventory,
|
||||||
character,
|
character,
|
||||||
communication,
|
communication,
|
||||||
actions);
|
actions,
|
||||||
|
movement);
|
||||||
|
|
||||||
Assert.False(populated.IsConverged);
|
Assert.False(populated.IsConverged);
|
||||||
Assert.Equal(1, populated.Inventory.ShortcutCount);
|
Assert.Equal(1, populated.Inventory.ShortcutCount);
|
||||||
|
|
@ -86,7 +89,9 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
AcDream.Core.Combat.CombatMode.Melee,
|
AcDream.Core.Combat.CombatMode.Melee,
|
||||||
populated.Actions.CombatMode);
|
populated.Actions.CombatMode);
|
||||||
Assert.Equal(1, populated.Actions.TrackedTargetHealthCount);
|
Assert.Equal(1, populated.Actions.TrackedTargetHealthCount);
|
||||||
|
Assert.True(populated.Movement.AutoRunActive);
|
||||||
|
|
||||||
|
movement.Dispose();
|
||||||
actions.Dispose();
|
actions.Dispose();
|
||||||
communication.Dispose();
|
communication.Dispose();
|
||||||
character.Dispose();
|
character.Dispose();
|
||||||
|
|
@ -98,7 +103,8 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
inventory,
|
inventory,
|
||||||
character,
|
character,
|
||||||
communication,
|
communication,
|
||||||
actions);
|
actions,
|
||||||
|
movement);
|
||||||
|
|
||||||
Assert.True(retired.IsConverged);
|
Assert.True(retired.IsConverged);
|
||||||
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
|
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
|
||||||
|
|
@ -108,13 +114,14 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BorrowerFailuresCannotStrandAnyJ4OwnerDuringTerminalDisposal()
|
public void BorrowerFailuresCannotStrandAnyGameplayOwnerDuringTerminalDisposal()
|
||||||
{
|
{
|
||||||
using var entities = new RuntimeEntityObjectLifetime();
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
var inventory = new RuntimeInventoryState(entities);
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
var character = new RuntimeCharacterState();
|
var character = new RuntimeCharacterState();
|
||||||
var communication = new RuntimeCommunicationState();
|
var communication = new RuntimeCommunicationState();
|
||||||
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
||||||
|
var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
|
||||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
|
|
@ -132,6 +139,7 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
|
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
|
||||||
Assert.Equal(1, communication.DispatchFailureCount);
|
Assert.Equal(1, communication.DispatchFailureCount);
|
||||||
|
|
||||||
|
movement.Dispose();
|
||||||
actions.Dispose();
|
actions.Dispose();
|
||||||
Assert.Throws<AggregateException>(inventory.Dispose);
|
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||||
Assert.Throws<AggregateException>(character.Dispose);
|
Assert.Throws<AggregateException>(character.Dispose);
|
||||||
|
|
@ -142,7 +150,8 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
inventory,
|
inventory,
|
||||||
character,
|
character,
|
||||||
communication,
|
communication,
|
||||||
actions);
|
actions,
|
||||||
|
movement);
|
||||||
|
|
||||||
Assert.True(retired.IsConverged);
|
Assert.True(retired.IsConverged);
|
||||||
Assert.Equal(1, retired.Communication.DispatchFailureCount);
|
Assert.Equal(1, retired.Communication.DispatchFailureCount);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
|
public sealed class RuntimeLocalPlayerMovementStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ViewProjectsTheExactCanonicalControllerAndAutorunOwner()
|
||||||
|
{
|
||||||
|
var controller = new PlayerMovementController(new PhysicsEngine())
|
||||||
|
{
|
||||||
|
LocalEntityId = 0x50000001u,
|
||||||
|
};
|
||||||
|
controller.SetPosition(
|
||||||
|
new Vector3(11f, 12f, 13f),
|
||||||
|
0xA9B40001u,
|
||||||
|
new Vector3(11f, 12f, 13f));
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState
|
||||||
|
{
|
||||||
|
Controller = controller,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Same(movement, movement.View);
|
||||||
|
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
||||||
|
|
||||||
|
RuntimeMovementSnapshot snapshot = movement.View.Snapshot;
|
||||||
|
Assert.True(snapshot.HasController);
|
||||||
|
Assert.Equal(0x50000001u, snapshot.LocalEntityId);
|
||||||
|
Assert.Equal(controller.CellPosition, snapshot.Position);
|
||||||
|
Assert.Equal(controller.BodyVelocity, snapshot.Velocity);
|
||||||
|
Assert.Equal(controller.IsAirborne, snapshot.IsAirborne);
|
||||||
|
Assert.Equal(controller.SimTimeSeconds, snapshot.SimulationTimeSeconds);
|
||||||
|
Assert.True(snapshot.AutoRunActive);
|
||||||
|
Assert.Equal(movement.Revision, snapshot.Revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
|
||||||
|
{
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
|
||||||
|
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
||||||
|
Assert.True(movement.AutoRunActive);
|
||||||
|
Assert.True(movement.View.Snapshot.AutoRunActive);
|
||||||
|
|
||||||
|
Assert.True(movement.Execute(RuntimeMovementCommand.Stop));
|
||||||
|
Assert.False(movement.AutoRunActive);
|
||||||
|
Assert.False(movement.View.Snapshot.AutoRunActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConcurrentRuntimeInstancesHaveIndependentMovementState()
|
||||||
|
{
|
||||||
|
using var first = new RuntimeLocalPlayerMovementState();
|
||||||
|
using var second = new RuntimeLocalPlayerMovementState();
|
||||||
|
var firstController = new PlayerMovementController(new PhysicsEngine())
|
||||||
|
{
|
||||||
|
LocalEntityId = 1u,
|
||||||
|
};
|
||||||
|
var secondController = new PlayerMovementController(new PhysicsEngine())
|
||||||
|
{
|
||||||
|
LocalEntityId = 2u,
|
||||||
|
};
|
||||||
|
first.Controller = firstController;
|
||||||
|
second.Controller = secondController;
|
||||||
|
|
||||||
|
first.Execute(RuntimeMovementCommand.ToggleRunLock);
|
||||||
|
firstController.SetPosition(Vector3.One, 0xA9B40001u, Vector3.One);
|
||||||
|
secondController.SetPosition(
|
||||||
|
new Vector3(2f),
|
||||||
|
0xA9B50001u,
|
||||||
|
new Vector3(2f));
|
||||||
|
|
||||||
|
Assert.True(first.Snapshot.AutoRunActive);
|
||||||
|
Assert.False(second.Snapshot.AutoRunActive);
|
||||||
|
Assert.Equal(1u, first.Snapshot.LocalEntityId);
|
||||||
|
Assert.Equal(2u, second.Snapshot.LocalEntityId);
|
||||||
|
Assert.NotEqual(first.Snapshot.Position, second.Snapshot.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MotionPreparationPublishesOnlyTheConstructionSeam()
|
||||||
|
{
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
IRuntimeLocalPlayerMotionSource source = movement;
|
||||||
|
var committed = new PlayerMovementController(new PhysicsEngine());
|
||||||
|
var candidate = new PlayerMovementController(new PhysicsEngine());
|
||||||
|
movement.Controller = committed;
|
||||||
|
bool drained = false;
|
||||||
|
|
||||||
|
using (movement.BeginMotionPreparation(
|
||||||
|
candidate,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
drained = true;
|
||||||
|
Assert.Same(committed.Motion, source.Motion);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
Assert.True(drained);
|
||||||
|
Assert.Same(committed, movement.Controller);
|
||||||
|
Assert.Same(candidate.Motion, source.Motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Same(committed.Motion, source.Motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TerminalDisposalConvergesWhenPreparationLeaseUnwindsLater()
|
||||||
|
{
|
||||||
|
var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||||
|
IDisposable preparation = movement.BeginMotionPreparation(controller);
|
||||||
|
movement.Controller = controller;
|
||||||
|
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
|
||||||
|
|
||||||
|
movement.Dispose();
|
||||||
|
preparation.Dispose();
|
||||||
|
|
||||||
|
RuntimeLocalMovementOwnershipSnapshot ownership =
|
||||||
|
movement.CaptureOwnership();
|
||||||
|
Assert.True(ownership.IsConverged);
|
||||||
|
Assert.Throws<ObjectDisposedException>(
|
||||||
|
() => movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypedSkillOptionsPreserveCompleteValuesAndFallbackPerField()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
new PlayerMovementConstructionOptions(321, 654),
|
||||||
|
PlayerMovementConstructionOptions.From(
|
||||||
|
new RuntimeMovementSkillSnapshot(321, 654, 1)));
|
||||||
|
Assert.Equal(
|
||||||
|
new PlayerMovementConstructionOptions(
|
||||||
|
PlayerMovementConstructionOptions.FallbackRunSkill,
|
||||||
|
654),
|
||||||
|
PlayerMovementConstructionOptions.From(
|
||||||
|
new RuntimeMovementSkillSnapshot(-1, 654, 1)));
|
||||||
|
Assert.Equal(
|
||||||
|
new PlayerMovementConstructionOptions(
|
||||||
|
321,
|
||||||
|
PlayerMovementConstructionOptions.FallbackJumpSkill),
|
||||||
|
PlayerMovementConstructionOptions.From(
|
||||||
|
new RuntimeMovementSkillSnapshot(321, -1, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarmMovementSnapshotsAndOwnershipCaptureAllocateNothing()
|
||||||
|
{
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
movement.Controller = new PlayerMovementController(new PhysicsEngine())
|
||||||
|
{
|
||||||
|
LocalEntityId = 7u,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 1_000; i++)
|
||||||
|
{
|
||||||
|
_ = movement.Snapshot;
|
||||||
|
_ = movement.CaptureOwnership();
|
||||||
|
}
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 100_000; i++)
|
||||||
|
{
|
||||||
|
_ = movement.Snapshot;
|
||||||
|
_ = movement.CaptureOwnership();
|
||||||
|
}
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0L, allocated);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue