Cross-referenced ACE's Transition.cs, SpherePath.cs, CollisionInfo.cs, BSPTree.cs, BSPNode.cs, LandCell.cs, EnvCell.cs, Sphere.cs against the decompiled retail client (chunk_00530000.c FUN_005387c0, FUN_00538180). Covers the full collision pipeline: - FindTransitionalPosition (step subdivision, main loop) - TransitionalInsert (per-step cell collision + response) - FindEnvCollisions (terrain + indoor BSP paths) - StepUp/StepDown (step height handling) - AdjustOffset/SlideSphere (wall slide projection) - ValidateTransition (post-step validation, FramesStationaryFall safety) Documents which primitives are already ported in CollisionPrimitives.cs and BSPQuery.cs, and catalogs what remains to be implemented. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
43 KiB
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
- Data Structures
- FindTransitionalPosition — main entry point
- TransitionalInsert — per-step collision check
- FindEnvCollisions — BSP query against environment
- StepUp / StepDown — step height handling
- AdjustOffset / SlideSphere — wall slide projection
- ValidateTransition — post-step validation
- Already Ported — what exists in CollisionPrimitives.cs
- Key Constants
- 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
-
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.
-
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. -
FUN_005387c0 (find_collisions at 0x005387C0): This is labeled as
CTransition::find_collisionsin the function map but actually corresponds toSphere.IntersectsSpherein 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 toObstructionEthereal, 0x174 toInsertType, 0x178 toStepDown, etc. -
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 as1.0f / dirLenSqidentity)_DAT_007938b8= 0.5f (used in half-offset calculations)_DAT_007938c0= 1.0f_DAT_007ca580= LandingZ (0.0871557)DAT_00796344= 0.0f (zero constant)
-
SetCollisionNormal normalization: ACE's
CollisionInfo.SetCollisionNormalnormalizes 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. -
Edge slide behavior: ACE's
EdgeSlidemethod has additional complexity aroundPrecipiceSlidethat 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.