Skip to content

feat(tool): a beacon sound the Mission Maker chooses (FEAT-CUSTOM-BEACON-SOUNDS) - #112

Merged
davidp57 merged 3 commits into
developfrom
feature/custom-beacon-sounds
Aug 9, 2026
Merged

feat(tool): a beacon sound the Mission Maker chooses (FEAT-CUSTOM-BEACON-SOUNDS)#112
davidp57 merged 3 commits into
developfrom
feature/custom-beacon-sounds

Conversation

@davidp57

@davidp57 davidp57 commented Aug 9, 2026

Copy link
Copy Markdown
Member

Why

radioSound / radioSoundFC3 were free-text boxes. Typing a name changed what the engine plays without putting any such file in the mission, so using your own beacon tone meant editing the .miz by hand — the manual step FEAT-ONE-CLICK-INSTALL exists to remove. Zip's verdict on the text box ("ça n'a aucun sens") was fair.

Each setting now gets a Default / Custom picker. A chosen .ogg is read at selection, held in the session, and written into the archive with its resource key and preload trigger exactly like the bundled ones.

The model (ADR 0012, grilled before any code)

  • A chosen file enters the mission under a reserved name (CTLD_beacon_custom.ogg), so the configuration value itself says the sound is customised. No second key that could contradict the engine — and no misreading of the ordinary case of a Mission Maker whose own file is called beacon.ogg, which "differs from the default" would silently overwrite.
  • The original name survives as a schema-only label (radioSoundOriginalName). Catalogued it would be a parameter under ADR 0011 Addendum 1, so completeness would demand it and every pre-lot configuration would report a missing setting at mission start — FIX-TOOL-I18N-LANG's wall, paid once already.
  • The bytes are read at selection, not at install. That is what makes an installed mission reconfigurable: reopening the .miz recovers the sound, so it reinstalls on another machine, with the file the Mission Maker chose long deleted. A path would rot; a .miz has no path to offer at all.
  • A .yaml cannot carry a binary, so reopening one blocks the install with a validation error naming the file to pick again — unless the target mission already holds it (a reinstall, or a file added through the Mission Editor).
  • OggS is checked; a renamed .mp3 would give silent beacons discovered in flight. No size cap — the size is reported instead.
  • Nothing is deleted from a Mission Maker's archive: resource keys keep deriving from the file name, so a default ↔ custom round trip leaves a dead .ogg until the Mission Editor drops it. Deliberate.

Tests

tests/test_sounds.py — 13 tests, the load-bearing one being the full round trip: install with a custom sound → unlink() the source file → session.reset() → reopen the .miz → the bytes and the label come back → reinstall into a different mission and assert the file lands byte for byte.

SoundPicker.test.ts — 7 component tests: both sources offered, the original name shown (not the reserved one), the warning when the file is no longer held, cancellation, and a backend refusal surfaced rather than swallowed.

278 Python tests, 128 frontend tests, ruff + mypy clean.

UI binding

editor: sound in the schema drives the picker — no setting name in a component (FEAT-EDITOR-COVERAGE banned that). hidden: true keeps the labels out of the families and out of search, so nobody hand-edits a label into disagreeing with the sound it describes.

Typing a file name by hand is unchanged and still supported. Documented EN + FR in the tool guide and the configuration reference.

🤖 Generated with Claude Code

Summary by Sourcery

Add support for mission-maker-selected beacon sound files, with backend, validation, install, and UI changes to treat them as first-class, reconfigurable resources.

New Features:

  • Introduce a Default/Custom beacon sound picker in the CTLD tools UI, backed by schema metadata and dedicated sound endpoints.
  • Allow installing custom .ogg beacon sounds into missions under reserved names, with their original filenames preserved as labels and reported after install.

Bug Fixes:

  • Prevent missions from ending up with missing or silently non-playable custom beacon sounds by validating availability and rejecting non-Ogg files.

Enhancements:

  • Extend validation, schema, and session state to track custom beacon sounds and hidden label metadata, enabling reconfiguration from reopened missions.
  • Update install logic to write either bundled or custom beacon sounds from session-held bytes and to expose per-sound details in the install report.
  • Refine frontend model to respect hidden schema keys, avoid exposing tool-written labels, and drive editors from schema-specified types rather than hard-coded setting names.

Documentation:

  • Document beacon sound configuration and the new picker behaviour in the mission-maker guides (EN/FR), including reserved filenames and YAML limitations.

