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