# AC Collision Transition System — Pseudocode

Research document for porting the retail AC collision/transition system to acdream.
Cross-referenced between ACE's `Transition.cs` (interpretation aid) and the decompiled
retail client `chunk_00530000.c` (ground truth).

**Naming convention:** ACE names throughout, with decompiled addresses in comments.

---

## Table of Contents

1. [Data Structures](#1-data-structures)
2. [FindTransitionalPosition](#2-findtransitionalposition) — main entry point
3. [TransitionalInsert](#3-transitionalinsert) — per-step collision check
4. [FindEnvCollisions](#4-findenvcollisions) — BSP query against environment
5. [StepUp / StepDown](#5-stepup--stepdown) — step height handling
6. [AdjustOffset / SlideSphere](#6-adjustoffset--slidesphere) — wall slide projection
7. [ValidateTransition](#7-validatetransition) — post-step validation
8. [Already Ported](#8-already-ported) — what exists in CollisionPrimitives.cs
9. [Key Constants](#9-key-constants)
10. [ACE vs Decompiled Differences](#10-ace-vs-decompiled-differences)

---

## 1. Data Structures

### TransitionState (enum)
```
Invalid   = 0  -- transition failed completely
OK        = 1  -- no collision, position accepted
Collided  = 2  -- hard collision, cannot pass
Adjusted  = 3  -- position was nudged to resolve overlap
Slid      = 4  -- position was projected along a surface
```

### Transition (main orchestrator)
```
ObjectInfo    : ObjectInfo      -- flags, step heights, object reference
SpherePath    : SpherePath      -- movement path with sphere(s)
CollisionInfo : CollisionInfo   -- accumulated collision results
CellArray     : CellArray       -- scratch buffer for cell enumeration
```

### SpherePath (movement descriptor)
```
NumSphere          : int           -- 1 or 2 spheres (foot + body)
LocalSphere[0..1]  : Sphere        -- spheres in object-local space
GlobalSphere[0..1] : Sphere        -- spheres transformed to world (at CheckPos)
GlobalCurrCenter[0..1] : Sphere    -- spheres transformed to world (at CurPos)
BeginPos, EndPos   : Position      -- start and end of the requested move
CurPos             : Position      -- last accepted position
CheckPos           : Position      -- candidate position being tested
CurCell, CheckCell : ObjCell       -- cells for CurPos and CheckPos
GlobalOffset       : Vector3       -- the offset being applied this step
InsertType         : InsertType    -- Transition(0), Placement(1), InitialPlacement(2)

-- Step-up state
StepUp             : bool
StepUpNormal       : Vector3
Collide            : bool          -- set when a collide-then-find-walkable is pending

-- Step-down state
StepDown           : bool
StepDownAmt        : float
WalkInterp         : float         -- interpolation factor [0..1], shrinks as step-down progresses

-- Walkable tracking
Walkable           : Polygon       -- the polygon we are standing on
WalkableCheckPos   : Sphere        -- position used for walkable checks
WalkableUp         : Vector3       -- up vector for the walkable surface
WalkablePos        : Position      -- local-space position of the walkable
WalkableScale      : float
WalkableAllowance  : float         -- minimum Z component of normal to be "walkable"

-- Backup for restore
BackupCell         : ObjCell
BackupCheckPos     : Position

-- Misc flags
NegPolyHit         : bool          -- secondary sphere hit a polygon (deferred)
NegStepUp          : bool          -- the deferred hit wants a step-up
NegCollisionNormal : Vector3       -- normal of the deferred hit
CellArrayValid     : bool
HitsInteriorCell   : bool
BuildingCheck      : bool
ObstructionEthereal: bool
CheckWalkable      : bool
PlacementAllowsSliding : bool      -- default true
```

### CollisionInfo (accumulated results)
```
ContactPlaneValid          : bool
ContactPlane               : Plane
ContactPlaneCellID         : uint
ContactPlaneIsWater        : bool

LastKnownContactPlaneValid : bool
LastKnownContactPlane      : Plane
LastKnownContactPlaneCellID: uint
LastKnownContactPlaneIsWater: bool

SlidingNormalValid         : bool
SlidingNormal              : Vector3    -- XY only (Z zeroed)

CollisionNormalValid       : bool
CollisionNormal            : Vector3

CollidedWithEnvironment    : bool
FramesStationaryFall       : int        -- gravity fall-through safety counter

AdjustOffset               : Vector3
CollideObject              : List<PhysicsObj>
LastCollidedObject         : PhysicsObj
```

### ObjectInfo (flags + per-object properties)
```
State          : ObjectInfoState flags  -- Contact, OnWalkable, IsViewer, PathClipped,
                                        -- FreeRotate, PerfectClip, EdgeSlide, etc.
StepUpHeight   : float
StepDownHeight : float
Ethereal       : bool
StepDown       : bool       -- true unless the object is a missile
Scale          : float
```

---

## 2. FindTransitionalPosition

**ACE:** `Transition.FindTransitionalPosition()`
**Decompiled:** Not directly in chunk_00530000; lives in the PhysicsObj transition caller.

This is the main entry point for moving an object from BeginPos to EndPos.

```
function FindTransitionalPosition() -> bool:
    if SpherePath.BeginCell is null:
        return false

    -- Calculate movement and subdivide into steps
    offset = BeginPos.GetOffset(EndPos)        -- total movement vector
    dist = length(offset)
    step = dist / LocalSphere[0].Radius

    if IsViewer:
        if dist <= EPSILON:
            numSteps = 0
            offsetPerStep = Zero
        else:
            offsetPerStep = offset * (1 / step)
            numSteps = floor(step) + 1
    else:
        if step > 1.0:
            numSteps = ceil(step)
            offsetPerStep = offset * (1 / numSteps)
        elif offset != Zero:
            offsetPerStep = offset
            numSteps = 1
        else:
            offsetPerStep = Zero
            numSteps = 0

    -- Safety cap: abort if too many steps (retail uses 30; ACE bumped to 1000)
    if numSteps > MAX_STEPS and not IsSightObj:
        return false

    -- Apply free rotation if flagged
    if FreeRotate:
        CurPos.Frame.Orientation = EndPos.Frame.Orientation

    SetCheckPos(CurPos, CurCell)

    -- Zero-step case: just validate cell membership
    if numSteps <= 0:
        if not FreeRotate:
            CurPos.Frame.Orientation = EndPos.Frame.Orientation
        find_cell_list(...)   -- rebuild cell array for current position
        return true

    -- Main stepping loop
    for step = 0 to numSteps-1:

        -- IsViewer: last step uses remaining distance instead of fixed step size
        if IsViewer and step == numSteps - 1:
            remaining = dist - LocalSphere[0].Radius * (numSteps - 1)
            offsetPerStep = offset * (remaining / dist)

        -- Apply contact-plane and sliding-normal adjustments to the offset
        GlobalOffset = AdjustOffset(offsetPerStep)

        -- Non-viewer: abort if adjusted offset is negligible
        if not IsViewer:
            if GlobalOffset.LengthSquared < EPSILON^2:
                return (step != 0 and transitionState == OK)

        -- Interpolate rotation if not free-rotating
        if not FreeRotate:
            delta = (step + 1) / numSteps
            CheckPos.Frame.InterpolateRotation(BeginPos, EndPos, delta)

        -- Reset per-step collision state
        CollisionInfo.SlidingNormalValid = false
        CollisionInfo.ContactPlaneValid = false
        CollisionInfo.ContactPlaneIsWater = false

        if InsertType != Transition:
            -- Placement mode: try insert, validate placement
            result = TransitionalInsert(3)
            transitionState = ValidatePlacementTransition(result)
            if transitionState == OK: return true
            if not PlacementAllowsSliding: return false
            AddOffsetToCheckPos(GlobalOffset)
        else:
            -- Transition mode: apply offset first, then check
            AddOffsetToCheckPos(GlobalOffset)
            result = TransitionalInsert(3)
            transitionState = ValidateTransition(result)
            if FramesStationaryFall > 0: break

        -- PathClipped objects stop on any collision normal
        if CollisionNormalValid and PathClipped: break

    return transitionState == OK
```

### Key insight: step subdivision
The movement is divided so each sub-step travels at most one sphere radius.
This ensures the sphere cannot "tunnel" through thin walls. The IsViewer path
uses a slightly different calculation (floor + 1 instead of ceil) because
viewers need to reach the exact endpoint on the last step.

---

## 3. TransitionalInsert

**ACE:** `Transition.TransitionalInsert(int num_insertion_attempts)`
**Decompiled:** Part of the CTransition methods in chunk_00530000.

Per-step collision detection. Checks the current cell, then neighboring cells,
then handles step-up, step-down, and slide responses.

```
function TransitionalInsert(maxAttempts) -> TransitionState:
    if CheckCell is null: return OK
    if maxAttempts <= 0: return Invalid

    for attempt = 0 to maxAttempts-1:

        -- Phase 1: Check collisions in current cell
        transitState = InsertIntoCell(CheckCell, maxAttempts)

        switch transitState:
            case OK:
                -- Phase 2: Check neighboring cells
                transitState = CheckOtherCells(CheckCell)
                if transitState != OK: NegPolyHit = false
                if transitState == Collided: return Collided
            case Collided:
                return Collided
            case Adjusted:
                NegPolyHit = false
            case Slid:
                ContactPlaneValid = false
                ContactPlaneIsWater = false
                NegPolyHit = false

        -- Phase 3: Post-collision response
        if transitState == OK:
            if not Collide:
                -- Handle deferred secondary-sphere polygon hit
                if NegPolyHit and not StepDown and not StepUp:
                    NegPolyHit = false
                    if NegStepUp:
                        if not StepUp(NegCollisionNormal):
                            transitState = StepUpSlide()
                    else:
                        transitState = SlideSphere(NegCollisionNormal, GlobalCurrCenter[0])

                -- Handle step-down when in contact but no ground found
                elif not ContactPlaneValid and Contact and not StepDown
                     and CheckCell != null and ObjectInfo.StepDown:

                    zVal = LandingZ (or GetWalkableZ if OnWalkable)
                    stepDownHeight = 0.04 (or ObjectInfo.StepDownHeight if OnWalkable)
                    WalkableAllowance = zVal
                    SaveCheckPos()

                    -- Split into half-steps if step height > 2*radius
                    radsum = GlobalSphere[0].Radius * 2
                    if NumSphere < 2 and radsum < stepDownHeight:
                        stepDownHeight = GlobalSphere[0].Radius * 0.5

                    if radsum >= stepDownHeight:
                        if StepDown(stepDownHeight, zVal):
                            Walkable = null; return OK
                    else:
                        stepDownHeight *= 0.5
                        if StepDown(stepDownHeight, zVal) or StepDown(stepDownHeight, zVal):
                            Walkable = null; return OK

                    -- Edge slide fallback
                    if EdgeSlide(transitState, stepDownHeight, zVal):
                        return transitState

                else:
                    return OK   -- clean transition, no issues

            else:
                -- Collide flag is set: find-walkable-then-validate pattern
                Collide = false
                reset = false

                if ContactPlaneValid and CheckWalkable(LandingZ):
                    -- Try re-inserting as Placement to verify position
                    save InsertType
                    InsertType = Placement
                    transitState = TransitionalInsert(maxAttempts)
                    restore InsertType

                    if transitState != OK:
                        transitState = OK
                        reset = true
                else:
                    reset = true

                Walkable = null

                if not reset: return transitState

                -- Reset: collision was not resolvable by stepping
                RestoreCheckPos()
                ContactPlaneValid = false
                ContactPlaneIsWater = false

                if LastKnownContactPlaneValid:
                    LastKnownContactPlaneValid = false
                    StopVelocity()    -- zero out the object's velocity
                else:
                    SetCollisionNormal(StepUpNormal)

                return Collided

    return transitState
```

### InsertIntoCell
```
function InsertIntoCell(cell, maxAttempts) -> TransitionState:
    if cell is null: return Collided

    for i = 0 to maxAttempts-1:
        transitState = cell.FindCollisions(this)
        switch transitState:
            case OK, Collided:
                return transitState
            case Slid:
                ContactPlaneValid = false
                ContactPlaneIsWater = false
                -- continue loop (retry after slide)

    return transitState   -- returns last state (usually Slid)
```

### CheckOtherCells
```
function CheckOtherCells(currCell) -> TransitionState:
    -- Build list of cells the sphere touches
    find_cell_list(CellArray, newCell, SpherePath)

    for each cell in CellArray:
        if cell == currCell: skip

        collides = cell.FindCollisions(this)
        switch collides:
            case Slid:
                ContactPlaneValid = false
                ContactPlaneIsWater = false
                return Slid
            case Collided, Adjusted:
                return collides

    CheckCell = newCell

    if newCell != null:
        AdjustCheckPos(newCell.ID)
        return OK

    if StepDown:
        return Collided

    -- Try to resolve position to outdoor cell
    adjustedPos = copy of CheckPos
    if is_outdoor(adjustedPos):
        AdjustToOutside(adjustedPos)
    if adjustedPos.CellID != 0:
        AdjustCheckPos(adjustedPos.CellID)
        SetCheckPos(adjustedPos, null)
        -- Rebuild cell array and re-check
        find_cell_list(...)
        return OK

    return Collided
```

---

## 4. FindEnvCollisions

### LandCell (outdoor terrain)
**ACE:** `LandCell.FindEnvCollisions()`

```
function LandCell.FindEnvCollisions(transition) -> TransitionState:
    -- Check entry restrictions (e.g., PK zones, housing)
    transitState = check_entry_restrictions(transition)
    if transitState != OK: return transitState

    -- Find which terrain triangle we are over
    blockOffset = GetBlockOffset(CheckPos.CellID, this.ID)
    localPoint = GlobalLowPoint - blockOffset
    walkable = find_terrain_poly(localPoint)   -- returns one of 2 triangles per cell
    if walkable not found: return OK

    -- Water check: entirely-water cells block non-viewer, non-missile objects
    if block_water_type == EntirelyWater and not IsViewer and not Missile:
        return Collided

    waterDepth = get_water_depth(localPoint)

    -- Create local-space check sphere
    checkPos = copy of GlobalSphere[0]
    checkPos.Center -= blockOffset

    -- Delegate to ValidateWalkable
    return ValidateWalkable(checkPos, walkable.Plane, isWater, waterDepth, this.ID)
```

### ValidateWalkable
**ACE:** `ObjectInfo.ValidateWalkable()`

The core terrain collision response. Two paths: IsViewer (camera) and normal objects.

```
function ValidateWalkable(checkSphere, contactPlane, isWater, waterDepth, cellID):

    if IsViewer:
        -- Camera collision: adjust along movement ray to stay above ground
        dist = dot(checkSphere.Center, contactPlane.Normal) + contactPlane.D - checkSphere.Radius
        if dist > -EPSILON and BeginPos is indoor:
            return OK   -- camera can clip through terrain when coming from indoors

        offset = checkSphere.Center - GlobalCurrCenter[0].Center
        angle = dist / dot(offset, contactPlane.Normal)
        if (angle <= 0 or angle > 1) and BeginPos is indoor:
            return OK

        AddOffsetToCheckPos(offset * -angle)
        SetCollisionNormal(contactPlane.Normal)
        CollidedWithEnvironment = true
        return Adjusted

    else:
        -- Normal object: signed distance from low point to terrain plane
        lowPoint = checkSphere.Center - (0, 0, checkSphere.Radius)
        dist = dot(lowPoint, contactPlane.Normal) + contactPlane.D + waterDepth

        if dist >= -EPSILON:
            -- Above or touching the surface
            if dist <= EPSILON:
                -- Resting on surface: set contact plane
                if StepDown or not OnWalkable or is_valid_walkable(contactPlane.Normal):
                    SetContactPlane(contactPlane, isWater, cellID)
                if not Contact and not StepDown:
                    SetCollisionNormal(contactPlane.Normal)
                    CollidedWithEnvironment = true
            return OK

        else:
            -- Below the surface
            if CheckWalkable: return Collided   -- walkable probe

            zDist = dist / contactPlane.Normal.Z

            if StepDown or not OnWalkable or is_valid_walkable(contactPlane.Normal):
                SetContactPlane(contactPlane, isWater, cellID)

                if StepDown:
                    -- Validate step-down interpolation
                    interp = (1 - (-1 / (StepDownAmt * WalkInterp)) * zDist) * WalkInterp
                    if interp >= WalkInterp or interp < -0.1:
                        return Collided
                    WalkInterp = interp

                AddOffsetToCheckPos(0, 0, -zDist)

            if not Contact and not StepDown:
                SetCollisionNormal(contactPlane.Normal)
                CollidedWithEnvironment = true

            return Adjusted
```

### EnvCell (indoor BSP)
**ACE:** `EnvCell.FindEnvCollisions()`

```
function EnvCell.FindEnvCollisions(transition) -> TransitionState:
    transitState = check_entry_restrictions(transition)
    if transitState != OK: return transitState

    ObstructionEthereal = false

    if CellStructure.PhysicsBSP is null:
        return OK

    -- Transform spheres into cell-local space
    CacheLocalSpaceSphere(cellPos, scale=1.0)

    if InsertType == InitialPlacement:
        transitState = PhysicsBSP.placement_insert(transition)
    else:
        transitState = PhysicsBSP.find_collisions(transition, scale=1.0)

    if transitState != OK and not Contact:
        CollidedWithEnvironment = true

    return transitState
```

### BSPTree.find_collisions (indoor collision core)
**ACE:** `BSPTree.find_collisions()`
**Decompiled:** Part of the BSP traversal functions in chunk_00530000.

```
function BSPTree.find_collisions(transition, scale) -> TransitionState:
    center = LocalSpaceCurrCenter[0].Center    -- current pos in local space
    localSphere = LocalSpaceSphere[0]          -- check pos in local space
    movement = localSphere.Center - center

    -- Placement / Ethereal: simple solid intersection test
    if InsertType == Placement or ObstructionEthereal:
        clearCell = true
        if BuildingCheck: clearCell = not HitsInteriorCell
        if sphere_intersects_solid(localSphere, clearCell):
            return Collided
        if NumSphere > 1 and sphere_intersects_solid(localSphere[1], clearCell):
            return Collided
        return OK

    -- Walkable probe
    if CheckWalkable:
        return hits_walkable(localSphere, LocalSpaceZ) ? Collided : OK

    -- Step-down: find walkable surface below
    if StepDown:
        return step_sphere_down(transition, localSphere, scale)

    -- Collide flag: find a walkable surface to land on
    if Collide:
        validPos = copy of localSphere
        changed = false
        find_walkable(validPos, hitPoly, movement, LocalSpaceZ, changed)
        if changed:
            offset = (validPos.Center - localSphere.Center) transformed to global * scale
            AddOffsetToCheckPos(offset)
            contactPlane = hitPoly.Plane transformed to global
            SetContactPlane(contactPlane)
            SetWalkable(validPos, hitPoly, LocalSpaceZ, LocalSpacePos, scale)
            return Adjusted
        return OK

    -- Normal collision: sphere vs BSP polygon intersection
    hitPoly = null
    contactPoint = Zero

    if Contact:
        -- Object is on the ground: step-up on collision, slide on secondary sphere
        if sphere_intersects_poly(localSphere, movement, hitPoly, contactPoint):
            globNormal = LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal)
            if StepUp(globNormal): return OK
            return StepUpSlide()

        if NumSphere > 1:
            if sphere_intersects_poly(localSphere[1], movement, hitPoly2, contactPoint):
                return slide_sphere(hitPoly2.Plane.Normal)
            -- Deferred hits: NegPolyHit
            if hitPoly2 != null: SetNegPolyHit(false, hitPoly2); return OK
            if hitPoly  != null: SetNegPolyHit(true,  hitPoly);  return OK

        return OK

    -- Not in contact: first collision triggers Collide mode
    if sphere_intersects_poly(localSphere, movement, hitPoly, contactPoint) or hitPoly != null:
        if PathClipped:
            return collide_with_pt(localSphere, center, hitPoly, contactPoint, scale)

        collisionNormal = LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal)
        WalkableAllowance = LandingZ
        SetCollide(collisionNormal)  -- saves backup pos, sets Collide flag
        return Adjusted

    -- Secondary sphere
    if NumSphere > 1:
        if sphere_intersects_poly(localSphere[1], movement, hitPoly, contactPoint) or hitPoly != null:
            collisionNormal = LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal)
            SetCollisionNormal(collisionNormal)
            return Collided

    return OK
```

### BSP Traversal: sphere_intersects_poly
**ACE:** `BSPNode.sphere_intersects_poly()` / `BSPLeaf.sphere_intersects_poly()`
**Already ported:** `BSPQuery.SphereIntersectsPoly()` in `BSPQuery.cs`

```
function BSPNode.sphere_intersects_poly(checkPos, movement, hitPoly, contactPoint) -> bool:
    -- Broad phase: bounding sphere rejection
    if not BoundingSphere.Intersects(checkPos): return false

    -- Classify sphere against splitting plane
    dist = dot(SplittingPlane.Normal, checkPos.Center) + SplittingPlane.D
    reach = checkPos.Radius - EPSILON

    if dist >= reach:
        return PosNode.sphere_intersects_poly(...)
    if dist <= -reach:
        return NegNode.sphere_intersects_poly(...)

    -- Straddles: check both
    if PosNode.sphere_intersects_poly(...): return true
    return NegNode.sphere_intersects_poly(...)

function BSPLeaf.sphere_intersects_poly(checkPos, movement, hitPoly, contactPoint) -> bool:
    if NumPolys == 0 or not BoundingSphere.Intersects(checkPos): return false

    for each polygon:
        if polygon.pos_hits_sphere(checkPos, movement, contactPoint, hitPoly):
            return true
    return false
```

**Note:** `pos_hits_sphere` is the polygon-level test corresponding to our already-ported
`CollisionPrimitives.SphereIntersectsPoly()`. The difference is that `pos_hits_sphere`
also returns the contact point and sets the hitPoly reference even when the sphere
overlaps but does not fully intersect (important for the `hitPoly != null` fallback
paths in `find_collisions`).

---

## 5. StepUp / StepDown

### StepUp
**ACE:** `Transition.StepUp(Vector3 collisionNormal)`

Called when an object walking on the ground hits a wall. Tries to "step up"
onto the obstacle by doing a step-down from a raised position.

```
function StepUp(collisionNormal) -> bool:
    ContactPlaneValid = false
    ContactPlaneIsWater = false

    SpherePath.StepUp = true
    StepUpNormal = collisionNormal

    stepDownHeight = 0.04            -- default step probe height
    zLandingValue = LandingZ         -- default walkable Z threshold (0.0872)

    if OnWalkable:
        zLandingValue = GetWalkableZ()       -- object's specific walkable threshold
        stepDownHeight = ObjectInfo.StepUpHeight  -- object's specific step height

    WalkableAllowance = zLandingValue
    BackupCell = CheckCell
    BackupCheckPos = copy of CheckPos

    -- The step-up trick: StepDown with StepUp=true skips the
    -- downward offset (the sphere is already at the collision point,
    -- which is above the step). StepDown then probes downward to
    -- find a walkable surface.
    success = StepDown(stepDownHeight, zLandingValue)

    SpherePath.StepUp = false
    Walkable = null

    if not success:
        RestoreCheckPos()

    return success
```

### StepDown
**ACE:** `Transition.StepDown(float stepDownHeight, float zVal)`

Probes downward from the current check position to find a walkable surface.

```
function StepDown(stepDownHeight, zVal) -> bool:
    NegPolyHit = false
    SpherePath.StepDown = true
    StepDownAmt = stepDownHeight
    WalkInterp = 1.0

    -- If NOT in step-up mode, apply the downward offset
    if not StepUp:
        CellArrayValid = false
        offset = (0, 0, -stepDownHeight)
        CheckPos.Frame.Origin.Z -= stepDownHeight
        CacheGlobalSphere(offset)

    -- Run collision detection with step-down flag active
    transitState = TransitionalInsert(5)     -- 5 attempts for step-down

    SpherePath.StepDown = false

    -- Accept the step-down if:
    --   1. Collision detection returned OK
    --   2. A valid contact plane was found
    --   3. The contact plane is walkable (Normal.Z >= zVal)
    --   4. Edge-slide check passes (if applicable)
    if transitState == OK
       and ContactPlaneValid
       and ContactPlane.Normal.Z >= zVal
       and (not EdgeSlide or StepUp or CheckWalkable(zVal)):

        -- Validate the final position with a placement insert
        save InsertType
        InsertType = Placement
        transitState = TransitionalInsert(1)
        restore InsertType

        return transitState == OK

    return false
```

### StepSphereUp (object-vs-object)
**ACE:** `Sphere.StepSphereUp()`

When an object on the ground collides with another sphere-based object:

```
function StepSphereUp(center, transition, disp, radsum) -> TransitionState:
    radsum += EPSILON

    -- If the obstacle is taller than our step-up height, slide instead
    if StepUpHeight < radsum - disp.Z:
        return SlideSphere(center, transition, disp, 0)

    -- Otherwise, try a full Transition.StepUp
    collisionNormal = GlobalCurrCenter[0].Center - center
    if Transition.StepUp(collisionNormal):
        return OK
    else:
        return StepUpSlide()
```

### step_sphere_down (BSP indoor)
**ACE:** `BSPTree.step_sphere_down()`

Finds a walkable surface below the sphere in BSP geometry:

```
function step_sphere_down(transition, checkPos, scale) -> TransitionState:
    step_down_amount = -(StepDownAmt * WalkInterp)
    movement = LocalSpaceZ * step_down_amount * (1 / scale)

    validPos = copy of checkPos
    changed = false
    polyHit = null

    find_walkable(validPos, polyHit, movement, LocalSpaceZ, changed)

    if changed:
        adjusted = validPos.Center - checkPos.Center
        offset = LocalSpacePos.LocalToGlobalVec(adjusted) * scale
        CheckPos.Frame.Origin += offset
        CacheGlobalSphere(offset)

        contactPlane = polyHit.Plane transformed to global, D *= scale
        SetContactPlane(contactPlane, false)
        ContactPlaneCellID = CheckPos.ObjCellID
        SetWalkable(validPos, polyHit, LocalSpaceZ, LocalSpacePos, scale)

        return Adjusted

    return OK
```

---

## 6. AdjustOffset / SlideSphere

### AdjustOffset
**ACE:** `Transition.AdjustOffset(Vector3 offset)`

Adjusts the per-step movement offset to account for existing contact planes
and sliding normals. This is called BEFORE applying the offset to CheckPos.

```
function AdjustOffset(offset) -> Vector3:
    result = copy of offset
    checkSlide = false

    -- Check if we should apply sliding
    slidingAngle = dot(result, SlidingNormal)
    if SlidingNormalValid:
        if slidingAngle < 0:
            checkSlide = true
        else:
            SlidingNormalValid = false

    -- No contact plane: simple sliding projection
    if not ContactPlaneValid:
        if checkSlide:
            result -= SlidingNormal * slidingAngle
        return result

    -- Have a contact plane: project movement onto the contact surface
    collisionAngle = dot(result, ContactPlane.Normal)
    slideOffset = cross(ContactPlane.Normal, SlidingNormal)

    if checkSlide:
        -- Project movement along the intersection of contact plane and slide plane
        if normalize(slideOffset) fails:
            result = Zero
        else:
            result = dot(slideOffset, result) * slideOffset
    elif collisionAngle <= 0:
        -- Moving into the contact plane: remove the component into the plane
        result -= ContactPlane.Normal * collisionAngle
    else:
        -- Moving away from contact plane: snap to plane
        ContactPlane.SnapToPlane(result)

    -- Safety: ensure sphere stays above contact plane
    if ContactPlaneCellID != 0 and not ContactPlaneIsWater:
        globSphere = GlobalSphere[0]
        blockOffset = GetBlockOffset(CheckPos.CellID, ContactPlaneCellID)
        dist = dot(globSphere.Center - blockOffset, ContactPlane.Normal) + ContactPlane.D
        if dist < globSphere.Radius - EPSILON:
            zDist = (globSphere.Radius - dist) / ContactPlane.Normal.Z
            if globSphere.Radius > abs(zDist):
                AddOffsetToCheckPos(0, 0, zDist)

    return result
```

### SlideSphere (wall slide — environment collision normal variant)
**ACE:** `Sphere.SlideSphere(Transition, ref Vector3 collisionNormal, Vector3 currPos)`
**Decompiled:** FUN_00538180 at 0x00538180

This is the primary slide function used after environment (BSP) collisions.

```
function SlideSphere(transition, collisionNormal, currPos) -> TransitionState:
    -- Degenerate case: zero collision normal
    if collisionNormal == Zero:
        halfOffset = (currPos - Center) * 0.5
        AddOffsetToCheckPos(halfOffset)
        return Adjusted

    SetCollisionNormal(collisionNormal)

    blockOffset = GetBlockOffset(CurPos.CellID, CheckPos.CellID)
    gDelta = blockOffset + (Center - currPos)    -- global displacement from curr to check

    -- Get the contact plane (prefer current, fall back to last known)
    contactPlane = ContactPlaneValid ? ContactPlane : LastKnownContactPlane

    -- Project movement along the crease between collision normal and contact plane
    direction = cross(collisionNormal, contactPlane.Normal)
    dirLenSq = direction.LengthSquared

    if dirLenSq >= EPSILON:
        -- Crease exists: project displacement onto it
        diff = dot(direction, gDelta)
        invDirLenSq = 1 / dirLenSq
        offset = direction * diff * invDirLenSq

        if offset.LengthSquared < EPSILON:
            return Collided

        offset -= gDelta     -- subtract current displacement to get correction
        AddOffsetToCheckPos(offset)
        return Slid

    -- Collision normal and contact plane are parallel
    if dot(collisionNormal, contactPlane.Normal) >= 0:
        -- Same direction: project out along collision normal
        diff = dot(collisionNormal, gDelta)
        offset = -collisionNormal * diff
        AddOffsetToCheckPos(offset)
        return Slid

    -- Opposing normals: give up, reverse direction
    collisionNormal = -gDelta
    if normalize(collisionNormal) succeeds:
        SetCollisionNormal(collisionNormal)
    return OK   -- note: returns OK, not Collided — this resets for retry
```

### SlideSphere (object-vs-object variant)
**ACE:** `Sphere.SlideSphere(Vector3 center, Transition, Vector3 disp, int sphereNum)`

Used when sliding off another physics object (sphere-sphere collision).

```
function SlideSphere_Object(center, transition, disp, sphereNum) -> TransitionState:
    globSphere = GlobalSphere[sphereNum]

    -- Collision normal: from obstacle center to our current center
    collisionNormal = GlobalCurrCenter[sphereNum].Center - center
    if normalize(collisionNormal) fails:
        return Collided

    SetCollisionNormal(collisionNormal)

    contactPlane = ContactPlaneValid ? ContactPlane : LastKnownContactPlane
    skid_dir = contactPlane.Normal
    direction = cross(collisionNormal, skid_dir)

    blockOffset = GetBlockOffset(CurPos.CellID, CheckPos.CellID)
    globOffset = globSphere.Center - GlobalCurrCenter[sphereNum].Center + blockOffset
    dirLenSq = direction.LengthSquared

    if dirLenSq >= EPSILON:
        skid_dir = dot(globOffset, direction) * direction
        invDirLenSq = 1 / dirLenSq
        skid_dir *= invDirLenSq
        direction = skid_dir

        if direction.LengthSquared < EPSILON:
            return Collided

        direction -= globOffset
        AddOffsetToCheckPos(direction)
        return Slid

    -- Parallel case
    if dot(skid_dir, disp) < 0:
        return Collided

    direction = -dot(globOffset, collisionNormal) * collisionNormal
    AddOffsetToCheckPos(direction)
    return Slid
```

---

## 7. ValidateTransition

**ACE:** `Transition.ValidateTransition()`

Called after each step to validate the result and update state.

```
function ValidateTransition(transitionState) -> TransitionState:
    moved = false   -- _redo in ACE

    if transitionState == OK and CheckPos != CurPos:
        -- Movement succeeded: accept the new position
        CurPos = CheckPos
        CurCell = CheckCell
        CacheGlobalCurrCenter()
        SetCheckPos(CurPos, CurCell)
        moved = true

    elif transitionState == OK:
        -- No movement (same position): accept as-is
        SetCurrentCheckPos()

    elif transitionState == Invalid:
        -- Do nothing, leave state as-is
        pass

    else:
        -- Collision/slide/adjusted: revert to current position
        if LastKnownContactPlaneValid:
            StopVelocity()
            -- Check if we are resting on the last known contact plane
            angle = dot(LastKnownContactPlane.Normal, GlobalCurrCenter[0].Center)
                    + LastKnownContactPlane.D
            if GlobalSphere[0].Radius + EPSILON > abs(angle):
                SetContactPlane(LastKnownContactPlane, LastKnownContactPlaneIsWater)
                ContactPlaneCellID = LastKnownContactPlaneCellID
                if OnWalkable: moved = true

        if not CollisionNormalValid:
            SetCollisionNormal(UnitZ)    -- default: push up

        SetCheckPos(CurPos, CurCell)
        find_cell_list(...)
        transitionState = OK

    -- Update sliding normal from collision
    if CollisionNormalValid:
        SetSlidingNormal(CollisionNormal)  -- XY only, Z zeroed

    -- Gravity fall-through safety (FramesStationaryFall)
    if not IsViewer and HasGravity:
        if not moved:
            if FramesStationaryFall > 0:
                if FramesStationaryFall > 1:
                    -- After 3 frames of no movement with gravity:
                    -- create a synthetic floor under the object
                    FramesStationaryFall = 3
                    syntheticPlane.Normal = UnitZ
                    syntheticPlane.D = GlobalSphere[0].Radius - GlobalSphere[0].Center.Z
                    SetContactPlane(syntheticPlane, false)
                    ContactPlaneCellID = CheckPos.ObjCellID
                    if not Contact:
                        SetCollisionNormal(UnitZ)
                        CollidedWithEnvironment = true
                else:
                    FramesStationaryFall = 2
            else:
                FramesStationaryFall = 1
        else:
            FramesStationaryFall = 0

    -- Preserve contact plane for next step
    LastKnownContactPlaneValid = ContactPlaneValid
    if ContactPlaneValid:
        LastKnownContactPlane = ContactPlane
        LastKnownContactPlaneCellID = ContactPlaneCellID
        LastKnownContactPlaneIsWater = ContactPlaneIsWater
        State |= Contact
        if is_valid_walkable(ContactPlane.Normal):
            State |= OnWalkable
        else:
            State &= ~OnWalkable
    else:
        State &= ~(Contact | OnWalkable)

    return transitionState
```

### Key insight: FramesStationaryFall
This is a safety valve for objects with gravity that get stuck. After 3 consecutive
frames where gravity is active but the object hasn't actually moved, the system
creates a synthetic horizontal floor plane under the object. This prevents objects
from falling through the world indefinitely.

---

## 8. Already Ported

The following functions are already ported in `src/AcDream.Core/Physics/CollisionPrimitives.cs`:

| Function | Decompiled Address | CollisionPrimitives Method |
|----------|-------------------|---------------------------|
| SphereIntersectsRay | FUN_005384e0 | `SphereIntersectsRay()` |
| ray_plane_intersect | FUN_00539060 | `RayPlaneIntersect()` |
| calc_normal | FUN_00539110 | `CalcNormal()` |
| sphere_intersects_poly | FUN_00539500 | `SphereIntersectsPoly()` |
| find_time_of_collision | FUN_00539ba0 | `FindTimeOfCollision()` |
| hits_walkable | FUN_0053a230 | `HitsWalkable()` |
| find_walkable_collision | FUN_0053a040 | `FindWalkableCollision()` |
| slide_sphere (parametric) | FUN_00538eb0 | `SlideSphere()` |
| land_on_sphere | FUN_00538f50 | `LandOnSphere()` |

The following are ported in `src/AcDream.Core/Physics/BSPQuery.cs`:

| Function | BSPQuery Method |
|----------|----------------|
| BSPNode.sphere_intersects_poly (tree traversal) | `BSPQuery.SphereIntersectsPoly()` |

### Not Yet Ported (needed for full transition system)

| ACE Class/Method | Purpose |
|-----------------|---------|
| `Transition` (entire class) | Main orchestrator |
| `SpherePath` | Movement descriptor and sphere caching |
| `CollisionInfo` | Collision result accumulation |
| `ObjectInfo` | Object flags and validation |
| `BSPTree.find_collisions` | Indoor BSP collision dispatcher |
| `BSPTree.step_sphere_down` | BSP step-down walkable search |
| `BSPTree.step_sphere_up` | BSP step-up via Transition.StepUp |
| `BSPTree.placement_insert` | Placement collision with iterative push-out |
| `BSPNode.sphere_intersects_solid` | Solid-region intersection test |
| `BSPNode.sphere_intersects_solid_poly` | Solid-region with polygon identification |
| `BSPNode.find_walkable` | Walkable surface search in BSP |
| `BSPNode.hits_walkable` | Quick walkable probe |
| `Sphere.IntersectsSphere` | Object-vs-object collision |
| `Sphere.StepSphereUp` | Object step-up response |
| `Sphere.StepSphereDown` | Object step-down response |
| `Sphere.SlideSphere` (both variants) | Wall slide projection |
| `Sphere.CollideWithPoint` | PerfectClip collision adjustment |
| `Sphere.LandOnSphere` | Landing on spherical object |
| `LandCell.FindEnvCollisions` | Outdoor terrain collision |
| `EnvCell.FindEnvCollisions` | Indoor BSP collision |
| `ObjCell.FindObjCollisions` | Object-vs-object in a cell |
| `ObjCell.find_cell_list` | Cell enumeration for sphere |
| `LandCell.add_all_outside_cells` | Neighbor cell enumeration |

---

## 9. Key Constants

| Constant | Value | ACE Name | Decompiled Address |
|----------|-------|----------|-------------------|
| EPSILON | 0.0002 | `PhysicsGlobals.EPSILON` | `_DAT_007ca5d8` area |
| EPSILON_SQ | 0.00000004 | `PhysicsGlobals.EpsilonSq` | |
| LandingZ | 0.0871557 | `PhysicsGlobals.LandingZ` | `_DAT_007ca580` |
| FloorZ | 0.6642 | `PhysicsGlobals.FloorZ` | |
| DefaultStepHeight | 0.01 | `PhysicsGlobals.DefaultStepHeight` | |
| Step-down probe | 0.04 | hardcoded in StepUp/TransitionalInsert | `0x3D23D70A` |
| Gravity | -9.8 | `PhysicsGlobals.Gravity` | |
| MaxVelocity | 50.0 | `PhysicsGlobals.MaxVelocity` | |
| DummySphereRadius | 0.1 | `PhysicsGlobals.DummySphereRadius` | |
| Max steps | 30 (retail) / 1000 (ACE) | hardcoded in FindTransitionalPosition | |
| BSP cell epsilon | 0.01 | hardcoded in sphere_intersects_cell_bsp | |
| Placement iterations | 20 | hardcoded in placement_insert | |
| Adjust-to-plane iterations | 15 | hardcoded in adjust_to_plane | |

### ObjectInfoState Flags
```
Contact         = 0x001  -- object is touching a surface
OnWalkable      = 0x002  -- object is on a walkable surface
IsViewer        = 0x004  -- this is the camera
PathClipped     = 0x008  -- PerfectClip movement (missiles)
FreeRotate      = 0x010  -- rotation is set directly, not interpolated
PerfectClip     = 0x040  -- precise collision adjustment
IsImpenetrable  = 0x080
IsPlayer        = 0x100
EdgeSlide       = 0x200  -- precipice edge-slide behavior enabled
IgnoreCreatures = 0x400
```

### TransitionState Summary
```
OK (1)       -- Position accepted. Move succeeded.
Collided (2) -- Hard stop. Cannot pass. Revert to CurPos.
Adjusted (3) -- Position was nudged to resolve penetration. Re-check needed.
Slid (4)     -- Position was projected along a surface. Re-check needed.
Invalid (0)  -- Internal error state. Should not propagate.
```

---

## 10. ACE vs Decompiled Differences

1. **Max steps:** Retail client uses 30 as the maximum number of sub-steps.
   ACE bumped this to 1000 with a comment, likely to handle server-side edge
   cases with large movements. For acdream (a client), 30 is correct.

2. **collide_with_point (FUN_00538180):** The decompiled code at 0x00538180 shows
   the SlideSphere function with explicit struct offset arithmetic. ACE's version
   matches the algorithm but uses cleaner variable naming. The key structure:
   the function computes `cross(collisionNormal, contactPlane.Normal)` to find
   the slide direction, then projects the displacement onto it. Both versions
   handle the parallel-normal degenerate case identically.

3. **FUN_005387c0 (find_collisions at 0x005387C0):** This is labeled as
   `CTransition::find_collisions` in the function map but actually corresponds
   to `Sphere.IntersectsSphere` in ACE — the object-vs-object collision
   dispatcher. The decompiled code shows the same branching structure: check
   ObstructionEthereal/Placement, then StepDown, then CheckWalkable, then
   Contact/OnWalkable paths. The constant at offset 0x1cc corresponds to
   `ObstructionEthereal`, 0x174 to `InsertType`, 0x178 to `StepDown`, etc.

4. **Global constants:** The decompiled code references globals by address
   (`_DAT_007ca5d8`, `_DAT_007938b0`, etc.). These map to:
   - `_DAT_007ca5d8` = EPSILON squared (~1e-8) in some contexts, EPSILON (~2e-4) in others
   - `_DAT_007938b0` = 1.0f (used as `1.0f / dirLenSq` identity)
   - `_DAT_007938b8` = 0.5f (used in half-offset calculations)
   - `_DAT_007938c0` = 1.0f
   - `_DAT_007ca580` = LandingZ (0.0871557)
   - `DAT_00796344` = 0.0f (zero constant)

5. **SetCollisionNormal normalization:** ACE's `CollisionInfo.SetCollisionNormal`
   normalizes and checks for small vectors. The decompiled code (`FUN_004524a0`)
   does the same check. If normalization fails (near-zero vector), ACE sets
   CollisionNormal to Zero; the decompiled code returns 0 to indicate failure.

6. **Edge slide behavior:** ACE's `EdgeSlide` method has additional complexity
   around `PrecipiceSlide` that handles walking off edges of walkable surfaces.
   The decompiled code for this is spread across multiple functions. The
   algorithm finds the crossed edge of the walkable polygon and uses that edge
   normal for the slide direction, preventing the player from walking off cliffs.
