Skip to content

Shared Datablock Manipulations Across Same TCWs / Added Shared Scope Feature#872

Draft
Jud6969 wants to merge 78 commits into
mmp:masterfrom
Jud6969:shared-tcw-display
Draft

Shared Datablock Manipulations Across Same TCWs / Added Shared Scope Feature#872
Jud6969 wants to merge 78 commits into
mmp:masterfrom
Jud6969:shared-tcw-display

Conversation

@Jud6969

@Jud6969 Jud6969 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Closes #736. I added a bit more to it.
Per-ACID track annotation sync (always on)

When two or more humans are at the same TCW, annotations placed on a specific aircraft propagate to every controller at that TCW:

  • J-ring radius, ATPA cone length
  • Leader-line direction (and FDAM variant), UseGlobalLeaderLine
  • Force-full-datablock, per-ACID PTL
  • TPA-size, ATPA monitor, ATPA warn/alert toggles
  • Display-requested-altitude, LDB beacon-code
  • Stale entries auto-pruned when a track leaves.

Opt-in scope-prefs sync ("Sync Scope Setup")

A checkbox on the Join-as-Relief dialog. When any relief ticks it, the TCW-wide flag flips on (sticky for the session) and scope preferences round-trip between the primary and every participating relief via a single opaque JSON blob:

  • Scope center, range, range-ring radius, rotation
  • Brightness levels, PTL length
  • Video map selections, weather levels
  • SSA filters + GI text, list positions
  • LDR direction and every other configurable DCB feature

Per-controller opt-out: a relief who leaves the checkbox off at join time does not participate in sync, even if another relief already enabled it TCW-wide — their scope stays local-only.

Excluded (remain strictly per-user): CharSize, AudioVolume, AudioEffectEnabled, DwellMode, AutoCursorHome, CursorHome, DisplayDCB, DCBPosition, RestrictionAreaSettings.

Architecture

  • Server owns TCWDisplayState per TCW, lazily created on first signon. Not persisted across sim restarts.
  • Delivery: RPC mutations bump a revision counter; every state-update poll carries the current blob. State-update poll cap tightened from 1 s → 100 ms so observers update within a frame.
  • Push throttle on the client side caps outgoing RPCs at 10 Hz so an active pan/drag doesn't flood the server.
  • Rejoin inherits the TCW's current state; last-leaves-then-rejoin preserves annotations for the session.

