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>
1185 lines
43 KiB
Markdown
1185 lines
43 KiB
Markdown
# 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.
|