Tests:

  • Add Python tests covering the custom beacon sound lifecycle end-to-end, including install/reopen/reinstall and YAML edge cases.
  • Add frontend tests for the SoundPicker component and schema/editor handling, ensuring UI behaviour and i18n parity.
  • Extend existing schema and web app tests to cover sound editor metadata and hidden label treatment.

@sourcery-ai

sourcery-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements custom beacon sound support end-to-end: schema metadata for a sound picker and hidden labels, backend session support for storing sound bytes and reading them from missions, validation and install logic to ensure custom sounds are present and written under reserved names, new REST APIs and Svelte UI components for choosing/resetting sounds, plus documentation and tests.

Sequence diagram for reopening a mission to recover custom beacon sounds

sequenceDiagram
  actor MissionMaker
  participant WebBackend as WebAppBackend
  participant Session
  participant Install as InstallModule

  MissionMaker->>WebBackend: GET /api/dialog/miz (pick_miz)
  WebBackend->>Dialogs: pick_miz()
  Dialogs-->>WebBackend: miz_path

  MissionMaker->>WebBackend: POST /api/load_path (loadPath)
  WebBackend->>Session: load_path(miz_path)
  Session->>Install: read_config(miz_path)
  Install-->>Session: FoundConfig(catalog)
  Session->>Session: _catalog = FoundConfig.catalog
  Session->>Install: read_sounds_from_miz(miz_path, _catalog)
  Install-->>Session: {setting: bytes} for customised sounds
  Session->>Session: _sounds = read_sounds_from_miz(...)

  MissionMaker->>WebBackend: GET /api/sounds (get_sounds)
  WebBackend->>Session: session.catalog
  WebBackend->>WebBackend: _sound_state()
  WebBackend->>Session: session.sound(setting)
  WebBackend->>WebBackend: _sounds_available(session.mission_path)
  WebBackend-->>MissionMaker: {sounds: SoundState[]}
Loading

File-Level Changes

Change Details Files
Backend sound handling: session storage, install/validate integration, and mission I/O for custom beacon sounds.
  • Extend validation to consider availability of customised sounds and emit blocking findings when a configured custom sound cannot be produced.
  • Add helpers to derive reserved custom sound names from the catalog, read custom sounds from .miz archives, and resolve which sound files to write based on catalog+held bytes.
  • Refactor install logic to accept catalog and held_sounds, write either bundled or custom sounds under appropriate resource keys, and report per-sound metadata in InstallReport.
  • Track custom sound bytes in Session (set/sound/drop/sounds), loading them from missions on open and clearing them on reset/default/yaml load.
tools/ctld-tools/ctld_tools/validate.py
tools/ctld-tools/ctld_tools/install.py
tools/ctld-tools/ctld_tools/web/state.py
tools/ctld-tools/tests/test_sounds.py
New sound-related web APIs and injection behaviour to support the UI picker and target-aware validation.
  • Expose editor and hidden metadata on schema keys via /api/schema and Schema.editor/hidden helpers.
  • Add endpoints to query current sound state, choose a custom sound via native file dialog, and reset to default, including Ogg signature checking.
  • Make validation and injection use sounds_available derived from session-held sounds and sounds already present in the target mission, and pass merged held_sounds into install.
  • Return written sound metadata in /api/inject responses.
tools/ctld-tools/ctld_tools/web/app.py
tools/ctld-tools/ctld_tools/schema.py
tools/ctld-tools/tests/test_schema.py
tools/ctld-tools/tests/test_web_app.py
tools/ctld-tools/ctld_tools/web/dialogs.py
Frontend support for beacon sound picker, schema-driven editor selection, and exclusion of tool-written labels from UI/search.
  • Extend SchemaKey with editor/hidden, adjust classify/settingKeys to ignore hidden keys in families and search, and update tests accordingly.
  • Add SoundState/InstalledSound types and REST helpers (getSounds, chooseSound, resetSound).
  • Integrate SoundPicker into SettingRow based on meta.editor === 'sound', including sound state wiring, refresh cycle, and reset-button behaviour.
  • Wire sound state refresh into app lifecycle (after load and on sound change), and adjust search to use settingKeys(snapshot, schema).
