research: indoor transition pseudocode from ACE + decompiled analysis
Sprint 2 research for indoor transitions. Documents: - ACE EnvCell.find_transit_cells: sphere-plane + BSP containment - ACE SortCell/BuildingObj.check_building_transit: outdoor→indoor - PortalSide semantics and portal polygon plane testing - Gap analysis: acdream needs CellBSP, BldPortal list, VisibleCells Key finding: full accuracy requires CellBSP (physics BSP from dat) for sphere_intersects_cell. Current PortalPlane.IsCrossing is a valid approximation. ACME's AABB PointInCell is an intermediate option. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64b1fcb31e
commit
4782532c4b
1 changed files with 428 additions and 0 deletions
428
docs/research/acclient_indoor_transitions_pseudocode.md
Normal file
428
docs/research/acclient_indoor_transitions_pseudocode.md
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# Indoor Cell Transitions — Research & Pseudocode
|
||||
|
||||
**Date:** 2026-04-13
|
||||
**Sources cross-referenced:**
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/EnvCell.cs` — primary ACE oracle
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/ObjCell.cs` — `find_cell_list`
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/LandCell.cs` — `add_all_outside_cells`
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/BuildingObj.cs` — surface building path
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/BldPortal.cs` — PortalFlags
|
||||
- `references/ACE/Source/ACE.Server/Physics/Common/CellStruct.cs` — `sphere_intersects_cell`, `box_intersects_cell`
|
||||
- `references/WorldBuilder-ACME-Edition/WorldBuilder/Editors/Landscape/EnvCellManager.cs` — ACME's editor implementation: `FindCameraCell`, `PointInCell`, `GetVisibleCells`
|
||||
- `docs/research/decompiled/chunk_00530000.c` — the 0x531A00-0x532440 range: data structure layout ops (serialize/deserialize), NOT transit cell logic
|
||||
- `references/AC2D/cWObject.cpp` — AC2D does NOT implement client-side cell transitions (sends keys to server, uses server Z)
|
||||
- `references/holtburger/` — holtburger is TUI only, no spatial physics
|
||||
|
||||
---
|
||||
|
||||
## Terminology
|
||||
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| **CellPortal** / `DatLoader.Entity.CellPortal` | A record in an EnvCell's `.CellPortals` list. Contains `PolygonId` (the portal polygon in the CellStruct), `OtherCellId` (low-16 of the destination cell, 0xFFFF = outdoor), and `Flags` (PortalSide bit). |
|
||||
| **PortalSide** | `true` = the outside-facing normal of the portal polygon points INTO the cell (you enter by crossing from the negative-dist side). `false` = the opposite. Derived from `Flags & 2` in the dat. |
|
||||
| **CellStruct.Polygons** | Dictionary of physics + rendering polygons keyed by polygon ID. Each polygon has a `Plane` (Normal + D). |
|
||||
| **CellStruct.CellBSP** | BSP tree used for `sphere_intersects_cell` and `box_intersects_cell` and `point_in_cell`. |
|
||||
| **EnvCell.VisibleCells** | Pre-built from `VisibleCellIDs` list: all cells visible from this one. Used for neighbor lookups. |
|
||||
| **BldPortal** | A portal used for above-ground buildings attached to LandCells via `SortCell`. Uses `PortalFlags`, `ExactMatch`, `PortalSide`, `OtherCellId`, `OtherPortalId`. |
|
||||
| **SortCell** | An outdoor landblock cell that may contain a `BuildingObj` (e.g. the Holtburg inn). Delegates transit detection to `BuildingObj.find_building_transit_cells`. |
|
||||
| **EPSILON** | `PhysicsGlobals.EPSILON` — used as sphere-radius padding in tests. In our codebase, use a small value like `0.02f`. |
|
||||
|
||||
---
|
||||
|
||||
## Overall Driver: `find_cell_list` (ObjCell.cs, static)
|
||||
|
||||
This is the top-level function called each movement tick. It builds the list of cells the entity currently occupies or is transitioning into.
|
||||
|
||||
```pseudocode
|
||||
find_cell_list(position, numSphere, spheres[], cellArray, ref currCell, path):
|
||||
cellArray.Clear()
|
||||
|
||||
visibleCell = GetVisible(position.ObjCellID) // looks up the cell from the dat
|
||||
|
||||
if position is indoor (ObjCellID & 0xFFFF >= 0x100):
|
||||
if path != null: path.HitsInteriorCell = true
|
||||
cellArray.add_cell(position.ObjCellID, visibleCell)
|
||||
else:
|
||||
LandCell.add_all_outside_cells(position, numSphere, spheres, cellArray)
|
||||
|
||||
if visibleCell != null AND numSphere > 0:
|
||||
// For each cell already in the array, ask it to find neighbors the sphere touches
|
||||
for each cell in cellArray (snapshot — cells may be added during iteration):
|
||||
cell.find_transit_cells(position, numSphere, spheres, cellArray, path)
|
||||
|
||||
// Determine which cell the sphere center is actually inside
|
||||
if currCell != null:
|
||||
currCell = null
|
||||
for each cell in cellArray:
|
||||
blockOffset = GetBlockOffset(position.ObjCellID, cell.ID)
|
||||
localPoint = spheres[0].Center - blockOffset
|
||||
if cell.point_in_cell(localPoint):
|
||||
currCell = cell
|
||||
if cell is indoor: path.HitsInteriorCell = true
|
||||
break
|
||||
|
||||
// Visibility filter: when indoors, strip cells that aren't in VisibleCells
|
||||
if NOT cellArray.LoadCells AND position is indoor:
|
||||
for each cell in cellArray (copy):
|
||||
if cell == visibleCell: continue
|
||||
if cell not in ((EnvCell)visibleCell).VisibleCells:
|
||||
cellArray.remove_cell(cell)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EnvCell.find_transit_cells (sphere variant)
|
||||
|
||||
Called from `find_cell_list` for each cell in the array. Detects which neighboring cells the spheres may have entered.
|
||||
|
||||
**Source:** `EnvCell.cs:311-371` (the sphere overload)
|
||||
|
||||
```pseudocode
|
||||
EnvCell.find_transit_cells(position, numSphere, spheres[], cellArray, path):
|
||||
checkOutside = false
|
||||
|
||||
for each portal in this.Portals:
|
||||
portalPoly = this.CellStructure.Polygons[portal.PolygonId]
|
||||
// portalPoly.Plane = (Normal, D) in cell-local space
|
||||
|
||||
if portal.OtherCellId == 0xFFFF: // exit to outdoor
|
||||
// Test each sphere for proximity to the portal plane
|
||||
for each sphere in spheres:
|
||||
rad = sphere.Radius + EPSILON
|
||||
center = this.Pos.Frame.GlobalToLocal(sphere.Center) // world → cell-local
|
||||
dist = Dot(center, portalPoly.Plane.Normal) + portalPoly.Plane.D
|
||||
|
||||
if dist > -rad AND dist < rad: // sphere straddles the plane
|
||||
checkOutside = true
|
||||
break // one sphere is enough for this portal
|
||||
else:
|
||||
otherCell = this.GetVisible(portal.OtherCellId) // look up in VisibleCells dict
|
||||
|
||||
if otherCell != null:
|
||||
// Convert sphere center to otherCell's local space and test containment
|
||||
for each sphere in spheres:
|
||||
center = otherCell.Pos.Frame.GlobalToLocal(sphere.Center)
|
||||
localSphere = Sphere(center, sphere.Radius)
|
||||
boundType = otherCell.CellStructure.sphere_intersects_cell(localSphere)
|
||||
if boundType != Outside:
|
||||
cellArray.add_cell(otherCell.ID, otherCell)
|
||||
break
|
||||
else:
|
||||
// otherCell not yet loaded: use plane-side test for load hint
|
||||
for each sphere in spheres:
|
||||
center = this.Pos.Frame.GlobalToLocal(sphere.Center)
|
||||
localSphere = Sphere(center, sphere.Radius + EPSILON)
|
||||
dist = Dot(localSphere.Center, portalPoly.Plane.Normal) + portalPoly.Plane.D
|
||||
portalSide = portal.PortalSide
|
||||
|
||||
// Sphere is on the "exit" side of the portal
|
||||
if (dist > -localSphere.Radius AND portalSide) OR
|
||||
(dist < localSphere.Radius AND NOT portalSide):
|
||||
cellArray.add_cell(portal.OtherCellId, null) // null = load hint
|
||||
break
|
||||
|
||||
if checkOutside:
|
||||
LandCell.add_all_outside_cells(position, numSphere, spheres, cellArray)
|
||||
```
|
||||
|
||||
### Key geometry: `sphere_intersects_cell`
|
||||
|
||||
`CellStruct.sphere_intersects_cell(sphere)` calls `CellBSP.sphere_intersects_cell_bsp(sphere)`.
|
||||
The BSP tree is built from the cell's physics polygons. The result is one of:
|
||||
- `Inside` — sphere entirely inside the cell volume
|
||||
- `Crossing` — sphere straddles a cell boundary plane
|
||||
- `Outside` — sphere entirely outside
|
||||
|
||||
For transit detection, `Inside` and `Crossing` both qualify ("not Outside").
|
||||
|
||||
---
|
||||
|
||||
## EnvCell.find_transit_cells (parts/AABB variant)
|
||||
|
||||
Used when the entity is represented by physics parts (creatures, large objects) rather than spheres. The bounding box of each part is tested instead of a sphere.
|
||||
|
||||
**Source:** `EnvCell.cs:245-309`
|
||||
|
||||
```pseudocode
|
||||
EnvCell.find_transit_cells(numParts, parts[], cellArray):
|
||||
checkOutside = false
|
||||
|
||||
for each portal in this.Portals:
|
||||
portalPoly = this.CellStructure.Polygons[portal.PolygonId]
|
||||
|
||||
for each part in parts:
|
||||
if part == null: continue
|
||||
sphere = part.GfxObj.PhysicsSphere ?? part.GfxObj.DrawingSphere
|
||||
if sphere == null: continue
|
||||
|
||||
// Convert sphere center to this cell's local space
|
||||
center = this.Pos.LocalToLocal(part.Pos, sphere.Center)
|
||||
rad = sphere.Radius + EPSILON
|
||||
|
||||
// Broad phase: sphere-plane distance test
|
||||
dist = Dot(center, portalPoly.Plane.Normal) + portalPoly.Plane.D
|
||||
if portal.PortalSide:
|
||||
if dist < -rad: continue // sphere fully behind portal (wrong side)
|
||||
else:
|
||||
if dist > rad: continue // sphere fully in front of portal (wrong side)
|
||||
|
||||
// Narrow phase: AABB intersection
|
||||
bbox = part.GetBoundingBox()
|
||||
box = BBox.LocalToLocal(bbox, part.Pos, this.Pos)
|
||||
sidedness = portalPoly.Plane.intersect_box(box)
|
||||
if (sidedness == Positive AND NOT portal.PortalSide) OR
|
||||
(sidedness == Negative AND portal.PortalSide):
|
||||
continue // box fully on wrong side, skip
|
||||
|
||||
// At this point the part potentially crosses the portal
|
||||
if portal.OtherCellId == 0xFFFF:
|
||||
checkOutside = true
|
||||
break
|
||||
|
||||
otherCell = this.GetVisible(portal.OtherCellId)
|
||||
if otherCell == null:
|
||||
cellArray.add_cell(portal.OtherCellId, null) // load hint
|
||||
break
|
||||
|
||||
cellBox = BBox.LocalToLocal(bbox, part.Pos, otherCell.Pos)
|
||||
if otherCell.CellStructure.box_intersects_cell(cellBox):
|
||||
cellArray.add_cell(otherCell.ID, otherCell)
|
||||
break
|
||||
|
||||
if checkOutside:
|
||||
LandCell.add_all_outside_cells(numParts, parts, cellArray, this.ID)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LandCell.find_transit_cells (outdoor → indoor entry)
|
||||
|
||||
When the entity is outdoors, `LandCell.find_transit_cells` runs. It first calls
|
||||
`add_all_outside_cells` to include neighboring outdoor cells, then delegates to
|
||||
`SortCell.find_transit_cells` which in turn calls `BuildingObj.find_building_transit_cells`
|
||||
if a building is attached.
|
||||
|
||||
**Source:** `LandCell.cs:271-281`, `SortCell.cs:33-43`
|
||||
|
||||
```pseudocode
|
||||
LandCell.find_transit_cells(position, numSphere, spheres[], cellArray, path):
|
||||
add_all_outside_cells(position, numSphere, spheres, cellArray)
|
||||
// then: SortCell.find_transit_cells → BuildingObj.find_building_transit_cells
|
||||
|
||||
BuildingObj.find_building_transit_cells(pos, numSphere, spheres[], cellArray, path):
|
||||
for each bldPortal in this.Portals:
|
||||
otherCell = bldPortal.GetOtherCell(CurCell.ID) // resolve EnvCell from OtherCellId
|
||||
if otherCell != null:
|
||||
otherCell.check_building_transit(bldPortal.OtherPortalId, pos, numSphere, spheres, cellArray, path)
|
||||
```
|
||||
|
||||
### EnvCell.check_building_transit (the entry-point test)
|
||||
|
||||
```pseudocode
|
||||
EnvCell.check_building_transit(portalId, pos, numSphere, spheres[], cellArray, path):
|
||||
if portalId == 0xFFFF: return // invalid portal
|
||||
|
||||
for each sphere in spheres:
|
||||
// Convert sphere center to this cell's local space
|
||||
globSphere = Sphere(this.Pos.Frame.GlobalToLocal(sphere.Center), sphere.Radius)
|
||||
|
||||
// Test if the sphere is inside this cell's geometry
|
||||
if this.CellStructure.sphere_intersects_cell(globSphere) == Outside:
|
||||
continue // sphere not inside this cell, skip
|
||||
|
||||
// Sphere IS inside the cell
|
||||
if path != null: path.HitsInteriorCell = true
|
||||
cellArray.add_cell(this.ID, this)
|
||||
// Note: no recursive portal search here — this is just the entry check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LandCell.add_all_outside_cells (sphere variant)
|
||||
|
||||
Determines which outdoor landblock cells an entity's spheres overlap. Each outdoor
|
||||
cell is a 24×24m square. The function adds the primary cell plus up to 3 neighbors
|
||||
when the sphere radius reaches a boundary.
|
||||
|
||||
**Source:** `LandCell.cs:82-118`
|
||||
|
||||
```pseudocode
|
||||
add_all_outside_cells(position, numSphere, spheres[], cellArray):
|
||||
if cellArray.AddedOutside: return
|
||||
|
||||
for each sphere in spheres:
|
||||
cellPoint = position.ObjCellID
|
||||
center = sphere.Center
|
||||
AdjustToOutside(ref cellPoint, ref center) // normalise to correct block
|
||||
|
||||
// Within the 24m×24m cell, compute local 2D coords
|
||||
point.X = center.X mod 24.0f
|
||||
point.Y = center.Y mod 24.0f
|
||||
minRad = sphere.Radius
|
||||
maxRad = 24.0f - minRad
|
||||
|
||||
lcoord = gid_to_lcoord(cellPoint) // world cell ID → grid coordinates
|
||||
add_outside_cell(cellArray, lcoord)
|
||||
check_add_cell_boundary(cellArray, point, lcoord, minRad, maxRad)
|
||||
|
||||
// check_add_cell_boundary: add neighbouring cells when sphere crosses cell edges
|
||||
check_add_cell_boundary(cellArray, point, lcoord, minRad, maxRad):
|
||||
x, y = lcoord
|
||||
if point.X > maxRad:
|
||||
add_outside_cell(cellArray, x+1, y)
|
||||
if point.Y > maxRad: add_outside_cell(cellArray, x+1, y+1)
|
||||
if point.Y < minRad: add_outside_cell(cellArray, x+1, y-1)
|
||||
if point.X < minRad:
|
||||
add_outside_cell(cellArray, x-1, y)
|
||||
if point.Y > maxRad: add_outside_cell(cellArray, x-1, y+1)
|
||||
if point.Y < minRad: add_outside_cell(cellArray, x-1, y-1)
|
||||
if point.Y > maxRad: add_outside_cell(cellArray, x, y+1)
|
||||
if point.Y < minRad: add_outside_cell(cellArray, x, y-1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PortalSide flag semantics
|
||||
|
||||
`PortalSide` is the single most confusing flag in the dat. Here's the exact semantics:
|
||||
|
||||
```
|
||||
portal.PortalSide = (cp.Flags & 2) == 0 ? 1 : 0 (ACME EnvCellManager.cs:379)
|
||||
|
||||
In ACE EnvCell.find_transit_cells (sphere):
|
||||
dist = Dot(center_in_cell_local, portalPoly.Normal) + portalPoly.D
|
||||
|
||||
if portal.OtherCellId == 0xFFFF (outdoor exit):
|
||||
// sphere straddling counts regardless of PortalSide
|
||||
trigger when: dist > -rad AND dist < rad
|
||||
|
||||
if otherCell != null (known neighbor):
|
||||
// use sphere_intersects_cell in otherCell's local space
|
||||
// PortalSide is NOT checked for sphere variant — containment in other cell is the test
|
||||
|
||||
if otherCell == null (load hint):
|
||||
if portalSide: trigger when dist > -localSphere.Radius
|
||||
if NOT portalSide: trigger when dist < localSphere.Radius
|
||||
|
||||
In ACE EnvCell.find_transit_cells (AABB):
|
||||
// Broad phase sphere-plane test:
|
||||
if portal.PortalSide: skip if dist < -rad (sphere fully behind negative half)
|
||||
if NOT PortalSide: skip if dist > rad (sphere fully in positive half)
|
||||
```
|
||||
|
||||
**Intuition:** PortalSide tells you which half-space of the portal plane is the "inside" of the cell. When PortalSide=true the cell sits on the positive side of the portal plane (dist > 0 is "in the cell"). When crossing into a neighbour cell, an entity must be transitioning from the current cell's side toward the portal, i.e. moving toward dist=0.
|
||||
|
||||
---
|
||||
|
||||
## point_in_cell — two implementations
|
||||
|
||||
### ACE / physics-accurate (CellStruct.CellBSP)
|
||||
|
||||
```pseudocode
|
||||
EnvCell.point_in_cell(worldPoint):
|
||||
localPoint = this.Pos.Frame.GlobalToLocal(worldPoint)
|
||||
return this.CellStructure.CellBSP.point_inside_cell_bsp(localPoint)
|
||||
```
|
||||
|
||||
Requires the full BSP tree from the dat. Exact for any convex or near-convex cell shape.
|
||||
|
||||
### ACME editor (AABB in cell-local space)
|
||||
|
||||
```pseudocode
|
||||
PointInCell(worldPoint, loadedCell):
|
||||
EPSILON = 0.01f
|
||||
localPoint = Transform(worldPoint, loadedCell.InverseWorldTransform)
|
||||
return localPoint is inside loadedCell.LocalBoundsMin..LocalBoundsMax (with EPSILON)
|
||||
```
|
||||
|
||||
ACME precomputes an AABB from all CellStruct vertices. Approximate — may include false positives in highly non-rectangular cells, but works well for building interiors. The ACME editor also has a portal-plane cut test for visibility (see `GetVisibleCells`).
|
||||
|
||||
---
|
||||
|
||||
## Portal visibility traversal (for rendering, not collision)
|
||||
|
||||
ACME's `GetVisibleCells` performs BFS from the camera cell through portals, testing:
|
||||
1. **Portal-side plane test** — camera must be on the InsideSide of the portal plane
|
||||
2. **Frustum test** — neighbour cell bounding box must intersect the view frustum
|
||||
|
||||
This is separate from collision transit detection. For Sprint 2 (indoor transitions), we need collision transit (above), not visibility traversal.
|
||||
|
||||
---
|
||||
|
||||
## Cell ID format
|
||||
|
||||
```
|
||||
Full cell ID: 0xAABBCCCC
|
||||
^^^^ landblock X (high 8 bits of high word)
|
||||
^^^^ landblock Y (low 8 bits of high word)
|
||||
^^^^ cell index (low word)
|
||||
|
||||
Low word < 0x100 → outdoor landblock cell (8×8 grid, values 0x0001..0x0040)
|
||||
Low word >= 0x100 → indoor EnvCell
|
||||
0xXXXXFFFF → sentinel meaning "outdoor" in CellPortal.OtherCellId context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design for acdream Sprint 2
|
||||
|
||||
The existing `PortalPlane.IsCrossing` and `CellSurface.SampleFloorZ` approach in `PhysicsEngine.cs` is a simplified subset of the full ACE algorithm. For Sprint 2, the upgrade path is:
|
||||
|
||||
### Current model (flat plane-crossing heuristic)
|
||||
- `PortalPlane.IsCrossing` = sign-change of plane-distance + centroid proximity guard
|
||||
- Works for simple convex cells with one entry portal
|
||||
|
||||
### ACE-accurate model (sphere containment)
|
||||
|
||||
Replace `IsCrossing` with the sphere variant of `find_transit_cells`:
|
||||
|
||||
```csharp
|
||||
// For each portal in the current indoor cell:
|
||||
// Get the portal's polygon plane in cell-local space
|
||||
// Convert world-sphere to cell-local sphere
|
||||
// Compute dist = Dot(localCenter, planeNormal) + planeD
|
||||
// If |dist| < sphere.Radius + EPSILON → sphere straddles plane
|
||||
// Then: convert sphere to otherCell's local space
|
||||
// call CellBSP.sphere_intersects_cell → if not Outside, add to candidate set
|
||||
```
|
||||
|
||||
**Outdoor → indoor:** the `check_building_transit` path is the missing piece:
|
||||
- When player is outdoor, iterate `BuildingObj.Portals` for the current SortCell
|
||||
- For each bldPortal, get the OtherCell (the nearest EnvCell in the building)
|
||||
- Test `sphere_intersects_cell(GlobalToLocal(sphere))` in that cell's local space
|
||||
- If not Outside → mark `HitsInteriorCell = true`, add the cell to the active set
|
||||
|
||||
**Key data not yet in acdream:**
|
||||
- `CellBSP` (BSP tree from environment dat) — required for `sphere_intersects_cell`
|
||||
- `BldPortal` list per SortCell (from LandBlock.Buildings) — required for outdoor→indoor detection
|
||||
- `VisibleCells` dict per EnvCell — required for indoor→indoor traversal
|
||||
|
||||
Until the BSP is wired, the current AABB / flat-plane approach is the best available approximation. The ACME `PointInCell` AABB approach is a valid intermediate step that doesn't need the BSP.
|
||||
|
||||
---
|
||||
|
||||
## Decompiled code note
|
||||
|
||||
The task hint asked about 0x531A00-0x531C00 in `chunk_00530000.c`. Inspecting that range:
|
||||
- `FUN_00531a30` (0x531A30): reads a count + array of structs from a stream (CellPortal deserialization candidate)
|
||||
- `FUN_00531ae0` (0x531AE0): destructor — frees an array
|
||||
- `FUN_00531b30` (0x531B30): vtable swap + destructor
|
||||
- `FUN_00531b70` (0x531B70): initializes an object with magic values (9, 0, 0, 0x51, 0xA2 — looks like CellStruct init with polygon/vertex counts)
|
||||
- `FUN_00531bd0` (0x531BD0): BSP/normal calculation from face adjacency — the vertex normal / rendering BSP builder
|
||||
- `FUN_00531e10` (0x531E10): polygon construction with vertex IDs and an "is horizontal" flag
|
||||
- `FUN_00531f20` (0x531F20): CellPortal serialization — writes 9 shorts per portal (polygon vertices)
|
||||
|
||||
**None of these contain the transit-cell intersection tests.** The transit tests in the decompiled client are almost certainly in the 0x004Dxxxx–0x0050xxxx range (where ObjCell, LandCell, EnvCell vtable methods live) but the symbols are not named in the Ghidra decompilation. The ACE port is the authoritative source.
|
||||
|
||||
---
|
||||
|
||||
## Summary table
|
||||
|
||||
| Transition type | Primary test | Fallback |
|
||||
|-----------------|-------------|---------|
|
||||
| Outdoor → outdoor | `add_all_outside_cells` (24m grid, boundary check) | N/A |
|
||||
| Outdoor → indoor | `check_building_transit`: `sphere_intersects_cell` in CellBSP | AABB (ACME) |
|
||||
| Indoor → same cell | `point_in_cell` (CellBSP or AABB) | no change |
|
||||
| Indoor → neighbour | `find_transit_cells`: `sphere_intersects_cell` in neighbour's CellBSP | load hint (plane-side only) |
|
||||
| Indoor → outdoor | `find_transit_cells`: OtherCellId == 0xFFFF + straddle test | plane sign-change |
|
||||
Loading…
Add table
Add a link
Reference in a new issue