fix(#185): landblock collision part-id uint32 overflow dropped stair steps

Root cause (live capture #3 + code): GameWindow's per-part landblock shadow
registration used a synthetic part-id `entity.Id * 256u + partIndex` that
OVERFLOWS uint32 for class-prefixed landblock ids (0x40/0x80/0xC0...). The << 8
drops the prefix byte, so different-class entities sharing the low 24 bits
collide on ONE shadow part-id and Register's deregister-then-insert silently
overwrites one entity's collision geometry. Landblock 0xF682 had 23 such
collisions incl. the stair runs (0xF6822100 <- {0x40F68221, 0xC0F68221}, ...),
so 3 mid-staircase steps rendered but had no collision -> the player floats into
the hole and the (faithful) PrecipiceSlide wedge fires = the 'invisible wall
half-way up the stairs'.

Fix (Option A, retail-faithful): register each multi-part landblock entity via
ShadowObjectRegistry.RegisterMultiPart under its UNIQUE 32-bit entity.Id
(retail CPhysicsObj::add_shadows_to_cells 0x00514ae0 -> CPartArray::AddPartsShadow
- one object, a part array; no synthetic per-part id). New testable builder
ShadowShapeBuilder.FromLandblockBspParts decomposes each MeshRef.PartTransform to
local pos/rot/scale; RegisterMultiPart reconstructs the identical world placement.
Building shells stay excluded (building channel); the Setup cyl/sphere path is
unchanged (runs only when entityBsp==0, retail BSP-xor-cyl dispatch). Despawn is
landblock-scoped (RemoveLandblock by cell prefix), so the id change is safe.

Tests: ShadowRegistrationOverflowTests (overflow arithmetic; old scheme drops one;
RegisterMultiPart keeps both; builder). Issue185OutdoorStairsSeamReplayTests
(dat-free clean-climb pin). Core 2629 / App 741 green, 0 warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-08 10:07:50 +02:00
parent b62b13ce21
commit 07c5b832cf
6 changed files with 897 additions and 67 deletions

View file

@ -0,0 +1,221 @@
{
"GfxObjId": 16779973,
"BoundingSphereOrigin": {
"X": -0.0486506,
"Y": -0.0466059,
"Z": 0.244318
},
"BoundingSphereRadius": 1.05416,
"ResolvedPolygons": [
{
"Id": 0,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 0,
"Y": -1,
"Z": 0
},
"D": -0.75
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": -0.25,
"Y": -0.75,
"Z": 0.2
},
{
"X": -0.25,
"Y": -0.75,
"Z": -0.4
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
}
]
},
{
"Id": 1,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": -1,
"Y": 0,
"Z": 0
},
"D": -0.25
},
"Vertices": [
{
"X": -0.25,
"Y": -0.75,
"Z": 0.2
},
{
"X": -0.25,
"Y": 0.75,
"Z": 0.2
},
{
"X": -0.25,
"Y": 0.75,
"Z": -0.4
},
{
"X": -0.25,
"Y": -0.75,
"Z": -0.4
}
]
},
{
"Id": 2,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 0,
"Y": 1,
"Z": 0
},
"D": -0.75
},
"Vertices": [
{
"X": -0.25,
"Y": 0.75,
"Z": 0.2
},
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
},
{
"X": -0.25,
"Y": 0.75,
"Z": -0.4
}
]
},
{
"Id": 3,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 1,
"Y": 0,
"Z": 0
},
"D": -0.25
},
"Vertices": [
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
}
]
},
{
"Id": 4,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": -0.62469506,
"Y": 0,
"Z": 0.78086877
},
"D": -0.31234753
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": -0.25,
"Y": 0.75,
"Z": 0.2
},
{
"X": -0.25,
"Y": -0.75,
"Z": 0.2
}
]
},
{
"Id": 5,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 0.62469506,
"Y": 0,
"Z": -0.78086877
},
"D": -0.15617374
},
"Vertices": [
{
"X": -0.25,
"Y": 0.75,
"Z": -0.4
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
},
{
"X": -0.25,
"Y": -0.75,
"Z": -0.4
}
]
}
]
}

View file

@ -0,0 +1,226 @@
{
"GfxObjId": 16779978,
"BoundingSphereOrigin": {
"X": -0.125,
"Y": 0,
"Z": 0.3
},
"BoundingSphereRadius": 0.900576,
"ResolvedPolygons": [
{
"Id": 0,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 0,
"Y": -1,
"Z": 0
},
"D": -0.75
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": -0.5,
"Y": -0.75,
"Z": 1.78814E-08
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
}
]
},
{
"Id": 1,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 0,
"Y": 1,
"Z": 0
},
"D": -0.75
},
"Vertices": [
{
"X": -0.5,
"Y": 0.75,
"Z": 1.78814E-08
},
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
}
]
},
{
"Id": 2,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 1,
"Y": 0,
"Z": 0
},
"D": -0.25
},
"Vertices": [
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
}
]
},
{
"Id": 3,
"NumPoints": 4,
"SidesType": 0,
"Plane": {
"Normal": {
"X": -0.62469506,
"Y": 0,
"Z": 0.7808688
},
"D": -0.31234753
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 0.6
},
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
},
{
"X": -0.5,
"Y": 0.75,
"Z": 1.78814E-08
},
{
"X": -0.5,
"Y": -0.75,
"Z": 1.78814E-08
}
]
},
{
"Id": 4,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 7.947333E-09,
"Y": 0,
"Z": -1
},
"D": 2.1855065E-08
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
},
{
"X": -0.5,
"Y": -0.75,
"Z": 1.78814E-08
},
{
"X": -0.5,
"Y": 0.75,
"Z": 1.78814E-08
}
]
},
{
"Id": 5,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 1,
"Y": 0,
"Z": 0
},
"D": -0.25
},
"Vertices": [
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
},
{
"X": 0.25,
"Y": 0.75,
"Z": 0.6
}
]
},
{
"Id": 6,
"NumPoints": 3,
"SidesType": 0,
"Plane": {
"Normal": {
"X": 7.947333E-09,
"Y": 0,
"Z": -1
},
"D": 2.1855065E-08
},
"Vertices": [
{
"X": -0.5,
"Y": 0.75,
"Z": 1.78814E-08
},
{
"X": 0.25,
"Y": 0.75,
"Z": 2.38419E-08
},
{
"X": 0.25,
"Y": -0.75,
"Z": 2.38419E-08
}
]
}
]
}