tools/ctld-tools/web/src/lib/api.ts
tools/ctld-tools/web/src/App.svelte
tools/ctld-tools/web/src/lib/model.ts
tools/ctld-tools/web/src/lib/model.test.ts
tools/ctld-tools/web/src/lib/SettingRow.svelte
tools/ctld-tools/web/src/lib/SoundPicker.svelte
tools/ctld-tools/web/src/lib/SoundPicker.test.ts
Resource and schema definitions for beacon sounds, including reserved custom names, ownership, Ogg validation, and hidden original-name labels.
  • Introduce SOUND_SETTINGS describing default/custom filenames and associated original-name label keys, plus OWNED_SOUND_NAMES and is_ogg helper.
  • Update sound key generation to derive resource keys from filenames via sound_key, so custom sounds get distinct resource keys.
  • Extend CTLD_config_schema.yaml with editor: sound for sound settings and hidden label entries for original file names, and document editor/hidden header semantics.
  • Annotate configuration docs to explain reserved custom filenames and link to beacon sound docs.
tools/ctld-tools/ctld_tools/resources.py
src/CTLD_config_schema.yaml
docs/mission-maker/configuration.md
docs/mission-maker/configuration.fr.md
User-facing documentation and backlog updates describing custom beacon sound feature and its lifecycle.
  • Add EN/FR mission-maker guide sections on beacon sounds, describing default/custom picker, reserved filenames, .yaml limitations, and behaviour when missions already hold files.
  • Extend UI strings with sound-picker labels and messages, including missing-file and not-Ogg warnings.
  • Update CHANGELOG and backlog tickets/README to mark FEAT-CUSTOM-BEACON-SOUNDS as done and describe behaviour/acceptance criteria.
docs/mission-maker/ctld-tools.md
docs/mission-maker/ctld-tools.fr.md
tools/ctld-tools/web/src/lib/strings.ts
CHANGELOG.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/01-schema-sound-editor-and-labels.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/02-session-holds-the-sound-bytes.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/03-install-the-chosen-sound.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/04-validate-the-sound.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/05-the-picker-in-the-interface.md
.backlog/FEAT-CUSTOM-BEACON-SOUNDS/tickets/06-documentation.md
.backlog/README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

davidp57 added a commit that referenced this pull request Aug 9, 2026
Set in the lot's own PR, per the convention — not left for a post-merge
commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@davidp57
davidp57 force-pushed the feature/custom-beacon-sounds branch from ed6e2f6 to 56cdba9 Compare August 9, 2026 08:57

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The validation error for missing custom sounds (validate.sound.missing) always reports the reserved in-mission filename; consider including the original disk name from the label when available so the Mission Maker can more easily identify which file to reselect.
  • Both _sounds_available and _target_sounds reopen and scan the target .miz independently during inject; you could refactor to share a single zip read per request to avoid duplicated I/O on large missions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The validation error for missing custom sounds (`validate.sound.missing`) always reports the reserved in-mission filename; consider including the original disk name from the label when available so the Mission Maker can more easily identify which file to reselect.
- Both `_sounds_available` and `_target_sounds` reopen and scan the target `.miz` independently during inject; you could refactor to share a single zip read per request to avoid duplicated I/O on large missions.

## Individual Comments

### Comment 1
<location path="tools/ctld-tools/web/src/lib/model.ts" line_range="71-72" />
<code_context>
+ * `schema` is optional only so old call sites keep working; pass it, or search will offer the
+ * hidden tool-written keys that `classify` deliberately leaves out of the families.
+ */
+export function settingKeys(snap: Snapshot, schema?: SchemaInfo): string[] {
+  return snap.keys.filter((k) => !isStructured(snap.values[k]) && !schema?.keys[k]?.hidden)
 }

</code_context>
<issue_to_address>
**suggestion (bug_risk):** Making `schema` optional for `settingKeys` can lead to inconsistencies with family classification

Because hidden-key filtering depends on `schema`, omitting it means search will surface hidden tool-maintained keys that `classify` intentionally excludes, reintroducing the inconsistency you just fixed. To avoid this, consider making `schema` required and updating call sites, or at least adding a dev-time assertion that there are no hidden keys when `schema` is not provided.

Suggested implementation:

```typescript
export function settingKeys(snap: Snapshot, schema?: SchemaInfo): string[] {
  if (!schema) {
    // Dev-time assertion: callers should always pass schema so hidden tool-maintained keys
    // stay consistent with family classification.
    if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
      throw new Error(
        'settingKeys: schema is required to avoid exposing hidden tool-maintained keys. ' +
          'Update call sites to pass SchemaInfo.',
      )
    }

    // In production, fall back to the legacy behavior (no hidden-key filtering) to avoid
    // breaking older call sites, at the cost of possible inconsistency.
    return snap.keys.filter((k) => !isStructured(snap.values[k]))
  }

  return snap.keys.filter(
    (k) => !isStructured(snap.values[k]) && !schema.keys[k]?.hidden,
  )
}

```

