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:
Erik 2026-07-12 22:20:17 +02:00
parent e671b8a5c4
commit 00fe993f6f
11 changed files with 643 additions and 16 deletions

View file

@ -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.