Jud6969 and others added 25 commits April 21, 2026 21:35
Introduces a new "shared TCW" multiplayer mode where multiple controllers
signed in to the same STARS workstation see a synchronized radar picture
(scope view + per-aircraft annotations). Personal comfort settings stay
local. Separate from relief mode; STARS only for this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reframe from "new shared-TCW mode" to "relief mode + display sync"
- No new join UI; existing relief click-through is the entry point
- Typed mutation RPCs (one per synced field)
- Event-driven push on mutation, 1Hz poll as correctness floor
- Preference-set loading never overrides synced TCWDisplayState
- Remove shared-vs-relief exclusivity question (obsolete under new framing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflective test asserts every field is explicitly categorized to prevent
silent drift as new preferences are added. Foundation for sharing
synced-bucket state across relief controllers at one TCW.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Seeds from a ScopeViewState and bumps a monotonic Rev on each mutation.
Additional scope-view fields and per-aircraft annotations will be
added by follow-up plans.
Sim.TCWDisplay is a map keyed by TCW. GetTCWDisplay returns nil when
not yet created; EnsureTCWDisplay seeds on first signon and is
idempotent on subsequent calls.
First human sign-in lazily creates the shared state seeded from the
sim's scenario-default range and center via new CommonState helpers
GetInitialRangeForTCW / GetInitialCenterForTCW. Relief joiners
inherit whatever is already there (idempotent EnsureTCWDisplay).

Test sim now wires up an empty STARSComputer so SignOn can complete
its GetUserState path.
Poll-based correctness floor for shared relief state. Every poll
returns the current snapshot of the caller's TCW display state,
which the server.SimStateUpdate.Apply path stores on SimState.
Nil snapshots leave existing state alone.

Event-push path is deferred to a follow-up; for now the 1 Hz poll
is the single source of truth.
Sim.SetTCWRange acquires the sim lock, mutates via TCWDisplayState.SetRange
(bumping Rev), and returns. The dispatcher echoes a fresh SimStateUpdate
in the reply so the caller's local State.TCWDisplay updates immediately
without waiting for the next 1 Hz poll.
Applies the echoed SimStateUpdate on success so client-local
State.TCWDisplay reflects the server value immediately.
…sent

When the server has published a TCWDisplayState for the caller's TCW,
syncedRange/syncedUserCenter/syncedRangeRingRadius return the shared
value; otherwise they fall back to local Preferences. The Draw entry
point also mirrors the snapshot back into the active prefs every frame
so saved-set serialization captures the current shared values.

Read-only shim — writes still go through ps until the next task. Sites
without a panes.Context in scope (DrawUI, fuzz generator) still read
ps directly; the mirror-back hook keeps ps in sync with the snapshot
during normal Draw flow.
Range adjustments from the DCB spinner, the wheel-zoom path, and the
keyboard input on the spinner now call ctx.Client.SetTCWRange; local
ps.Range is no longer the source of truth. dcbRadarRangeSpinner now
takes get/set callbacks instead of a *float32 pointer so the spinner
machinery can dispatch RPCs without holding stale local state.

Manual smoke testing of UI input round-trip is deferred to the
two-client integration test in Task 13.
Mirrors the Range pattern: Sim.SetTCWUserCenter under lock, typed RPC
SetTCWUserCenterArgs/RPC, async client wrapper, and redirected write
sites (drag-to-pan, wheel-zoom recenter, PLACE CNTR DCB drag).

Sim-level test parallels TestSimSetTCWRangeLocksAndBumpsRev. Server
dispatcher wiring is exercised end-to-end by the Task 13 integration
test.
Third scope-view scalar through the foundation pattern: Sim helper +
typed RPC + async client wrapper + DCB spinner refactored to take
get/set callbacks. The drawRangeRings read site uses the syncedRangeRingRadius
helper from Task 8.

Sim-level test parallels TestSimSetTCWRange/UserCenter; integration
coverage of the dispatcher wiring is in Task 13.
PreferenceSet.SetCurrent now merges the loaded set against the existing
Current via mergeLoadedPreferences: synced (TCW-shared) fields stay
where they are; unsynced personal-comfort fields apply. The PREF
sub-menu RESTORE button is rerouted through SetCurrent to pick up the
same protection.

When a user loads a saved preference set at a TCW with relief
partners, brightness/fonts/unsynced chrome are applied locally, but
synced scope-view fields are left untouched so the partner's picture
isn't yanked out from under them.
A sets Range, B polls and sees it. B sets UserCenter, A polls and
sees it. A sets RangeRingRadius, B sees it. State persists when A
signs off while B remains. Exercises the full dispatcher +
SimStateUpdate round-trip for the three fields in this plan.

To make this testable without standing up the WX provider or HTTP
server, sim/export_test.go is renamed to sim/testsim.go (no _test
suffix) so cross-package tests can construct a minimal Sim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers spec's "last leaves, new human joins" case: A signs in, mutates
Range, signs off; the TCW's display state survives the gap; the next
controller's first GetStateUpdate sees the inherited Range rather than
a freshly seeded one.

Closes the plan's testing-coverage gap noted in Task 14 step 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the shipped scope-view sync (Range/UserCenter/RangeRingRadius)
with per-ACID STARS track annotations shared across relief controllers
at the same TCW. Relief wants independent pan/zoom but shared workflow
annotations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace scope-view sync (Range/UserCenter/RangeRingRadius) with
per-ACID TrackAnnotations shared across relief controllers at the
same TCW: J-ring, cone, leader-line, FDB/PTL/ATPA toggles, requested
altitude, LDB beacon code.

- sim.TCWDisplayState now carries map[ACID]TrackAnnotations + Rev.
- 12 SetTrack* sim helpers mutate + bump Rev under the sim lock.
- Tick loop prunes entries whose ACID has left s.Aircraft.
- Dispatcher exposes 12 typed RPCs; each echoes a fresh state update.
- Client wraps each RPC; the matching scope-view wrappers are gone.
- TrackState loses the 12 per-controller fields; STARS reads go
  through sp.annotations(ctx, acid), writes through ctx.Client.SetTrack*.
- Preference-set load no longer merges scope-view fields; those
  belong to the (removed) foundation slice.

Closes plan tasks 1-9 of 2026-04-22-shared-track-annotations.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rewrite server/shared_tcw_integration_test.go for the annotation
model: two clients see each other's annotation mutations, rejoin
inherits persistent annotations across signoff, and tick-loop prune
drops entries for departed ACIDs.

Adds sim.Sim.PruneTCWDisplayAnnotationsForTest so cross-package tests
can drive the prune pass that otherwise fires only inside the private
updateState tick.

Closes plan task 10 of 2026-04-22-shared-track-annotations.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Shared annotation mutations (J-ring, cone, leader line, FDB/PTL/ATPA
toggles) rely on the periodic GetStateUpdate poll to mirror between
relief controllers at the same TCW. The previous 1s cap meant a
controller could wait up to a full second to see the other's change;
dropping the cap to 100ms keeps the sync visually tight while the
50ms floor and SimRate scaling still prevent overly aggressive
polling under fast-forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…llers

Revive the removed scope-view sync (range, pan, range-ring radius) as
an opt-in feature controlled by a "Sync Scope Setup" checkbox on the
Join as Relief dialog. When on, the relief controller's scope reads
and writes flow through the shared TCWDisplay.ScopeView; when off, they
stay on the local preference.

Plumbing: JoinSimRequest.SyncScopeState round-trips through ConnectToSim
to NewControlClient, which stashes the flag on ControlClient. STARS
reads go through sp.scopeRange/scopeUserCenter/scopeRangeRingRadius and
writes through sp.setScopeRange/setScopeUserCenter/setScopeRangeRingRadius,
each branching on the flag. Dispatcher and client wrappers for the 3
SetTCW* RPCs are restored; sim helpers mutate + bump Rev under the lock.

Tests: sim-level Sim.SetTCW* bump Rev and coexist with per-ACID
annotations; dispatcher-level two-clients-see-each-other and
survives-rejoin round trips; stars-level gating helpers under sync
on/off and seeded/unseeded shared state.

Covers the "Opt-in scope-view sync" follow-up from
docs/superpowers/plans/2026-04-22-shared-track-annotations.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…both sync

The previous commit wired scope-view sync through a per-client
`SyncScopeState` flag set only for reliefs who ticked the Join-as-Relief
checkbox. That gated both reads and writes on the same flag, which broke
bidirectional sync: the primary joined without the checkbox, so its
writes went to local only, shared state was never seeded, and the
opt-in relief always fell back to local. Manual test showed datablock
annotations synced but range/pan/range-ring did not.

Move the gate server-side: add `ScopeSyncEnabled` to `TCWDisplayState`,
flip it to true on ConnectToSim when `JoiningAsRelief && SyncScopeState`,
and have the STARS helpers read it off the shared state that ships with
every SimStateUpdate. Once any relief opts in, every controller at the
TCW — primary included — routes scope reads and writes through the
shared ScopeView. Sticky for the session; a subsequent plain relief
join does not clear it.

Tests: stars helpers now gate on `TCWDisplay.ScopeSyncEnabled` instead
of the client flag (4 cases); server integration test confirms the
ConnectToSim path flips the flag and makes it visible to the primary
via its own poll, and that a non-opt-in relief join leaves the flag on.
Replaces the per-field ScopeView RPCs (Range/UserCenter/RangeRingRadius)
with a single SetScopePrefsBlobRPC that ships the entire Preferences
struct as JSON. Local-only fields (CharSize, AudioVolume, DwellMode,
AutoCursorHome, CursorHome, DisplayDCB, DCBPosition,
RestrictionAreaSettings) are zeroed on encode and restored on apply so
they stay per-user. The reconciliation loop in syncScopePrefs runs
once per Draw: on enable the primary seeds and reliefs wait; after
that each side pulls when the server's Rev advances and pushes when
its local blob diverges from the last snapshot. A new IsRelief flag on
ControlClient breaks the first-tick tie so the primary's defaults
don't race against the relief's.

All the per-field scope helpers on STARSPane are gone; call sites read
ps.Range/UserCenter/RangeRingRadius directly again. Tests for the old
ScopeView round-trip are replaced with blob-based equivalents; the
sticky-enable integration test is kept.
Resolved sim/sim.go conflict: upstream's file restructure (04ae4fb)
moved GetStateUpdate / updateState to new locations in sim.go. Our
branch's three tiny hunks (TCWDisplay field on Sim, TCWDisplay in the
StateUpdate literal, pruneTCWDisplayAnnotations() call at end of
updateState) reapplied onto upstream's versions.

All other files auto-merged. Build + sim/server/client/stars tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A relief joining with the "Sync Scope Setup" checkbox off now opts
itself out locally, even if the TCW-wide ScopeSyncEnabled flag is
already on from an earlier relief. The server flag stays sticky; the
opt-out is a client-side gate in scopeSyncActive.

Pushes from syncScopePrefs are rate-limited to 100ms (matches the
state-update poll cap on the observer side) so an active pan/drag
doesn't flood the server with per-frame RPCs.

Also untracks docs/superpowers/ planning docs (already in
.git/info/exclude; these were committed before the rule).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Jud6969 Jud6969 changed the title Shared tcw display Shared Datablock Manipulations Across Same TCWs / Added Shared Scope Feature Apr 23, 2026
Comment thread sim/tcw_display.go
Comment thread stars/track_shared.go Outdated
Comment thread stars/track_shared.go Outdated
Comment thread stars/ui.go Outdated
Comment thread stars/track.go Outdated
Comment thread stars/cmdtools.go Outdated
Comment thread server/manager.go Outdated
Comment thread server/dispatcher.go Outdated
Comment thread client/client.go Outdated
Jud6969 and others added 3 commits April 23, 2026 20:59
…st test

track_shared.go only held two small helpers (annotations,
annotationsForTrack) that belong alongside trackStateForACID in
track.go, plus a string slice (SharedTrackAnnotationFields) whose only
consumer was a reflective test that verified the slice mirrored the
sim.TrackAnnotations struct. The slice drove no production behavior,
so both it and its test are gone.

Also revert a leftover rng-local in stars/ui.go that was introduced
when the now-deleted sp.scopeRange(c) helper was in play; inline
ps.Range is fine post-unified-blob refactor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Jud6969 and others added 26 commits April 25, 2026 23:46
These test files were development scaffolding from earlier iterations.
The functionality they covered is exercised through the existing
integration tests and manual verification — these duplicates add
nothing and clutter the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching between `./build.sh` and `./build.sh --vulkan` after a prior
build skips the whisper.cpp rebuild, leaving libggml.a out of sync with
the new vulkan tag and producing an opaque `undefined reference to
ggml_backend_vk_reg` link error. Document the fix (rm -rf
whisper.cpp/build_go) so users aren't stuck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements StartPTT/StopPTT/RecordPTTChunk/ClearTalkerForToken on *Sim,
guarded by the existing s.mu mutex. Adds activeTalker map[TCW]string field
to Sim struct and posts PeerVoiceEvents to the eventStream for fan-out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add PeerVoicePlayback (listener side of same-TCW PTT voice relay) to
client/voice.go. It subscribes to the client-local EventStream, drains
PeerVoiceEvents each frame, and feeds their PCM to AppendSpeechPCM.
Wire it into ControlClient: field initialized lazily in GetUpdates
(alongside transmissions.SetEventStream) and drained in updateSpeech.
Add two unit tests in client/voice_test.go; all 5 client tests pass.
Wire PTTRelay into uiHandlePTTKey: on press, request the talker slot
from the server; if granted, stream mic chunks via PTTRelay.SendChunk
in addition to the existing STT path; if denied, enqueue a 250 ms
heterodyne tone and skip mic capture for that press. Add pttDenied bool
to the ui struct and a lazy PTTRelay() accessor to ControlClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Treat each TCW as the authoritative radio bus: hold-state and per-transmission
start times live on TCWDisplayState, sync via existing snapshot mechanism.
Adds RadioHoldUntil and PlayAt fields, three server-side write paths (pilot
TX, PTT start, PTT release). Removes local hold-timer fields and the dead
ScopeSyncEnabled flag. Includes Phase 0 to fix the existing controller-voice
relay before testing the new radio-bus sync end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 instruments the existing controller voice relay to find why
real two-machine PTT produces silence. Phase 1 lands the radio-bus
sync feature: RadioHoldUntil on TCWDisplayState; PlayAt on Event;
three server-side write paths (pilot TX, PTT start, PTT release);
client TransmissionManager refactor to consume shared state; new
PilotVoicePlayback for observer-side TTS synthesis (today only the
requester synthesizes); cleanup of dead code (ScopeSyncEnabled,
HoldAfterTransmission, HoldAfterSilentContact). Ten implementation
tasks plus a manual end-to-end verification gated on Phase 0 fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Temporary DBG_VOICE logs at four points to identify why the same-TCW
controller voice relay produces silence in real two-machine testing
even though integration tests pass. Logs are removed once the bug is
diagnosed and fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RadioHoldUntil is the shared cutoff sim-time before which all
TransmissionManagers at this TCW must pause playback. Server-side
writers extend it; clients read it via the existing TCWDisplay
snapshot mechanism. ScopeSyncEnabled was already deprecated and
unused; deleted in the same commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlayAt carries the server-stamped sim-time at which listening clients
should start audio playback. Non-radio events leave it zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single helper in sim/radio.go owns the per-TCW timing arithmetic. It
stamps PlayAt = max(SimTime+200ms, current hold), advances RadioHoldUntil
to PlayAt + duration-estimate + post-event pad, and posts the event.
Refactors postReadbackTransmission and the in-line radio.go event-post
site to route through it. Constants moved here from the client TM.

Existing call sites had a mix of locked and unlocked callers, so this
introduces Foo / fooLocked pairs (matching the codebase pattern):
postRadioTransmission(Locked), postReadbackTransmission(Locked),
renderAndPostReadback(Locked). SayAgain/SayNotCleared/PilotMixUp use
the Locked variants since they already hold s.mu;
RunAircraftControlCommands stays unlocked and uses the wrappers.

Also tightens the doc comment on Event.PlayAt to explicitly call out
that zero means "not stamped" and consumers must gate on Type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StartPTT pushes RadioHoldUntil to SimTime+60s on grant (generous upper
bound while the controller talks). StopPTT and ClearTalkerForToken
replace it with SimTime+2s cooldown. Pilot transmissions on the same
TCW now park behind a live human PTT and resume after release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three scenarios: two subscribers see the same PlayAt for one event;
StartPTT advances RadioHoldUntil; different TCWs do not cross-pollute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today only the requesting client renders pilot TTS (via the
RunAircraftCommands RPC result). Observers on the same TCW saw the
text in the Messages pane but heard nothing. PilotVoicePlayback
subscribes to the local event stream and synthesizes audio for
RadioTransmissionEvents whose RequesterToken does not match the
local controller's token (matches the existing PeerVoiceEvent
SenderToken filter pattern).

Adds RequesterToken field on sim.Event so the requester can be
identified per event rather than via the shared destination TCP
(which is the same for all peers on a TCW). Wiring into
ControlClient and population of RequesterToken in dispatch paths
land in Tasks 8 and 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds SpokenVoice string on Event; postRadioTransmissionLocked fills
it from VoiceAssigner if the caller left it empty (and the sim has
one). Lets observer-side PilotVoicePlayback render the same voice
as the requester's RPC-result-driven synthesis without an extra
round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TransmissionManager.Update accepts SimTime + TCWDisplayState, gates on
the shared RadioHoldUntil instead of the per-instance holdUntil, and
defers each queue entry until SimTime >= PlayAt. PlayAt rides on the
queue entry from EnqueueReadbackPCM/EnqueueTransmissionPCM, which now
take it as an argument.

RPC results carry ReadbackPlayAt and ContactPlayAt; sim functions
return the PlayAt from postRadioTransmission and the dispatcher
propagates it to clients. synthesizeAndEnqueueReadback and
synthesizeAndEnqueueContact accept it; synthesizeAndEnqueueObserved
(the new observer-side path) uses it for same-TCW peers.

Removes HoldAfterTransmission, HoldAfterSilentContact, the local
post-event constants, and the per-TM holdUntil field. Single source
of truth for radio-bus timing now lives in sim/radio.go and
sim/voice.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Jud6969 Jud6969 marked this pull request as draft May 5, 2026 22:18
@Jud6969

Jud6969 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

I'm switching this back to a draft I'm not confident that this is complete or fully functioning. I have to update a lot since there have been a lot of commits since I've last tried this with someone else.

Jud6969 added 2 commits May 18, 2026 08:13
Reverts client/{client,stt}.go to upstream and drops the local-side TCW
radio bus consumers: PlayAt arg on synthesize* helpers and EnqueuePCM
calls, PTTRelay handshake in PTT key handling. Server-side TCW work and
the sim/radio.go three-value return are kept. Adds the TCW spec/plan
docs and the radio-bus integration tests to .gitignore so they stay
local only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

should see same TCW (data block positions, P cones, J rings, etc.) when sharing a scope

2 participants