If your tooling allows it, you may also want to:
1. Tighten the TypeScript signature to `schema: SchemaInfo` once all call sites are updated and the dev-time error stops firing.
2. Optionally, add a TODO above the function indicating that the optional `schema` parameter is temporary and will be made required after legacy call sites are migrated.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +71 to +72
export function settingKeys(snap: Snapshot, schema?: SchemaInfo): string[] {
return snap.keys.filter((k) => !isStructured(snap.values[k]) && !schema?.keys[k]?.hidden)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Making schema optional for settingKeys can lead to inconsistencies with family classification

Because hidden-key filtering depends on schema, omitting it means search will surface hidden tool-maintained keys that classify intentionally excludes, reintroducing the inconsistency you just fixed. To avoid this, consider making schema required and updating call sites, or at least adding a dev-time assertion that there are no hidden keys when schema is not provided.

Suggested implementation:

export function settingKeys(snap: Snapshot, schema?: SchemaInfo): string[] {
  if (!schema) {
    // Dev-time assertion: callers should always pass schema so hidden tool-maintained keys
    // stay consistent with family classification.
    if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
      throw new Error(
        'settingKeys: schema is required to avoid exposing hidden tool-maintained keys. ' +
          'Update call sites to pass SchemaInfo.',
      )
    }

    // In production, fall back to the legacy behavior (no hidden-key filtering) to avoid
    // breaking older call sites, at the cost of possible inconsistency.
    return snap.keys.filter((k) => !isStructured(snap.values[k]))
  }

  return snap.keys.filter(
    (k) => !isStructured(snap.values[k]) && !schema.keys[k]?.hidden,
  )
}

If your tooling allows it, you may also want to:

  1. Tighten the TypeScript signature to schema: SchemaInfo once all call sites are updated and the dev-time error stops firing.
  2. Optionally, add a TODO above the function indicating that the optional schema parameter is temporary and will be made required after legacy call sites are migrated.

davidp57 and others added 3 commits August 9, 2026 10:59
Carried here rather than into a one-line PR of its own, per Zip. The
convention is to set the index line inside the lot's own PR; #110
shipped without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…CON-SOUNDS)

radioSound and radioSoundFC3 were text boxes: typing a name changed what
the engine plays without putting any such file in the mission, so using
your own beacon tone meant editing the .miz by hand — the manual step
FEAT-ONE-CLICK-INSTALL exists to remove.

Each now gets a Default / Custom picker. A chosen .ogg is read at
selection, kept in the session, and written into the archive with its
resource key and preload trigger like the bundled ones.

The model, per ADR 0012: a chosen file enters the mission under a
reserved name (CTLD_beacon_custom.ogg), so the configuration value
itself says the sound is customised — no second key that could
contradict the engine, and no misreading of a Mission Maker whose own
file is called beacon.ogg. The name it had on disk survives as a
schema-only label, never catalogued: a catalogue entry would be a
parameter under ADR 0011 Addendum 1, so completeness would demand it and
every pre-lot configuration would report a missing setting at mission
start (FIX-TOOL-I18N-LANG's wall).

Reading the bytes at selection rather than at install is what makes an
installed mission reconfigurable: reopening the .miz recovers the sound,
so it reinstalls on another machine with the original file deleted. A
.yaml cannot carry a binary, so reopening one blocks the install with a
validation error naming the file to pick again — unless the target
mission already holds it. An OggS signature check catches the renamed
.mp3 that would give silent beacons discovered in flight; no size cap,
the size is reported instead.

The picker is bound to `editor: sound` in the schema, never to a setting
name in a component (FEAT-EDITOR-COVERAGE), and `hidden: true` keeps the
labels out of the families and out of search.

Typing a file name by hand still works for a sound added through the
Mission Editor. Documented EN + FR, in the tool guide and the
configuration reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Set in the lot's own PR, per the convention — not left for a post-merge
commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@davidp57
davidp57 force-pushed the feature/custom-beacon-sounds branch from 56cdba9 to c2f1d8f Compare August 9, 2026 09:00
@davidp57
davidp57 merged commit b5e7796 into develop Aug 9, 2026
8 checks passed
@davidp57
davidp57 deleted the feature/custom-beacon-sounds branch August 9, 2026 09:03
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.

1 participant