Skip to content

Commit 876b9f6

Browse files
wormeymanclaude
andcommitted
Run the placement path's cliff collision test: oriented boxes, truncated catalog, queued orientation (#407)
The Vulcanus cliff collision test the port shipped was tryToAddCliff's, the map PREVIEW path, which discards rotbb's 1/8 tag and scans the raw rectangle. The path that places cliffs hands Surface::wouldCollide the box WITH its orientation word, widens it with getAABB, and runs BoundingBox::collide - an oriented separating-axis test - per blocking tile. #406 read those calls out of the running game; this is the graded change that adopts them. Three edits under crates/fmw-noise/src/cliffs/. collision.rs (new) transcribes getAABB and collide from the 2.0.77 disassembly and reproduces the game's AABB and tile verdict on all 2157 recorded calls; placement.rs's rejection calls it. catalog.rs truncates rotbb's doubles toward zero as the loader does - the rounded table was one 1/256 unit off on 15 of 16 corner boxes. connections.rs tests each cell with its queued orientation, which the game does on 2088 of 2088 calls. engine.wasm rebuilt. Every frozen count that moved, moved toward the game: the shipping crossing-stage arm 1504/21/22/6 -> 1521/10/16/0 on the 1531 game cliffs, region [0,0] exact on position and orientation (283 of 283), the out-of-sample volcanism z from -1.80 to -2.55. The four frozen cliff render windows are byte-identical; one composite count moved (ore covered by a cliff, 206 -> 199). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiGqmKAaxnRnDXwND8rq2y
1 parent 34b6191 commit 876b9f6

11 files changed

Lines changed: 573 additions & 455 deletions

File tree

CLAUDE.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,12 @@ measured on #380 and #381 rather than read off its docs.
806806
minutes after the check had finished. A push cannot clear that, because
807807
nothing arrives to replace the old verdict.
808808

