fix(cliffs): the collision box is the RAW rectangle - #88 was wrong (#84) - #90
Merged
Conversation
The banner kept the pre-#88 figure. Since the collision box was corrected to rotbb's rotated rectangle the rejection costs ZERO true positives, so the shipping matched set is identical to the unfiltered one - 1531 either way, 37 wrong orientations, 2.42%. Measured on both paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt
) Disassembly, prompted by flagging #88's fix as the thing I was least confident about. It was right to flag: #88 scored best on every metric and was wrong about the mechanism. The engine does NOT collide against rotbb's rectangle rotated 45 degrees. It uses the RAW stored rectangle and discards the orientation tag entirely: 1. EntityMapGenerationTask::tryToAddCliff (0x101625038) loads the orientation's box from proto + 0x5c0 + id*0x48 (20 bytes: four int32 edges at +4, the orientation word at +0x14) and calls wouldCollide with Direction = 0 - literally `mov x4, #0x0`. 2. wouldCollide (0x101625468) forwards box and direction to BoundingBox::BoundingBox(BoundingBox const&, Direction) (0x101c04380), then floors with (box + position) >> 8 over an inclusive tile rect. 3. That constructor zeroes the destination, writes sentinel 0x80010000 into the destination's orientation word, and dispatches through a jump table whose entry 0 is 0 (read at 0x102d01400) - the identity arm, which copies left_top/right_bottom verbatim. The source orientation is never read; the rotate arm is reachable only for a non-zero Direction. Corroborated by the API mirror: BoundingBox is documented as {MapPosition, MapPosition} or {..., RealOrientation} with orientation OPTIONAL, and OrientedCliffPrototype::collision_bounding_box is a plain BoundingBox. | box | false rej | recall | precision | evidence | | ------------------------------ | --------- | ------ | --------- | ---------- | | AABB (until #88) | 13 | 0.9675 | 0.9743 | assumption | | 45-degree oriented rect (#88) | 0 | 0.9758 | 0.9727 | fit only | | raw stored rect (this) | 6 | 0.9720 | 0.9713 | disasm | The middle row is the trap. It shrank the box past the engine's, and the excess shrinkage absorbed a DIFFERENT defect: 4 of the 6 cliffs the correct box still rejects are cells where our orientation disagrees with the game's, so we load the wrong box entirely. Those 4 belong to the standing orientation residual and should stay visible. A model that scores perfectly by hiding a second bug is worse than one that leaves it exposed. Edges are quantised to 1/256 - MapPosition is 8-bit fixed point, so x_dist's sqrt(2) cannot reach the engine at full precision. test/cliffOrientation.spec.ts asserted the OPPOSITE of this until now, on the stated reasoning that the AABB "is what the engine ends up scanning". That block is rewritten: the shipped box is the raw rectangle, it DEPENDS on intersect (the old file asserted independence), and its area is at most half the AABB's - though it is not contained in it, since a small intersect pushes hx past size/2. An attempt to assert containment on every axis failed for exactly that reason. cliffBoxCoversTile and the separating-axis narrow phase are deleted; the broad phase alone is now the whole rule, which is what the engine does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt
This was referenced Aug 2, 2026
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
cliffOrientationOracle.spec.ts counts the residual and bounds it. This pins what it looks like, because the shape is the lead and a change in shape is a change in cause even when the count holds. Measured after #83 (multisample grid), #86 (lava rejection) and #90 (raw collision box): - 37 of 1531 matched cells, and all 37 differ in EXACTLY ONE edge. Not one two-edge difference survives; before #83 the dominant mode was two edges (125 of 175), a whole corner on the wrong side of a band. - Every one is an OVER-detection: the game reports a -to-none orientation and the port reports a crossing on that edge, never the reverse. - Spread over all four edges (L11/R6/T7/B13) and all three regions (7/26/4), so it is not a directional off-by-one. Two candidate causes are already eliminated, which is why the shape is worth pinning rather than re-deriving next session: - crossesCliff is EXACT. Disassembled at 0x10160c914 under 2.1.12 (the VA in cliffs-NOTES.md had moved); cliffPlacement.ts reproduces it line for line including the a<0||b<0 early-out, the boundary<e0 check, and the strict >0.5 gate and strict crossing comparisons. No >=-vs-> slip exists to find. - cliffiness_basic is EXONERATED. Substituting the game's own corner cliffiness leaves the count at exactly 37/1531. So the residual is in the grid-4 cliff-elevation field - the one input in the chain with no direct per-corner oracle. A single-edge, strictly one-directional over-detection is what a small positive field offset looks like. The direction assertion is the load-bearing one: if under-detections ever appear, the cause has changed and that reading is dead. Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Disassembly, prompted by flagging #88's fix as the thing I was least confident about. That flag was right: #88 scored best on every metric and was wrong about the mechanism.
What the engine actually does
It does not collide against
rotbb's rectangle rotated 45°. It uses the raw stored rectangle and discards the orientation tag:EntityMapGenerationTask::tryToAddCliff(0x101625038) loads the box fromproto + 0x5c0 + id*0x48(20 bytes: fourint32edges at+4, orientation word at+0x14) and callswouldCollidewithDirection = 0— literallymov x4, #0x0.wouldCollide(0x101625468) forwards box and direction toBoundingBox::BoundingBox(BoundingBox const&, Direction)(0x101c04380), then floors with(box + position) >> 8over an inclusive tile rect.0x80010000into the destination's orientation word, and dispatches through a jump table whose entry 0 is 0 (read at0x102d01400) — the identity arm, copyingleft_top/right_bottomverbatim. The source orientation is never read; the rotate arm is reachable only for a non-zeroDirection.Corroborated by the API mirror rather than disassembly alone:
BoundingBoxis documented as{MapPosition, MapPosition}or{..., RealOrientation}withorientationoptional, andOrientedCliffPrototype::collision_bounding_boxis a plainBoundingBox.Three shapes, and the best-scoring one is wrong
The middle row is the trap. It shrank the box past the engine's, and the excess shrinkage absorbed a different defect: 4 of the 6 cliffs the correct box still rejects are cells where our orientation disagrees with the game's, so we load the wrong box entirely. Those 4 belong to the standing orientation residual and should stay visible. A model that scores perfectly by hiding a second bug is worse than one that leaves it exposed.
Also
test/cliffOrientation.spec.tsasserted the opposite of this until now, on the stated reasoning that the AABB "is what the engine ends up scanning". Rewritten: the shipped box is the raw rectangle, it depends onintersect(the old file asserted independence), and its area is at most half the AABB's — though it is not contained in it, since a smallintersectpusheshxpastsize/2. My first attempt asserted containment on every axis and failed for exactly that reason.cliffBoxCoversTileand the separating-axis narrow phase are deleted. The broad phase alone is the whole rule, which is what the engine does.MapPositionis 8-bit fixed point, sox_dist'ssqrt(2)cannot reach the engine at full precision.pnpm run verifygreen: 1311 passed / 3 skipped, 0 warnings.🤖 Generated with Claude Code
https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt