acdream/docs/research/2026-07-12-login-placement-and-repeat-cancel-pseudocode.md
Erik 00fe993f6f 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>
2026-07-12 22:20:17 +02:00

3.9 KiB

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.

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:

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

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.