fix(collision): index a loc where it is clicked from, not where it is stored #133

Merged
sickday merged 1 commit from fix/bridge-loc-index into main 2026-08-06 13:27:27 +00:00
Owner

A bridge tile stores its locs one plane above the walkway they stand on, and
the plane in a click packet is the plane the player is standing on.
World.Collision.Flags.visual_plane/2 has resolved that since the bridge
marker was corrected, and the flag build applies it. The loc index did
not
— it keyed by loc.plane, and argued for it in its own @doc:

Indexed at the plane the loc was placed on: the bridge-tile visual
adjustment the flag build applies is a rendering concern, not where the
object lives for interaction purposes.

visual_plane/2's own doc, forty lines away in the same subsystem, says the
opposite and gives the reason: the plane a player will be on when they click
it — and therefore the plane the server sees in the click packet — is this one
rather than loc.plane. Anything that keys a loc by position needs the same
answer.

So a bridge-tile loc had its collision on one plane and its identity on the
one above
. The client draws it, draws its menu and sends a perfectly ordinary
click; locs_at/1 reads the tile the player is standing on, finds nothing
there, and the interaction is refused. Nothing is logged, because from the
dispatch's point of view the player clicked empty ground.

Measured over the whole world

locs total                     2,368,897
  indexed correctly            2,283,582
  MIS-INDEXED                     56,800     across 1,647 ids
  dropped (below plane 0)         28,515
  of the mis-indexed, interactive    981     across 222 ids

The interactive ones are what a bug report would have been written about:

  249  3077   Wall            Lean against
   91  3082   Wall            Lean against
   28  9534   Crate           Search
   14  3203   Trapdoor        Forfeit
   10  16115  Rock            Jump-To
    8  27635  Ladder          Climb
    6  24347  Bank booth      Bank, Collect
    6  5159   Bridge          Cross
    6  3238   Ledge           Cross
    4  10061  Grand Exchange booth   Exchange, Collect

It is why the Grand Exchange had no on-foot entrance

That was recorded as a map-data gap — not one tile within five of any of the
four booths is floored
— and filed as quite possibly the bridge-flag family.
It was the bridge-flag family, one layer up: the scan was correct and it was
scanning plane 1, where the booths were indexed and where nothing in that map
square exists at all.

With the index fixed the booths are at {0, 3164, 3487}, {0, 3165, 3487},
{0, 3164, 3492} and {0, 3165, 3492}, each on open floor, with
Contract.Reach answering true from the tile either side.
World.Content.Exchange.booth?(10_061, 1) has always been true, so nothing
about the exchange needed building — the door was mis-filed. Bot.Goals.Restock
targets the booth and can now derive a real one.

The clerk is still unreachable, and now for a knowable reason rather than a
mysterious one: it stands on the desk block, and the booth counter is a wall loc
on the tile between it and the only open ground, so interactable_from?/4's
edge check refuses across the counter. That is the ordinary wall rule doing its
job, and the booth does not need it answered.

The change

Three lines of behaviour. decode/2 carries the square's terrain alongside its
locs, and index_locs/4 keys by Flags.visual_plane/2, dropping a loc that
resolves below plane 0 exactly as the flag build drops it — the clutter under an
arch, which the client also does not draw. The two halves of publish/1 now
answer the same plane for the same loc, which is the invariant that was missing
rather than a special case for bridges.

Every consumer of the index wants the new answer: World.Doors and
World.Depletion look up what a click landed on, World.RoutePlan and
World.Climbing.Geometry resolve a target, and Bot.Sites and World.Teleports
build id-to-position tables for navigation.

Two tests pinned in World.CollisionTest: the booth is indexed at plane 0 and
absent from plane 1, and there is somewhere to stand beside it that reach
accepts.

Gates

Suite green — 3167 tests, 1338 doctests, 55 properties, 0 failures (SQLite
leg); Credo clean. mix format --check-formatted and mix dialyzer fail, and
both fail identically on a stashed clean origin/main on this machine: that is
issue #131, not this branch.

