fix(combat): escape occupied login positions
Port retail's radius-aware placement ring so a relogging player is seated beside creatures occupying the saved location, and register the local body in the shared resolved-shadow pipeline. Route new forward movement and jump through AbortAutomaticAttack so repeat combat cancels immediately on movement. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
e671b8a5c4
commit
00fe993f6f
11 changed files with 643 additions and 16 deletions
|
|
@ -46,6 +46,58 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #207 — Repeat attack continues after movement begins
|
||||
|
||||
**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate
|
||||
**Severity:** HIGH
|
||||
**Component:** combat / input
|
||||
|
||||
**Description:** With Repeat Attacks enabled, starting to move did not stop the
|
||||
attack loop, making combat difficult to disengage from.
|
||||
|
||||
**Root cause:** The attack controller had no port of
|
||||
`ClientCombatSystem::AbortAutomaticAttack`, and semantic movement input was
|
||||
never forwarded to combat as retail's `ACCmdInterp::HandleNewForwardMovement`
|
||||
does.
|
||||
|
||||
**Resolution:** Forward, backward, autorun, and jump press transitions now send
|
||||
the retail cancel-attack event, clear repeat mode, and hide an active combat
|
||||
power build. Turns and sidesteps remain independent, matching the retail
|
||||
command-list split. Controller tests prove movement sends one cancel and that a
|
||||
later AttackDone cannot launch another repeat.
|
||||
|
||||
**Research:** `docs/research/2026-07-12-login-placement-and-repeat-cancel-pseudocode.md`
|
||||
|
||||
**Acceptance:** Enable Repeat Attacks, start attacking, then press forward. The
|
||||
attack loop stops immediately and movement proceeds normally.
|
||||
|
||||
## #206 — Relogging into a monster leaves the player stuck
|
||||
|
||||
**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate
|
||||
**Severity:** HIGH
|
||||
**Component:** physics / login lifecycle
|
||||
|
||||
**Description:** If a monster moved onto the character's saved position while
|
||||
the user was logged out, logging back in placed both bodies together and the
|
||||
character could not escape.
|
||||
|
||||
**Root cause:** Login used the cell/floor-only `Resolve` snap and omitted retail
|
||||
`CTransition::find_placement_pos`, the radius-aware nearest-clear-position
|
||||
search. The local player was also omitted from the shadow registry, so remote
|
||||
creatures could not collide/de-overlap against it as retail physics objects do.
|
||||
|
||||
**Resolution:** The retail four-metre concentric compass-ring placement search
|
||||
is ported into Core and runs after login's existing AdjustPosition/floor snap.
|
||||
The local player now registers a normal multipart collision shadow, skips that
|
||||
shadow in its own transition, and republishes the resolved body position through
|
||||
the same shared synchronizer as remotes. A flat-world conformance test begins
|
||||
inside a monster and proves the selected position clears both bodies.
|
||||
|
||||
**Research:** `docs/research/2026-07-12-login-placement-and-repeat-cancel-pseudocode.md`
|
||||
|
||||
**Acceptance:** Log out beside a monster, let it occupy the saved position, and
|
||||
log back in. The character is seated at the nearest clear point and can move.
|
||||
|
||||
## #205 — Auto Target clears the toolbar and includes friendly creatures
|
||||
|
||||
**Status:** DONE — 2026-07-12, user confirmed the replacement target appears correctly
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 2. Adaptation (AD) — 36 rows (AD-39/40/41 added 2026-07-07 — the #182 verbatim player-physics rebuild; AD-25 narrowed to the remote-DR sweep, its player half retired by the rebuild)
|
||||
## 2. Adaptation (AD) — 37 rows (AD-42 added 2026-07-12 — enter-world placement is split across the existing snap plus the ported object-aware ring search)
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -103,6 +103,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| 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-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-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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
# Login placement and repeat-attack cancellation — retail pseudocode
|
||||
|
||||
## Sources
|
||||
|
||||
- `CTransition::find_placement_pos` @ `0x0050BA50`
|
||||
- `CTransition::find_placement_position` @ `0x0050C170`
|
||||
- `CObjCell::find_obj_collisions` @ `0x0052B750`
|
||||
- `CPhysicsObj::add_shadows_to_cells` @ `0x00514AE0`
|
||||
- `CPhysicsObj::enter_world` @ `0x00516170`
|
||||
- `ClientCombatSystem::AbortAutomaticAttack` @ `0x0056AE90`
|
||||
- `ClientCombatSystem::CommenceJump` @ `0x0056AF90`
|
||||
- `ACCmdInterp::HandleNewForwardMovement` @ `0x0058B1F0`
|
||||
- Named pseudo-C: `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- Interpretation cross-check: ACE `Transition.cs`, `CommandInterpreter.cs`, and
|
||||
`ACCmdInterp.cs` on the `ACEmulator/ACE` `master` branch.
|
||||
|
||||
## Enter-world placement
|
||||
|
||||
Retail `enter_world(position)` stores the server position and calls
|
||||
`enter_world(true)`. That constructs `SetPosition` flags `0x11`, so placement
|
||||
may slide away from an obstruction.
|
||||
|
||||
```text
|
||||
find_placement_position():
|
||||
check = current
|
||||
insert_type = INITIAL_PLACEMENT
|
||||
test environment/cells at check
|
||||
(CObjCell skips shadow-object collisions for INITIAL_PLACEMENT)
|
||||
validate initial placement
|
||||
|
||||
insert_type = PLACEMENT
|
||||
if !find_placement_pos(): fail
|
||||
|
||||
optionally step down to a walkable surface
|
||||
validate final placement
|
||||
```
|
||||
|
||||
`find_placement_pos` is the object-aware nearest-clear-point search:
|
||||
|
||||
```text
|
||||
find_placement_pos():
|
||||
check = current
|
||||
clear sliding/contact state
|
||||
if placement_test(check) succeeds:
|
||||
accept check and return true
|
||||
|
||||
if placement sliding is disabled:
|
||||
return false
|
||||
|
||||
search_distance = 4 metres
|
||||
search_radius = 4 metres
|
||||
radius = mover foot-sphere radius
|
||||
|
||||
if radius < 0.125 metres:
|
||||
fake_sphere = true
|
||||
search_radius = 2 metres
|
||||
else if radius < 0.48 metres:
|
||||
radius = 0.48 metres
|
||||
|
||||
exact_step_count = 4 / radius
|
||||
if fake_sphere: exact_step_count *= 0.5
|
||||
if exact_step_count <= 1: return false
|
||||
|
||||
step_count = ceil(exact_step_count)
|
||||
distance_step = search_radius / step_count
|
||||
radians_step = PI * distance_step / radius
|
||||
total_distance = 0
|
||||
total_radians = 0
|
||||
|
||||
for each radial step:
|
||||
total_distance += distance_step
|
||||
total_radians += radians_step
|
||||
compass_samples = ceil(total_radians) * 2
|
||||
heading_step_degrees = 360 / compass_samples
|
||||
|
||||
for sample = 0 .. compass_samples-1:
|
||||
check = current
|
||||
heading = heading_step_degrees * sample
|
||||
offset = forward_vector(heading) * total_distance
|
||||
offset = adjust_offset(offset)
|
||||
if length(offset) < epsilon: continue
|
||||
|
||||
check += offset
|
||||
clear sliding/contact state
|
||||
if placement_test(check) succeeds:
|
||||
accept check and return true
|
||||
|
||||
return false
|
||||
```
|
||||
|
||||
The local player must also own a normal shadow entry. Retail registers every
|
||||
placed `CPhysicsObj`; collision queries exclude only the moving object's own
|
||||
pointer. Therefore remote monsters can collide/de-overlap against the local
|
||||
player, while the player's placement search skips its own shadow by entity id.
|
||||
|
||||
## Movement cancels repeat attack
|
||||
|
||||
```text
|
||||
ACCmdInterp.HandleNewForwardMovement():
|
||||
if combat_system exists:
|
||||
combat_system.AbortAutomaticAttack()
|
||||
base.HandleNewForwardMovement() // also stops auto-run
|
||||
|
||||
ClientCombatSystem.AbortAutomaticAttack():
|
||||
if server_response_pending
|
||||
or attack_request_in_progress
|
||||
or attack_in_progress
|
||||
or repeat_attacking:
|
||||
send Event_CancelAttack
|
||||
repeat_attacking = false
|
||||
if combat power build is active:
|
||||
HidePowerBar()
|
||||
```
|
||||
|
||||
`CommandInterpreter::AddCommand` calls `HandleNewForwardMovement` for the
|
||||
forward-substate command list, which includes forward/backward/autorun rather
|
||||
than the independent turn and sidestep lists. Jump has the same cancellation
|
||||
at `ClientCombatSystem::CommenceJump` before its jump power bar begins.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue