Shared Datablock Manipulations Across Same TCWs / Added Shared Scope Feature#872
Draft
Jud6969 wants to merge 78 commits into
Draft
Shared Datablock Manipulations Across Same TCWs / Added Shared Scope Feature#872Jud6969 wants to merge 78 commits into
Jud6969 wants to merge 78 commits into
Conversation
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>
mmp
reviewed
Apr 23, 2026
…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>
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>
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. |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
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