A bridge tile stores its locs one plane above the walkway they stand on, and the plane in a click packet is the plane the player is standing on. `World.Collision.Flags.visual_plane/2` has resolved that since the bridge marker was corrected, and the flag build applies it. **The loc index did not** — it keyed by `loc.plane`, and argued for it in its own `@doc`: > Indexed at the plane the loc was *placed* on: the bridge-tile visual > adjustment the flag build applies is a rendering concern, not where the > object lives for interaction purposes. `visual_plane/2`'s own doc, forty lines away in the same subsystem, says the opposite and gives the reason: *the plane a player will be on when they click it — and therefore the plane the server sees in the click packet — is this one rather than `loc.plane`. Anything that keys a loc by position needs the same answer.* So a bridge-tile loc had its **collision on one plane and its identity on the one above**. The client draws it, draws its menu and sends a perfectly ordinary click; `locs_at/1` reads the tile the player is standing on, finds nothing there, and the interaction is refused. Nothing is logged, because from the dispatch's point of view the player clicked empty ground. ## Measured over the whole world ``` locs total 2,368,897 indexed correctly 2,283,582 MIS-INDEXED 56,800 across 1,647 ids dropped (below plane 0) 28,515 of the mis-indexed, interactive 981 across 222 ids ``` The interactive ones are what a bug report would have been written about: ``` 249 3077 Wall Lean against 91 3082 Wall Lean against 28 9534 Crate Search 14 3203 Trapdoor Forfeit 10 16115 Rock Jump-To 8 27635 Ladder Climb 6 24347 Bank booth Bank, Collect 6 5159 Bridge Cross 6 3238 Ledge Cross 4 10061 Grand Exchange booth Exchange, Collect ``` ## It is why the Grand Exchange had no on-foot entrance That was recorded as a map-data gap — *not one tile within five of any of the four booths is floored* — and filed as quite possibly the bridge-flag family. It was the bridge-flag family, one layer up: the scan was correct and it was scanning plane 1, where the booths were indexed and where nothing in that map square exists at all. With the index fixed the booths are at `{0, 3164, 3487}`, `{0, 3165, 3487}`, `{0, 3164, 3492}` and `{0, 3165, 3492}`, each on open floor, with `Contract.Reach` answering true from the tile either side. `World.Content.Exchange.booth?(10_061, 1)` has always been true, so nothing about the exchange needed building — the door was mis-filed. `Bot.Goals.Restock` targets the booth and can now derive a real one. The **clerk** is still unreachable, and now for a knowable reason rather than a mysterious one: it stands on the desk block, and the booth counter is a wall loc on the tile between it and the only open ground, so `interactable_from?/4`'s edge check refuses across the counter. That is the ordinary wall rule doing its job, and the booth does not need it answered. ## The change Three lines of behaviour. `decode/2` carries the square's terrain alongside its locs, and `index_locs/4` keys by `Flags.visual_plane/2`, dropping a loc that resolves below plane 0 exactly as the flag build drops it — the clutter under an arch, which the client also does not draw. The two halves of `publish/1` now answer the same plane for the same loc, which is the invariant that was missing rather than a special case for bridges. Every consumer of the index wants the new answer: `World.Doors` and `World.Depletion` look up what a click landed on, `World.RoutePlan` and `World.Climbing.Geometry` resolve a target, and `Bot.Sites` and `World.Teleports` build id-to-position tables for navigation. Two tests pinned in `World.CollisionTest`: the booth is indexed at plane 0 and absent from plane 1, and there is somewhere to stand beside it that reach accepts. ## Gates Suite green — **3167 tests, 1338 doctests, 55 properties, 0 failures** (SQLite leg); Credo clean. `mix format --check-formatted` and `mix dialyzer` fail, and both fail identically on a stashed clean `origin/main` on this machine: that is issue #131, not this branch.
fix(collision): index a loc where it is clicked from, not where it is stored
All checks were successful
ci / gates (pull_request) Successful in 1m1s
build / image (push) Successful in 33s
ci / gates (push) Successful in 59s
17156e58e6
A bridge tile stores its locs one plane above the walkway they stand on, and
the plane in a click packet is the plane the player is standing on. The flag
build has applied `Flags.visual_plane/2` since the marker was corrected; the
loc index did not, and argued for it in its own doc.

So 56,800 locs had their collision on one plane and their identity on the one
above. The client drew them, drew their menus and sent perfectly ordinary
clicks; the lookup read the player's tile, found nothing, and refused. 981 of
those placements are interactive -- `Lean against` walls, `Search` crates,
`Climb` ladders, `Forfeit` entrances, `Cross` bridges, bank booths, and all
four Grand Exchange booths, which is the whole of the exchange having no
on-foot entrance.

`decode/2` now carries the square's terrain alongside its locs and
`index_locs/4` keys by the visual plane, dropping a loc that resolves below
plane 0 exactly as the flag build drops it. The two halves of `publish/1`
answer the same plane for the same loc, which is the invariant that was
missing rather than a special case for bridges.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server!133
No description provided.