fix(cockpit): a coarse read never takes a river bridge back to the sea

Before the first full-detail water read lands (it can take many seconds
from cold tiles), a refresh's loaded-tile read with one low probe still
dropped a river height to the geoid. Before that landing, loaded-tile reads
may now find raised water but only a full-detail read lowers it.

CURRENT-STATE also says what the earlier fixes changed: C stays the CCTV
shortcut when the bridge cannot be entered, voice entry is refused on a
bridge, and the ridden ship's trail is hidden with its sprite.
This commit is contained in:
Sameh Khamis committed 2026-11-08 14:09:18 -08:00
1 parent 5c937e67e1
commit 80a33ba52c
3 files changed
+44 -9

No files matched your search

+10 -5
View File
@@ -1530,16 +1530,20 @@ With no aircraft tracked, Cockpit can ride the selected AIS vessel instead.
Entry needs only the layer of the vessel it rides, Live AIS Vessels, and a Entry needs only the layer of the vessel it rides, Live AIS Vessels, and a
selected vessel; Contacts and every other layer may be off, before or during selected vessel; Contacts and every other layer may be off, before or during
the bridge (`bridgeEntryAllowed` in `src/contextModePolicy.js`). A Context mode the bridge (`bridgeEntryAllowed` in `src/contextModePolicy.js`). A Context mode
change still in flight blocks entry until it settles. `C` enters it, and the change still in flight blocks entry until it settles. `C` enters it (when
the bridge cannot be entered, `C` stays the CCTV shortcut), and the
entry button reads BRIDGE with a ship icon both in Contacts (`#cockpit-entry`) entry button reads BRIDGE with a ship icon both in Contacts (`#cockpit-entry`)
and under the selected vessel's Intel HUD readout in the bottom-right corner and under the selected vessel's Intel HUD readout in the bottom-right corner
(`#cockpit-entry-readout`, shown with the HUD on, in its Tactical and Cyber (`#cockpit-entry-readout`, shown with the HUD on, in its Tactical and Cyber
layouts). Turning Live AIS Vessels off ends the bridge: the layer then has no layouts). Turning Live AIS Vessels off ends the bridge: the layer then has no
subject (`getCockpitSubject` returns null). The bridge picture uses AIS alone, subject (`getCockpitSubject` returns null). The bridge picture uses AIS alone,
so no other feed being off makes it partial. so no other feed being off makes it partial.
Voice (`control_cockpit`) and the selection entry transaction stay aircraft-only. Voice (`control_cockpit`) and the selection entry transaction stay aircraft-only;
voice `enter` on a bridge is refused before it touches tracking, with the
reason to exit the bridge first.
The vessel layer supplies the subject (`getCockpitSubject`), hides that ship's The vessel layer supplies the subject (`getCockpitSubject`), hides that ship's
sprite, card and detection bracket while it is held (`setCockpitSubject`), and sprite, card, detection bracket and trail while it is held
(`setCockpitSubject`), and
ignores scene clicks so open water cannot deselect it. The eye sits on the AIS ignores scene clicks so open water cannot deselect it. The eye sits on the AIS
position at a representative bridge height for the AIS type family position at a representative bridge height for the AIS type family
(`src/layers/vessels/cockpitSubject.js`: cargo and tanker 30 m, passenger 25 m, (`src/layers/vessels/cockpitSubject.js`: cargo and tanker 30 m, passenger 25 m,
@@ -1569,8 +1573,9 @@ eight probes 30 m out, the lowest probe taken as the water when it is 2 m to
the full-detail read replaces it; it repeats every 5 s. Only one full-detail the full-detail read replaces it; it repeats every 5 s. Only one full-detail
read runs at a time: while one is still running, a refresh reads the loaded read runs at a time: while one is still running, a refresh reads the loaded
tiles instead, and the slow read applies whenever it lands. Once a tiles instead, and the slow read applies whenever it lands. Once a
full-detail read has answered, loaded-tile reads no longer change the height, full-detail read has answered, loaded-tile reads no longer change the height;
and a read that sampled nothing keeps the height it had. A failed read keeps before then they may find raised water but never take it back to the sea.
A read that sampled nothing keeps the height it had. A failed read keeps
the sea surface. the sea surface.
The HUD keeps its frame, vision styles, briefing, weather and Display/Radio The HUD keeps its frame, vision styles, briefing, weather and Display/Radio
+24
View File
@@ -1350,6 +1350,30 @@ test('a river height stands through reads that sample nothing or one coarse prob
); );
}); });
test('before any full read lands, a coarse loaded read keeps the river it found', async (t) => {
const clock = manualClock(t, START_MS);
const sea = await bridgeAt(t, clock);
frames(sea.cockpit, clock, sea.shots, 60);
const seaEyeM = eyeHeightM(sea.shots.at(-1));
sea.cockpit.exit();
clock.set(START_MS);
const record = { ...RECORD, lastPositionEpoch: START_MS / 1000 };
const ship = fixture(t, { record });
// The full-detail read is cold and never lands in this test.
ship.viewer.scene.sampleHeightMostDetailed = () => new Promise(() => {});
ship.viewer.scene.sampleHeight = () => 27;
ship.cockpit.enter();
await settle();
ship.cockpit.update();
frames(ship.cockpit, clock, ship.shots, 60);
near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'on the river', 0.5);
// A refresh meets one low probe on coarse tiles.
ship.viewer.scene.sampleHeight = () => 0.5;
frames(ship.cockpit, clock, ship.shots, 240);
near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'still on the river', 0.5);
});
test('a client clock minutes fast does not age the bridge into a stale feed', async (t) => { test('a client clock minutes fast does not age the bridge into a stale feed', async (t) => {
// The feed's newest fix is this ship's; the client clock runs 200 s // The feed's newest fix is this ship's; the client clock runs 200 s
// ahead of the feed and received the refresh just now. // ahead of the feed and received the refresh just now.
+10 -4
View File
@@ -110,9 +110,10 @@ export function update() {
* every frame until it settles): while one is still running, a refresh * every frame until it settles): while one is still running, a refresh
* reads the loaded tiles instead, and the slow read applies whenever it * reads the loaded tiles instead, and the slow read applies whenever it
* lands. Once a full-detail read has answered, only full-detail reads * lands. Once a full-detail read has answered, only full-detail reads
* change the height: a loaded-tile read stands in only until the first * change the height; before then a loaded-tile read may find raised water
* lands, so one coarse probe cannot drop a river bridge to the sea, and a * but never takes a raised height back to the sea. One coarse probe cannot
* read that sampled nothing keeps the height it had. * drop a river bridge to the sea, and a read that sampled nothing keeps
* the height it had.
* @param {{latitude: number, longitude: number}} [at] Where to read the * @param {{latitude: number, longitude: number}} [at] Where to read the
* water: the dead-reckoned position the camera rides. * water: the dead-reckoned position the camera rides.
* @returns {number} Ellipsoidal metres. * @returns {number} Ellipsoidal metres.
@@ -145,7 +146,12 @@ function vesselSurfaceM(info, nowMs, at = info) {
read.sampledMs = nowMs; read.sampledMs = nowMs;
if (read.inFlight && !read.landed) { if (read.inFlight && !read.landed) {
const loadedM = loadedWaterlineM(scene, probes, seaM); const loadedM = loadedWaterlineM(scene, probes, seaM);
if (Number.isFinite(loadedM)) read.heightM = loadedM; // Taking a river back to the sea is the full-detail read's to say.
const lowers =
Number.isFinite(read.heightM) &&
read.heightM > seaM &&
!(loadedM > seaM);
if (Number.isFinite(loadedM) && !lowers) read.heightM = loadedM;
} }
} }
if (!read.inFlight) readWaterline.call(this, read, scene, probes, seaM); if (!read.inFlight) readWaterline.call(this, read, scene, probes, seaM);