345 lines
15 KiB
Markdown
345 lines
15 KiB
Markdown
# Slice I collision layout and scratch oracle
|
|
|
|
**Status:** I0 fixed evidence
|
|
**Scope:** storage layout, traversal order, scratch lifetime, production
|
|
retention, and allocation baseline for
|
|
[`2026-07-25-modern-runtime-slice-i.md`](../plans/2026-07-25-modern-runtime-slice-i.md).
|
|
This note does not authorize a collision-math change.
|
|
|
|
## 1. Sources and authority
|
|
|
|
Named retail is the behavior oracle:
|
|
|
|
| Mechanism | Retail symbol | Address | Named pseudo-C |
|
|
|---|---|---:|---:|
|
|
| scratch acquisition | `CTransition::makeTransition` | `0x0050B150` | 272873 |
|
|
| scratch release | `CTransition::cleanupTransition` | `0x00509DC0` | 271938 |
|
|
| transition reset | `CTransition::init` | `0x00509DD0` | 271946 |
|
|
| collision reset | `COLLISIONINFO::init` | `0x00509D60` | 271910 |
|
|
| path reset | `SPHEREPATH::init` | `0x0050C330` | 273903 |
|
|
| path initialization | `SPHEREPATH::init_path` | `0x0050CE20` | 274359 |
|
|
| movement loop | `CTransition::find_transitional_position` | `0x0050BDF0` | 273613 |
|
|
| object entry point | `CPhysicsObj::transition` | `0x00512DC0` | 280904 |
|
|
| six-path BSP dispatch | `BSPTREE::find_collisions` | `0x0053A440` | 323725 |
|
|
| cell point predicate | `BSPNODE::point_inside_cell_bsp` | `0x0053C1F0` | 325508 |
|
|
| cell sphere predicate | `BSPNODE::sphere_intersects_cell_bsp` | `0x0053C260` | 325546 |
|
|
| walkable internal node | `BSPNODE::find_walkable` | `0x0053CC80` | 326211 |
|
|
| walkable leaf | `BSPLEAF::find_walkable` | `0x0053D6F0` | 326793 |
|
|
| polygon overlap | `BSPNODE::sphere_intersects_poly` | `0x0053CA30` | 326091 |
|
|
| solid overlap | `BSPNODE::sphere_intersects_solid` | `0x0053CAF0` | 326130 |
|
|
| solid/polygon overlap | `BSPNODE::sphere_intersects_solid_poly` | `0x0053CD50` | 326246 |
|
|
|
|
Interpretation aids:
|
|
|
|
- ACE `master` commit
|
|
`65f092dd02505c04532b701c48d11af117cbc815`:
|
|
`Physics/Transition.cs`, `SpherePath.cs`, `ObjectInfo.cs`,
|
|
`Collision/CollisionInfo.cs`, and `Physics/BSP/{BSPTree,BSPNode,BSPLeaf}.cs`.
|
|
- Chorizite DatReaderWriter `master` commit
|
|
`c5359870963fecb55c9b90e8143a92fc7fe88e11`; acdream consumes package
|
|
`2.1.7`. Its `PhysicsBSPNode` and `CellBSPNode` unpackers establish the
|
|
source child-presence and stream order.
|
|
- Existing acdream translations:
|
|
[`transition_pseudocode.md`](transition_pseudocode.md),
|
|
[`2026-06-24-obstruction-ethereal-pseudocode.md`](2026-06-24-obstruction-ethereal-pseudocode.md),
|
|
[`2026-07-05-ccylsphere-collision-family-pseudocode.md`](2026-07-05-ccylsphere-collision-family-pseudocode.md),
|
|
[`2026-07-07-csphere-collision-family-pseudocode.md`](2026-07-07-csphere-collision-family-pseudocode.md),
|
|
and the physics digest.
|
|
|
|
Retail wins if an interpretation aid disagrees. Slice I keeps the current graph
|
|
implementation executable until every flat query is compared against it. A
|
|
pre-existing graph-vs-retail mismatch, if found, is adjudicated separately; it
|
|
must not be hidden inside the layout cutover.
|
|
|
|
## 2. Retail scratch lifetime
|
|
|
|
`CTransition::makeTransition` is not a heap constructor per movement call:
|
|
|
|
```text
|
|
on first use:
|
|
construct static CTransition transit[10]
|
|
|
|
level = transition_level
|
|
if level >= 10:
|
|
return null
|
|
|
|
transition = &transit[level]
|
|
CTransition::init(transition)
|
|
transition_level++
|
|
return transition
|
|
```
|
|
|
|
`CTransition::cleanupTransition` only decrements `transition_level`. This is a
|
|
ten-deep, stack-disciplined scratch arena. Nested transitions receive distinct
|
|
records and release in LIFO order. The object graph and embedded arrays retain
|
|
their storage identity; `init` resets the logical state before each lease.
|
|
|
|
Retail reset pseudocode:
|
|
|
|
```text
|
|
CTransition::init:
|
|
object_info.object = null
|
|
object_info.state = 0
|
|
object_info.targetID = 0
|
|
SPHEREPATH::init(sphere_path)
|
|
collision_info.last_known_contact_plane_valid = false
|
|
collision_info.contact_plane_valid = false
|
|
collision_info.sliding_normal_valid = false
|
|
collision_info.collision_normal_valid = false
|
|
collision_info.num_collide_object = 0
|
|
clear last_collided_object and adjacent collision result fields
|
|
collision_info.contact_plane_cell_id = 0
|
|
|
|
SPHEREPATH::init:
|
|
num_sphere = 0
|
|
begin_cell/begin_pos/curr_cell/check_cell = null
|
|
insert_type = TRANSITION
|
|
step_down/step_up/collide = false
|
|
hits_interior_cell/bldg_check/obstruction_ethereal = false
|
|
backup_cell = null
|
|
walkable_allowance = 0
|
|
walkable = null
|
|
check_walkable/cell_array_valid/neg_step_up/neg_poly_hit = false
|
|
placement_allows_sliding = true
|
|
```
|
|
|
|
The acdream reset must cover additional modern representations of the same
|
|
logical state: cell IDs instead of pointers, orientations, carried block
|
|
origin, retained vertex buffers, diagnostic counters, GUID lists, target and
|
|
self IDs, physics-state flags, water bits, and value-type planes/vectors.
|
|
Retained storage may survive only if its logical contents are reset completely.
|
|
|
|
## 3. Immutable source storage
|
|
|
|
### Physics BSP
|
|
|
|
The source graph fields consumed by collision are:
|
|
|
|
```text
|
|
PhysicsBSPNode:
|
|
Type
|
|
SplittingPlane
|
|
PosNode, NegNode
|
|
LeafIndex
|
|
Solid
|
|
BoundingSphere (origin + radius)
|
|
Polygons[] (ordered ushort ids)
|
|
```
|
|
|
|
DatReaderWriter decodes child presence from `BSPNodeType`. When both children
|
|
exist, positive is read before negative. A flat representation therefore uses
|
|
explicit signed child indices (`-1` = absent) and preserves source preorder:
|
|
node, positive subtree, negative subtree.
|
|
|
|
### Cell-containment BSP
|
|
|
|
```text
|
|
CellBSPNode:
|
|
Type
|
|
SplittingPlane
|
|
PosNode, NegNode
|
|
LeafIndex
|
|
```
|
|
|
|
Containment trees do not carry physics leaf polygons or bounding spheres. They
|
|
must remain a separate schema from the physics BSP rather than wasting fields
|
|
or accidentally applying physics-node traversal rules.
|
|
|
|
### Resolved polygons
|
|
|
|
Runtime physics reads:
|
|
|
|
```text
|
|
ResolvedPolygon:
|
|
Id
|
|
Plane (normal + D)
|
|
SidesType
|
|
NumPoints
|
|
Vertices[] in source order
|
|
```
|
|
|
|
The flat asset stores one polygon table and one contiguous vertex stream.
|
|
Every polygon has an explicit `(vertexStart, vertexCount)` and every leaf has an
|
|
explicit `(polygonIndexStart, polygonIndexCount)`. Polygon IDs are resolved to
|
|
indices during preparation. Missing IDs are corrupt prepared content, never a
|
|
runtime dictionary fallback.
|
|
|
|
Every float is copied through its `SingleToInt32Bits` representation. Planes
|
|
are copied, not reconstructed from vertices.
|
|
|
|
### Setup and cell payload
|
|
|
|
`SetupPhysics` consumes ordered `CylSpheres`, ordered `Spheres`, `Height`,
|
|
`Radius`, `StepUpHeight`, and `StepDownHeight`.
|
|
|
|
`CellPhysics` currently combines immutable geometry with publication state:
|
|
physics BSP, resolved physics polygons, cell-containment BSP, world and inverse
|
|
world transforms, ordered portals, resolved portal polygons, visible-cell IDs,
|
|
and `SeenOutside`. The transforms and topology are per EnvCell placement.
|
|
Immutable CellStruct collision geometry may alias only when its bytes and
|
|
source identity agree; placement/topology may not be aliased merely because
|
|
geometry matches.
|
|
|
|
## 4. Load-bearing traversal order
|
|
|
|
### Walkable search
|
|
|
|
```text
|
|
find_walkable(node, path, mutableSphere, hitPoly, movement, up, changed):
|
|
if node.boundingSphere does not intersect mutableSphere:
|
|
return
|
|
|
|
distance = signedDistance(node.splittingPlane, mutableSphere.center)
|
|
threshold = mutableSphere.radius - 0.0002
|
|
|
|
if distance >= threshold:
|
|
visit positive only
|
|
else if distance <= -threshold:
|
|
visit negative only
|
|
else:
|
|
visit positive
|
|
visit negative
|
|
|
|
find_walkable(leaf, ...):
|
|
if no polygons or leaf bounds miss:
|
|
return
|
|
for polygon in leaf.polygons IN STORED ORDER:
|
|
if polygon.walkable_hits_sphere(...)
|
|
and polygon.adjust_sphere_to_plane(...):
|
|
changed = true
|
|
hitPoly = polygon
|
|
```
|
|
|
|
The second child sees the sphere position mutated by the first child. A later
|
|
successful polygon replaces `hitPoly`. Consequently child order, leaf order,
|
|
and reference-style sphere mutation are behavior, not implementation detail.
|
|
|
|
### Six-path collision dispatch
|
|
|
|
`BSPTREE::find_collisions` evaluates in this order:
|
|
|
|
1. placement or obstruction-ethereal: solid test, primary sphere then secondary;
|
|
2. `check_walkable`;
|
|
3. `step_down`;
|
|
4. pending `collide`: walkable search and adjustment;
|
|
5. existing contact: primary collision and step-up, then secondary slide/negative hit;
|
|
6. airborne/default: primary collision/set-collide, then secondary hard collision.
|
|
|
|
The dispatcher observes and mutates `SpherePath`, `ObjectInfo`, and
|
|
`CollisionInfo`. The flat port must preserve every early return and the
|
|
primary-before-secondary order.
|
|
|
|
### Cell containment
|
|
|
|
Point and radius-aware containment are distinct entry points.
|
|
`sphere_intersects_cell_bsp` expands the radius by exactly `0.01`. Null-child,
|
|
on-plane, and near-plane behavior is pinned by the current conformance tests and
|
|
must be compared bit-for-bit during I4. No generic “BSP traversal helper” may
|
|
merge containment, walkable, solid, and collision traversal; their null-child
|
|
and return semantics differ.
|
|
|
|
## 5. Mutable query state and returned side effects
|
|
|
|
The query scratch is:
|
|
|
|
- `ObjectInfo`: state, step heights, scale, ethereal/step-down state, mover
|
|
physics state, target ID, self ID, gravity state, and velocity-killed result.
|
|
- `CollisionInfo`: live and last-known contact planes including cell/water
|
|
metadata, sliding and collision normals, environment hit, stationary-fall
|
|
count, adjustment offset, collision GUID list, last collision GUID, and
|
|
diagnostic write count.
|
|
- `SpherePath`: all local/global/current spheres, four positions and
|
|
orientations, current/check/backup cells, carried block origin, movement
|
|
offset, step-up/down state, mutable current/last walkable state, negative-hit
|
|
state, insertion mode, placement policy, and building/interior/ethereal flags.
|
|
- per-query `CollisionSphere` values used as reference-mutated valid positions.
|
|
|
|
Returned behavior includes more than `ResolveResult`: body contact and
|
|
last-contact state, walkable polygon, stationary-fall/transient flags, sliding
|
|
normal, killed velocity, selected hit polygon, collision GUIDs, and swept cell
|
|
membership. Differential tests must compare these side effects as well as the
|
|
boolean/enum return and final position.
|
|
|
|
## 6. Production graph-call inventory
|
|
|
|
| Entry point | Production consumers |
|
|
|---|---|
|
|
| `PointInsideCellBsp` | `World/Cells/EnvCell`; `CellTransit` point/portal membership; `Transition.CheckOtherCells` diagnostics and admission |
|
|
| `SphereIntersectsCellBsp` | `CellTransit` candidate, current-cell, and transit membership; `Transition.CheckOtherCells` |
|
|
| `FindCrossedEdge` | precipice slide and nearest-inside-edge handling in `SpherePath`/`Transition` |
|
|
| `FindWalkableSphere` | indoor walkable-plane probe in `Transition.TryFindIndoorWalkablePlane` |
|
|
| `FindCollisions` | EnvCell physics BSP, static/multipart GfxObj BSP, and building-shell BSP in `Transition` |
|
|
|
|
The legacy dictionary/`VertexArray` overloads and static
|
|
`SphereIntersectsPoly*` entry points currently serve tests and diagnostics, not
|
|
the production movement call graph. I6 removes them from production ownership
|
|
only after their required tooling users are explicit.
|
|
|
|
## 7. Parsed graph retention inventory
|
|
|
|
The current worker `LandblockBuildFactory` creates a `PhysicsDatBundle` holding
|
|
raw `LandBlockInfo`, `EnvCell`, `Environment`, `Setup`, and `GfxObj` DBObjs in
|
|
per-build dictionaries. It also opportunistically populates
|
|
`PhysicsDataCache` while gathering entity and scenery bounds.
|
|
|
|
On the update thread, `LandblockPhysicsPublisher`:
|
|
|
|
1. resolves EnvCell → Environment → CellStruct from that raw bundle;
|
|
2. calls `CacheCellStruct`, retaining physics BSP, containment BSP, raw polygon
|
|
dictionaries/vertex arrays, resolved polygons, and topology;
|
|
3. calls `CacheGfxObj`, retaining GfxObj physics BSP, raw polygon/vertex graphs,
|
|
resolved polygons, and bounds;
|
|
4. reads Setup graphs for collision shapes, buildings, and multipart statics.
|
|
|
|
`LandblockStaticPresentationPublisher` also reads raw Setup light definitions.
|
|
`LandblockStreamResultCost` currently counts bundle dictionary entries but not
|
|
the complete transitive DBObj graph. I3 must replace collision publication with
|
|
prepared immutable payloads without removing the separately required render or
|
|
lighting data.
|
|
|
|
## 8. Fixed fixture ledger
|
|
|
|
| Required shape | Existing fixed evidence |
|
|
|---|---|
|
|
| outdoor/no BSP | `PhysicsEngineTests`, `CellMarchLandblockPreservationTests`, projectile terrain fixtures |
|
|
| Facility Hub indoor | `Issue137CorridorSeamReplayTests` (`0x8A02016E↔0x8A02017A`), `Issue180CorridorSweepHysteresisReplayTests` (`0x8A020164`) |
|
|
| Holtburg cottage indoor | `ThresholdPortalCrossingReplayTests`, `CameraCornerSealReplayTests`, `Fixtures/flap-doorway/*` |
|
|
| staircase/ramp | `BSPStepUpFixtures`, `Issue185OutdoorStairsSeamReplayTests` |
|
|
| cellar lip | `CellarLipWedgeTests`, `CellarUpTrajectoryReplayTests`, `Fixtures/cellar-lip/*`, `Fixtures/issue98/*` |
|
|
| multipart door/building shell | `DoorCollisionApparatusTests`, `DoorBugTrajectoryReplayTests`, GfxObj fixture `0x01000A2B` |
|
|
| projectile/thin wall | `ProjectilePhysicsStepperTests.RegisterThinBspWall` |
|
|
| asymmetric/null children | `SphereIntersectsCellBspTests`, `PhysicsEngineAdjustPositionTests`, real DAT containment fixtures |
|
|
| multi-polygon/equal candidate order | `BSPQueryTests`, `IndoorWalkablePlaneTests`, `BSPStepUpFixtures`; I2 adds explicit flat-order structural cases before flattening gates |
|
|
|
|
I0 graph-path fixture command:
|
|
|
|
```powershell
|
|
dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Release --no-build `
|
|
--filter "FullyQualifiedName~BSPQueryTests|FullyQualifiedName~SphereIntersectsCellBspTests|FullyQualifiedName~CellarLipWedgeTests|FullyQualifiedName~CellarUpTrajectoryReplayTests|FullyQualifiedName~CameraCornerSealReplayTests|FullyQualifiedName~ProjectilePhysicsStepperTests|FullyQualifiedName~DoorCollisionApparatusTests|FullyQualifiedName~Issue137CorridorSeamReplayTests|FullyQualifiedName~ThresholdPortalCrossingReplayTests|FullyQualifiedName~Issue185OutdoorStairsSeamReplayTests|FullyQualifiedName~CylSphereFamilyTests|FullyQualifiedName~SphereCollisionFamilyTests|FullyQualifiedName~TransitionAllocationBaselineTests"
|
|
```
|
|
|
|
Result on 2026-07-25: **111 passed, 0 skipped, 0 failed** against the current
|
|
object-graph route and installed retail DATs.
|
|
|
|
## 9. Allocation baseline
|
|
|
|
`TransitionAllocationBaselineTests` warms 256 calls, measures 4,096 Release
|
|
resolves on one thread, and exercises a fixed BSP wall through the four
|
|
production mover call shapes:
|
|
|
|
| Mover | Current graph path |
|
|
|---|---:|
|
|
| player, two spheres | 4,448 B/resolve |
|
|
| remote, two spheres | 4,448 B/resolve |
|
|
| projectile, one 5 cm sphere | 6,848 B/resolve |
|
|
| camera/viewer, one 30 cm sphere | 2,512 B/resolve |
|
|
|
|
The projectile crosses the same distance using a much smaller sphere, causing
|
|
more retail substeps and therefore more per-step query-scratch allocation.
|
|
This is why allocation must be measured by mover family rather than quoting one
|
|
average.
|
|
|
|
I1's gate is zero steady allocation attributable to transition construction,
|
|
walkable buffer cloning, and `CollisionSphere` query scratch after warmup.
|
|
It does not claim that every optional diagnostic/capture path is allocation
|
|
free.
|