gap: you cannot find out what a navigation mark is #214
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#214
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The client draws every buoy and beacon on the Dutch waterways correctly and then drops every word that says what one is.
The words are read and discarded at parse time.
read_featurepullsobj_vorm/naut_funct,obj_kleur, the topmark and the light colour (crates/cartopolis/src/systems/map/nav_marks.rs:337-378) and nothing else.WireMark's own doc comment names what goes: "the light's character and period, the fairway name, the RD coordinates, the sixteen sector definitions" (nav_marks.rs:140-144).naut_functis read for fixed marks, but only throughFixedForm::parse, which collapses the register's 14 values into 3 drawable posts (crates/geo/src/nav_marks.rs, and the census indocs/notes/navigation-marks.md:79-81).Nothing downstream could name one even if it had the words.
MarkCell::marksisVec<(f64, f64, MarkForm)>(nav_marks.rs:170-173);cell_marksturns it into bareNavMarks (nav_marks.rs:460-472);build_mark_meshesmerges those into the cell'sSurfaceGroup::Furnituremeshes (crates/geo/src/nav_marks.rs:423-447). TheMarkCellis moved into the worker closure and dropped. All that survives is a count:cell_places.surveyed.5(crates/cartopolis/src/systems/map/map_geometry.rs:701), summed byWorldPlaces::surveyed()(crates/cartopolis/src/systems/map/world_places.rs:377-384) for--dump-state.So the three ways a player asks "what is that?" all miss.
picking::prim_pickingonly sees prims.WorldPlaces::insert_cellbuildsInteractables fromplaces.furnitureandplaces.poisonly (world_places.rs:300-326), soplaces.nearest(pos, INTERACT_RADIUS)ininteract.rs:284can never return a mark.bag_address's readout chain ends atnearest_berth(crates/cartopolis/src/systems/player/interact.rs:395-399) and knows nothing of marks.And the layer has no row of its own. Marks ride the Furniture group's mesh, material, altitude fade and toggle — the decision the geo module calls "what makes this layer nearly free" (
crates/geo/src/nav_marks.rs:418-421) — so the only control is the Street furniture checkbox atcrates/cartopolis/src/systems/gui/data_layers.rs:2388-2392.Approach
Three parts, all in
cartopolisexcept one small label table incartopolis_geo.A. Carry the register's words through to the client
crates/cartopolis/src/systems/map/nav_marks.rs:read_featureadditionally readsbenaming,vaarwaterandnaut_functthrough the existingprop()helper (nav_marks.rs:311-313), each filtered throughis_absent(nav_marks.rs:91-93— the register writes absence three ways).MarkCell::marks' tuple with a named struct (lat,lng,form, plus the threeOption<String>s).parse_itemsandcell_markschange signature with it; both are only used inside this module and its tests (verified by grep — the only othernav_marksreferences aremap_geometry,world_places,shot_harnessandmod.rs).WireMarkgains the three fields under#[serde(default)], andcache_key'sv1becomesv2(nav_marks.rs:296). Without the bump, every cell already instorage::Namespace::Cachedecodes with empty names and is never re-fetched (load_marks_cellreturns the cached cell unconditionally,nav_marks.rs:413-415) — permanently nameless marks in exactly the cells a user has already visited. Same rule theKINDStable states for itself atnav_marks.rs:97-99.crates/geo/src/nav_marks.rsgains one pure function: an English label perMarkKind("spar buoy", "can buoy", "beacon", "groyne mark", …), in the style ofworld_places::prop_name(world_places.rs:175-200). Nothing else in the geo crate changes —NavMarkandMarkFormstay string-free, so the mesh path is untouched.B. A mark becomes something you can walk up to and ask about
On the prompt, not the HUD line. The issue proposes the
nearest_berthshape, i.e.InteractState::address, which the HUD draws asplace(crates/cartopolis/src/systems/gui/hud.rs:1029, field documented athud.rs:64-72). That surface cannot carry this: it is one truncated line whose row count the HUD's own size test bounds (hud.rs:1276-1279), and it is fed by a chain in which the address wins and the berth is only the fallback (interact.rs:395-399). On the Waalkade at Nijmegen a quay building is insideADDRESS_RADIUS, so a mark line placed in that chain would be shadowed in the exact spot the QA pass stood. The prompt is a separate world-anchoredArea(interact.rs:12-15, registered atlib.rs:2732), it already has "Look at" as its verb for a named point (world_places.rs:143-147), andTargetInfoalready carriesposso the route-to affordance works.crates/cartopolis/src/systems/map/world_places.rs:CellPlacesgainsmarks: Vec<…>(lat/lng already projected to tile-local by the worker, same as everything else arriving here — see the module note atworld_places.rs:12-15).map_geometry.rs:698-704fills it beside the existingsurveyed.5count.InteractKindgains aMarkvariant;glyph()returns an icon already in the bundled subset (icons::FLAG,crates/cartopolis/src/systems/gui/icons.rs:164) — there is no buoy or anchor glyph in it, and a glyph outside the subset renders as a tofu box (CLAUDE.md,docs/notes/icon-font.md).verb()returns"Look at".Interactable::seat_spotgains theMarkarm returningNone(world_places.rs:87-90).Interactablegainsdetail: Option<String>andreach: f32, both set at insertion.detailfor a POI iskind.detail().map(str::to_owned); for a propNone; for a mark the composed string below.reachisINTERACT_RADIUSfor props and POIs,MARK_RADIUSfor marks.WorldPlaces::nearest(pos, radius)keepsradiusas a ceiling and additionally filters each candidate against its ownreach(world_places.rs:398-407).crates/cartopolis/src/systems/player/interact.rs:MARK_RADIUS: f32 = 35.0, the same number and the same reason asADDRESS_RADIUS(interact.rs:32-34): the point is not where you can stand. A mark stands in the fairway and the avatar walks on the bank, so at the 3.5 mINTERACT_RADIUS(interact.rs:26-29) the only way to reach one is to fly over the water. Per-object reach is already the pattern the prim path uses (interact.rs:360-362).places.nearestis called withMARK_RADIUSatinteract.rs:284; nearest still wins, so a bench at 2 m beats a buoy at 20 m.TargetInfo::detailbecomesOption<String>;line()(interact.rs:118-123) and the change-detection compare (interact.rs:285-291) follow. Keep the compare-before-build rule the comment atinteract.rs:257-262states — compare the strings, do not build aTargetInfoper frame.Emote::Interactand the receipt line (interact.rs:196-203). No seat, noTouchPrim.The composed line. Name is
benamingwhere the register gives one, else the form's English label. Detail isnaut_functverbatim when present (fixed marks only), else the form's English label; then·andvaarwaterwhen present. Dutch verbatim, with·as the separator, follows the two precedents already on this line:monument_categoryis kept "in its own language" (interact.rs:459-462) andbag_addressjoins with·(interact.rs:403). Result:Look at 884.120R (Kribbaken · BOVEN-RIJN EN WAAL),Look at W 12D (spar buoy · BOVEN-RIJN EN WAAL).C. Its own row in the Layers panel
A sub-row of Street furniture, not a sibling. Marks are built into
SurfaceGroup::Furniture, and that group's entities carryFurnitureSurfaceplusfurniture_lod.visibility()(map_geometry.rs:1182-1187), which hides the whole group at once — so a marks row independent of Furniture would build meshes and then hide them. Nesting it is also the honest shape and has a precedent: 3DBAG sits under Buildings viaadd_enabled_ui(dl.buildings_enabled, …)(data_layers.rs:2258).DataLayersgainsnav_marks_enabled(data_layers.rs:499neighbourhood, plus theDefault,apply/save andfreshpaths at:758,:819,:1709,:2559).LayerPrefsgainsnav_marks: boolwith#[serde(default = "detail_default")](crates/cartopolis/src/systems/net/user_store.rs:229— an absent key is "not stated", not "off").DetailWantsgainsmarks: bool(map_geometry.rs:390-401); the fetch gate atmap_geometry.rs:474and the build at:698-704becomewants.furniture && wants.marks;wantsis assembled atmap_geometry.rs:950-957. Toggling costs a ring retear, which is what every flag in that struct already costs (map_geometry.rs:941-950).data_layers.rs:2388-2392), labelled "Navigation marks", with the same coverage honesty the Aerial row carries ("PDOK imagery — Netherlands only",data_layers.rs:~2425) — the register is Dutch waterways only.D. One new metric
ShotMetricsgainsnav_marks_named: usize— marks in the loaded cells carrying abenaming— beside the existingnav_marks(crates/cartopolis/src/systems/dev/shot_harness.rs:633-640), sourced fromWorldPlacesthe same way. It is the pairlod22_dated/lod22_elementsalready models (shot_harness.rs:641-646):nav_marks > 0withnav_marks_named == 0means the marks arrived and the words did not. Add it to the CSV header atshot_harness.rs:835and to the zeroed default at:3221.Acceptance criteria
read_featurereadsbenaming,vaarwaterandnaut_funct, each treating"",X,#andNiet toegewezenas absent viais_absent(nav_marks.rs:91-93).cache_keyis bumped fromv1tov2(nav_marks.rs:296).cartopolis_geo::nav_marksstill exposes noStringonNavMarkorMarkForm; the only addition there is a pureMarkKind→&'static strlabel.WorldPlaces::nearestfrom up toMARK_RADIUS(35 m) and not beyond; props and POIs keepINTERACT_RADIUS(3.5 m).Look at <benaming> (<naut_funct> · <vaarwater>), and for a floating oneLook at <benaming> (<form label> · <vaarwater>), with each parenthesised part omitted when the register gives nothing.benamingis named by its form label rather than left blank or dropped.Emote::Interactand leaves the receipt line; it does not seat the avatar and sends noTouchPrim.InteractKind::Markreturns a glyph that is present inicons::ALL(the existing uniqueness/coverage tests aticons.rs:425-435must still pass).DetailWantsgainsmarks, and the flag-by-flag retear test atmap_geometry.rs:2011-2039covers it (that test enumerates the fields by hand and will not catch a new one on its own).LayerPrefs::nav_marksuses#[serde(default = "detail_default")], so auser.jsonwritten before this change reads as the tier default rather than as off.ShotMetrics::nav_marks_namedexists, appears in the--dump-stateJSON and in the--csvheader atshot_harness.rs:835, and is zero in the default at:3221.parse_itemsover the committed real capturescrates/cartopolis/tests/fixtures/nav_marks_{floating,fixed}.json(nav_marks.rs:493-494) recovers the three new fields for a named feature — this is what pins the property keys against the live register.encode_cell→decode_cellround-trips the three fields.WorldPlaces::insert_cellplaces a mark at the cell origin offset andnearestfinds it at 30 m.docs/notes/navigation-marks.mdgains what the client now carries and why the readout is on the prompt rather than the HUD line; its "What is deliberately not drawn" list keeps the pass-side rule as out of scope.Verification
Runnable here (read-only pass did not run them):
Note the container needs the ALSA headers extracted before
cartopolisbuilds at all (alsa-is-missing-in-this-container);--no-default-featuresis not a workaround, it drops theaudiofeature and fails elsewhere.Headless capture — runs in this container (lavapipe is installed; the CLAUDE.md "no Vulkan driver" line is stale), but was not run by this pass, which is read-only:
The count assertions are honest evidence on a software rasteriser. Two things are not verifiable here and belong on a workstation or a phone:
icons::FLAGreads as a navigation mark next to the bench and café glyphs.Also unmeasured, and worth a number before merge: the cache size of a dense cell after the strings are added. The note records the Waal cell at 57 KB on the wire and 4 KB cached (
docs/notes/navigation-marks.md:44-52);benaming+vaarwater+naut_functacross 43 marks is the delta. Read it off thefeatures/marks/v2/14/8458/5422.jsonentry after one run over that cell.Out of scope
sign_kar/sign_perio). Already excluded by the layer's note (docs/notes/navigation-marks.md:133-138) and unchanged here — a flashing lantern is a second material.picking::prim_pickingstays prim-only. Proximity is the whole surface.docs/notes/navigation-marks.md:151-154) — an invisible wall in a fairway is worse than a buoy you can walk through.tools/subset-icon-font.py,docs/notes/icon-font.md) and touching an LFS-trackedinclude_bytes!asset. Reusing a subset glyph is the safe move; a proper glyph is a font change on its own.docs/notes/navigation-marks.md:117-125); nothing here changes that.Open questions
None.
Branch:
feat/214-nav-marks-readoutOriginal request
Found by the QA pass on #210, walking what #206/#207 shipped as a player would meet it.
What I did
Flew to the Waal at Nijmegen and looked at the marks the layer draws (headless capture, settled,
nav_marks=115). They are there and they look right — yellow cans in the fairway, a beacon on the far bank, everything standing at the waterline.Then I did the thing anyone does next with a thing on a map that obviously means something: tried to find out what it is.
What happened
Nothing. There is no way to ask.
FurnitureInstance, sopicking::prim_pickingnever sees it.systems::interactresolves streamed places, addresses, houseboat berths and scripted prims out ofWorldPlaces; a mark never entersWorldPlaces— the only thing #206 put there is a counter (CellPlaces::surveyed.5).Budget::balanced(); it is off on thephonerung, i.e. Settings ▸ Map ▸ Battery saver orCARTO_QUALITY=phone, wheredetail_layersis false.)Grepping bears it out: outside its own two modules,
nav_marksis referenced only bymap_geometry(to build the mesh),world_places(the counter) andshot_harness(the metric).Why that is a wall rather than a missing nicety
A navigation mark is the one thing on this map whose entire purpose is to tell you something. The layer's own note is explicit about it:
The client went to the trouble of drawing shape, band pattern, topmark and lantern colour correctly — and then a player looking at a red can with a cylinder on top has no way to be told "port-hand mark, keep it to your left", or even what it is called. The register already answers all of it and the answers are dropped at parse time:
benamingW 12D(floating),884.120R(fixed)vaarwaterBOVEN-RIJN EN WAALnaut_functKribbaken,Havenlichtsign_kar/sign_perioIso (isophased)/4WireMark's doc comment says so plainly: "the light's character and period, the fairway name … is dropped at parse time rather than carried through the cache." That was the right call for the cache; it also means nothing downstream can name a mark.What a user would expect instead
The same thing this client already does for every other point register it streams. Walk within a few metres of a mark and get a line: name, what it is, which fairway. That is exactly
interact.rs'snearest_berth— a lat/lng point register, held outsideWorldPlaces, matched by distance and turned into one readout line — and a mark would need no more machinery than a berth does.A cheaper half-step, if the readout is too much: give the marks their own row in the Layers panel under Map, so at least the layer is findable and can be switched on without knowing it lives under Street furniture.
Where the seam is
crates/cartopolis/src/systems/player/interact.rs—nearest_berthis the precedent; there is nonearest_markcrates/cartopolis/src/systems/map/nav_marks.rs—WireMarkis wherebenaming/vaarwater/naut_functare droppedcrates/cartopolis/src/systems/map/world_places.rs— marks reach it as a count onlycrates/cartopolis/src/systems/gui/data_layers.rs— the Furniture row, ~line 2391Filed by the QA pass on #210. Not
autonomous— a person decides whether this becomes work.🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.
🤖 Promoted into the build lane by the 3-day retrospective (#218) —
autonomous+ship.Why this one:
benaming,vaarwater,naut_functand the light's character.interact.rs'snearest_berth(line 475) is the same shape: a lat/lng point register held outsideWorldPlaces, matched by distance, turned into one readout line.nearest_markcopies it. The shape of the panel — which the fences do put out of bounds — is inherited, not invented.nearest_markis a pure distance match over a known cell and is unit-testable directly; add a count toShotMetricsthe way every other layer here did, and--expectgates it.WireMark, the on-disk cache format undercache/features/marks/v1/— notNetMessage, noPROTOCOL_HISTORYrow, no migration. Bump thev1path segment so an existing cache is not read with the old shape.Scope it to the readout. The ticket's cheaper half-step — a Layers-panel row of its own — is a judgement about where a row belongs and is left to the maintainer; build the thing that answers the question, not the thing that relocates a checkbox.
The two gaps left unpromoted are #212 and #215; the retrospective's reasoning for leaving them is on #218.
🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.
If the direction is wrong, say so now — @agent refine with a correction rewrites it, and
agent:skipstops me.🤖 viberfox issue agent on the VPS — 31 turns · $3.33 · session
d78dd775-da4a-4268-822c-242a3754c2fd. Reply with @agent to continue this session.🤖 Picked this up — working on branch
feat/214-nav-marks-readoutin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.You can now walk up to a buoy and be told what it is. Pushed as
6a77c90onfeat/214-nav-marks-readout.What changed
benaming(its name),vaarwater(the fairway) andnaut_funct(what a fixed mark is) — and carries them through the per-cell cache instead of discarding them. Each goes through the same "nothing here" filter as the rest, becauseXread as a name would put Look at X on the water.v1tov2. A cached cell is returned without re-checking, so without this every cell someone had already visited would keep decoding with empty names forever.Look at 884.120R (Kribbaken · BOVEN-RIJN EN WAAL). Pressing E plays the ordinary interact gesture and leaves the receipt line — no sitting, and nothing sent to the server. A mark with no registered name is called by its shape ("spar buoy").nav_marks_named, in the state dump and the CSV.nav_marksabove zero withnav_marks_namedat zero means the marks arrived and the words did not.How I verified it
cargo test -p cartopolis_geo— 224 tests pass.cargo test -p cartopolis— 812 tests pass, 0 failures.cargo fmt --checkclean. New tests cover the three fields read off the two committed captures of real service responses, the cache round trip, the composed line in both collections including the everything-absent fallback, the 35-metre reach and that a bench does not inherit it, and that the icon is one the bundled font actually contains.Not verified
I did not run a headless capture, and two things need an eye on a real screen:
Also unmeasured: the cache size of a dense cell now that the strings are in it. That needs a live fetch of the Nijmegen cell, which I did not do.
No commits were made.
🤖 viberfox issue agent on the VPS — 332 turns · $48.51 · session
e0b46552-b898-4146-a9b3-801fa98f9d54. Reply with @agent to continue this session.🤖 Not shipping this one — the branch conflicts with
main(another ticket landed first).Rebase
feat/214-nav-marks-readoutontomain, resolve, push, and put theshiplabel back to land it.Stuck, waiting for you: nonewhile three tickets wait for a person #270