809+
A FOURTH, seen on #406 (2026-09-08): the push that took both findings got
810+
a green `CodeRabbit` check reading **`Review rate limited`** and no review
811+
at all. Same trap as the third, with a different label - nothing arrives to
812+
replace the verdict, and the PR sits `BLOCKED` on a review whose findings
813+
are already fixed.
814+
809815
So a hand dismissal is the tool for a standing review that nothing is going
810816
to supersede - whether because you declined its findings or because the
811817
re-review said nothing. Put the reasoning in the message - it is the only
@@ -1521,8 +1527,10 @@ The port found real defects in shipped TypeScript. **None was fixed inside the
15211527
port** - each got an issue and landed as its own graded change, because a
15221528
unilateral fix on the Rust side reads as a port bug in tier 2, which is the
15231529
whole point of having tier 2. All of them are landed now: the precision
1524-
findings (#269, #270, #273, #279, #290, #293, #309), then #320 and #324. The
1525-
rule stands for the next one.
1530+
findings (#269, #270, #273, #279, #290, #293, #309), then #320 and #324, then
1531+
#407 - the cliff collision test, found by reading `Surface::wouldCollide` out
1532+
of the running game with lldb rather than by disassembling the wrong path
1533+
(#406). The rule stands for the next one.
15261534

15271535
Two are worth carrying forward, because both were hidden the same way - the
15281536
evidence held one input constant everywhere it looked:

crates/fmw-noise/src/cliffs/catalog.rs

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@
3030
//! [`super::connections`] are derived from these names, which is what makes a
3131
//! transcription slip fail rather than shift the model.
3232
//! - [`CLIFF_ORIENTATION_COLLISION_BOX`] is the table the engine loads into
33-
//! `proto + 0x5c0 + id * 0x48`, and `tryToAddCliff` hands it to `wouldCollide`
34-
//! with `Direction = 0` - the identity arm, which copies the rectangle
35-
//! verbatim and discards `rotbb`'s `1/8` orientation tag. So the collision
36-
//! shape is the RAW stored rectangle, not a rotated one.
33+
//! `proto + 0x5c0 + id * 0x48` (`0x590` on 2.0.77). `tryToAddCliff` - the map
34+
//! PREVIEW generator - hands it to `wouldCollide` with `Direction = 0` and
35+
//! discards `rotbb`'s `1/8` tag. The path that PLACES cliffs,
36+
//! `EntityMapGenerationTask::applyCliffs`, does not: the box goes to
37+
//! `Surface::wouldCollide` WITH its orientation word, and the collision shape
38+
//! is the rotated rectangle. That test lives in [`super::collision`] and is
39+
//! graded against the game's own calls; see #407.
3740
//!
3841
//! ## The boxes are literals here and computed in the TypeScript
3942
//!
@@ -48,6 +51,12 @@
4851
//! full precision. Shipping the quantised values means the render path does no
4952
//! floating-point rounding for the boxes at all, while the derivation stays
5053
//! checkable against the Lua it came from.
54+
//!
55+
//! **The quantisation is TRUNCATION toward zero, not rounding.** Measured
56+
//! against the boxes the engine tested on every recorded call (#407): `trunc`
57+
//! reproduces all 16 corner boxes, `Math.round` misses 15 by one unit on one
58+
//! to three edges, and floor, ceil and round-half-even miss all 16. The table
59+
//! shipped rounded from 2026-08-02 to #407.
5160
5261
/// Default `cliff_elevation_0` map-gen setting.
5362
pub const CLIFF_ELEVATION_0_DEFAULT: f64 = 10.0;
@@ -308,26 +317,26 @@ pub type CliffCollisionBox = [f64; 4];
308317
/// multiple of `1/256` - see the module docs for why that is the format rather
309318
/// than a coincidence.
310319
pub const CLIFF_ORIENTATION_COLLISION_BOX: [CliffCollisionBox; 20] = [
311-
[-2.0, -1.5, 2.0, 1.5], // 0 west-to-east
312-
[-1.0, -2.0, 1.0, 2.0], // 1 north-to-south
313-
[-2.0, -0.5, 2.0, 0.5], // 2 east-to-west
314-
[-1.0, -2.0, 1.0, 2.0], // 3 south-to-north
315-
[-2.3125, -2.87109375, -0.1875, 1.37109375], // 4 west-to-north
316-
[-0.87109375, -1.8125, 3.37109375, 0.3125], // 5 north-to-east
317-
[0.04296875, -0.51953125, 1.45703125, 3.01953125], // 6 east-to-south
318-
[-2.51953125, 0.54296875, 1.01953125, 1.95703125], // 7 south-to-west
319-
[-3.37109375, -0.3125, 0.87109375, 1.8125], // 8 west-to-south
320-
[-1.45703125, -3.01953125, -0.04296875, 0.51953125], // 9 north-to-west
321-
[-1.01953125, -1.95703125, 2.51953125, -0.54296875], // 10 east-to-north
322-
[0.1875, -1.37109375, 2.3125, 2.87109375], // 11 south-to-east
323-
[-2.20703125, -1.4140625, -0.79296875, 1.4140625], // 12 west-to-none
324-
[0.0859375, -0.70703125, 2.9140625, 0.70703125], // 13 none-to-east
325-
[0.89453125, -0.6640625, 1.60546875, 2.1640625], // 14 east-to-none
326-
[-2.66796875, 0.40234375, 0.17578125, 1.109375], // 15 none-to-west
327-
[-0.9140625, -1.70703125, 1.9140625, -0.29296875], // 16 north-to-none
328-
[0.14453125, -0.76953125, 0.85546875, 2.76953125], // 17 none-to-south
329-
[-2.26953125, 0.64453125, 1.26953125, 1.35546875], // 18 south-to-none
330-
[-1.20703125, -2.4140625, 0.20703125, 0.4140625], // 19 none-to-north
320+
[-2.0, -1.5, 2.0, 1.5], // 0 west-to-east
321+
[-1.0, -2.0, 1.0, 2.0], // 1 north-to-south
322+
[-2.0, -0.5, 2.0, 0.5], // 2 east-to-west
323+
[-1.0, -2.0, 1.0, 2.0], // 3 south-to-north
324+
[-2.30859375, -2.87109375, -0.1875, 1.37109375], // 4 west-to-north
325+
[-0.87109375, -1.80859375, 3.37109375, 0.30859375], // 5 north-to-east
326+
[0.0390625, -0.515625, 1.45703125, 3.015625], // 6 east-to-south
327+
[-2.515625, 0.5390625, 1.015625, 1.95703125], // 7 south-to-west
328+
[-3.37109375, -0.30859375, 0.87109375, 1.80859375], // 8 west-to-south
329+
[-1.45703125, -3.015625, -0.0390625, 0.515625], // 9 north-to-west
330+
[-1.015625, -1.95703125, 2.515625, -0.5390625], // 10 east-to-north
331+
[0.1875, -1.37109375, 2.30859375, 2.87109375], // 11 south-to-east
332+
[-2.20703125, -1.4140625, -0.7890625, 1.4140625], // 12 west-to-none
333+
[0.08203125, -0.70703125, 2.9140625, 0.70703125], // 13 none-to-east
334+
[0.89453125, -0.6640625, 1.6015625, 2.1640625], // 14 east-to-none
335+
[-2.6640625, 0.3984375, 0.17578125, 1.10546875], // 15 none-to-west
336+
[-0.9140625, -1.70703125, 1.9140625, -0.2890625], // 16 north-to-none
337+
[0.14453125, -0.765625, 0.8515625, 2.765625], // 17 none-to-south
338+
[-2.265625, 0.64453125, 1.265625, 1.3515625], // 18 south-to-none
339+
[-1.20703125, -2.4140625, 0.20703125, 0.4140625], // 19 none-to-north
331340
];
332341

333342
/// The four straight orientations, written as plain boxes in the Lua rather
@@ -379,35 +388,18 @@ pub const CLIFF_ORIENTATION_ROTBB: [Option<[f64; 4]>; 20] = [
379388
/// `tests::the_square_root_constant_is_the_one_the_typescript_writes` pins it.
380389
const SQRT2: f64 = std::f64::consts::SQRT_2;
381390

382-
/// `Math.round`, which is NOT `f64::round`.
383-
///
384-
/// JavaScript rounds a half UP (toward `+inf`), so `Math.round(-0.5)` is `-0`;
385-
/// Rust rounds a half AWAY FROM ZERO, so `(-0.5f64).round()` is `-1`.
386-
///
387-
/// Every edge below is far from a half in practice - `rotbb`'s `sqrt(2)` sees
388-
/// to that - but "in practice" is not a reason to write the other function, and
389-
/// `tests::the_rounding_is_javascripts_and_not_rusts` plants the case that
390-
/// separates them.
391-
#[inline]
392-
fn js_round(v: f64) -> f64 {
393-
(v + 0.5).floor()
394-
}
395-
396391
/// `rotbb(x, y, size, intersect)` as the ENGINE reads it back
397392
/// (`entity-util.lua:9`), returning the RAW rectangle.
398393
///
399394
/// `rotbb` builds a rectangle centred at `(x + size/2, y + size/2)` with
400395
/// half-extents `((1 - intersect/size) * d, (intersect/size) * d)` where
401-
/// `d = size/2 * sqrt(2)`, and tags it with an orientation of `1/8`. **The tag
402-
/// is discarded for collision** - three steps of disassembly establish it, and
403-
/// the module docs name them - so this returns the rectangle unrotated.
396+
/// `d = size/2 * sqrt(2)`, and tags it with an orientation of `1/8`. This
397+
/// returns the rectangle unrotated; the tag is [`super::collision::ROTBB_TAG`],
398+
/// and the placement path applies it (#407).
404399
///
405-
/// Two wrong shapes shipped in the TypeScript before this one, and the more
406-
/// accurate-looking of them was the wrong one: a 45-degree separating-axis test
407-
/// scored better on every metric because it also absorbed an unrelated
408-
/// orientation defect. See `test/cliffCollisionBox.spec.ts`.
409-
///
410-
/// Edges are quantised to `1/256` because `MapPosition` is 8-bit fixed point.
400+
/// Edges are quantised to `1/256` by truncation toward zero, because
401+
/// `MapPosition` is 8-bit fixed point and that is what the loader does - see
402+
/// the module docs.
411403
#[must_use]
412404
pub fn rotbb_box(x: f64, y: f64, size: f64, intersect: f64) -> CliffCollisionBox {
413405
let dist = (size / 2.0) * SQRT2;
@@ -416,7 +408,7 @@ pub fn rotbb_box(x: f64, y: f64, size: f64, intersect: f64) -> CliffCollisionBox
416408
let y_dist = y_ratio * dist;
417409
let cx = x + size / 2.0;
418410
let cy = y + size / 2.0;
419-
let q = |v: f64| js_round(v * 256.0) / 256.0;
411+
let q = |v: f64| (v * 256.0).trunc() / 256.0;
420412
[
421413
q(cx - x_dist),
422414
q(cy - y_dist),
@@ -434,8 +426,12 @@ pub struct CliffTileBox {
434426
pub bottom: i64,
435427
}
436428

437-
/// The tile rectangle `EntityMapGenerationTask::wouldCollide` scans for a cliff
438-
/// of cell `code` centred at `(center_x, center_y)`.
429+
/// The tile rectangle `EntityMapGenerationTask::wouldCollide` - the map PREVIEW
430+
/// path's test - scans for a cliff of cell `code` centred at
431+
/// `(center_x, center_y)`. The placement path scans `getAABB`'s square instead
432+
/// and then tests each tile against the oriented box; see
433+
/// [`super::collision`]. Kept as the "before" model the game's recorded calls
434+
/// are graded against.
439435
///
440436
/// Both ends are **inclusive** and both come from a **floor**, because the
441437
/// engine works in `MapPosition`'s 8-bit fixed point and takes
@@ -525,8 +521,6 @@ mod tests {
525521
}
526522
}
527523

528-
/// `Math.round` and `f64::round` disagree on a negative half, and this port
529-
/// needs JavaScript's. Planted, because no real box edge lands on one.
530524
/// The TypeScript writes `1.4142135623730951`; Rust's constant must be the
531525
/// same bits, or `rotbb_box` evaluates different arithmetic.
532526
///
@@ -539,13 +533,19 @@ mod tests {
539533
assert_eq!(SQRT2.to_bits(), 1.414_213_562_373_095_1_f64.to_bits());
540534
}
541535

536+
/// Truncation, not rounding: planted on the one edge where the two differ
537+
/// by a whole unit, and on a negative edge, where truncation and floor
538+
/// differ. `south-to-east`'s right edge is `2.30859375` truncated and
539+
/// `2.3125` rounded; the engine holds the former on every recorded call.
542540
#[test]
543-
fn the_rounding_is_javascripts_and_not_rusts() {
544-
assert_eq!(js_round(-0.5), 0.0);
545-
assert_eq!((-0.5f64).round(), -1.0);
546-
assert_eq!(js_round(0.5), 1.0);
547-
assert_eq!(js_round(1.5), 2.0);
548-
assert_eq!(js_round(-1.5), -1.0);
541+
fn the_quantisation_truncates_toward_zero() {
542+
let b = rotbb_box(-1.0, -1.5, 4.5, 3.0);
543+
assert_eq!(b[2], 2.308_593_75);
544+
assert_eq!(b[0], 0.1875);
545+
assert_eq!(
546+
b[1], -1.371_093_75,
547+
"a negative edge truncates toward zero, not down"
548+
);
549549
}
550550

551551
/// The floor is inclusive at both ends, so a straight orientation's 4-tile

0 commit comments

Comments
 (0)