ReviewOS

craft-native/craft

[S3-018] Complete native audio and content-import capabilities

#397
Open chris opened this 8 days ago ·
8 days ago

S3-018: Complete native audio and content-import capabilities

Parent and ownership

Parent epic: https://github.com/stacksjs/the-settlers-iii-remake/issues/6 Source owner: craft-native/craft. Master roadmap: https://github.com/stacksjs/the-settlers-iii-remake/issues/4.

Problem and observable result

Support concurrent effects and music. Handle interruption and denied file operations. Dispose imported content and audio resources correctly.

The current application is a browser prototype. Passing the Riverlands campaign does not establish original-game or native parity. This issue is complete only when its stated behavior has retained evidence.

Evidence and interfaces

Craft owns graphics, native hosting, and platform capabilities. stx owns compilation/reactivity. ts-css owns style semantics. Stacks owns build and release orchestration. The game consumes published contracts.

Pin the inspected revisions and fixture hashes in the implementation evidence. Original-game observations govern rules and timing. JSettlers is implementation evidence, not an authoritative balance specification. Unknown values remain explicitly unverified.

Acceptance criteria

  • Support concurrent effects and music.
  • Handle interruption and denied file operations.
  • Dispose imported content and audio resources correctly.
  • The visible result agrees with the underlying simulation or platform state, including failure paths.
  • Tests exercise the maintained public interface and retain reproducible fixtures or execution receipts.

Test matrix

  • Play overlapping effects with interruption.
  • Cancel import and repeat resource cleanup.
  • Exercise invalid input, cancellation or interruption, and repeated execution where this interface permits them.
  • Verify determinism/conservation for simulation changes and resource ownership/disposal for platform changes.
  • Run owning-repository lint, relevant type checks and tests. Capture actual native output for native capability claims.

Scope constraints

Implement this behavior as a complete verifiable slice. Do not silently expand into adjacent tickets or substitute simplified gameplay. Keep the shared TypeScript simulation, shared stx signals, and ts-css conventions. Native gameplay and HUD must not depend on a WebView. HomeOS reuse must not introduce a kernel dependency into the game. Existing unrelated edits and authored guidance must be preserved.

Blocked by

Upstream release and adoption

Fix and test the owning source repository, run its existing bun run release:patch when it is a Bun package repository, verify published artifacts, then install into Stacks and release Stacks when affected. Update the game from verified published packages. HomeOS uses its own build/source provenance.

No node_modules edits, copied runtimes, generated-source string patches, fabricated globals, successful no-op implementations, or skipped release gates. Push tested conventional commits as Chris to the owning origin/main without coauthor trailers or force pushes.

Completion evidence

  • Implementation commits and relevant upstream issue links.
  • Exact checks and results, fixture hashes, and screenshots/native execution evidence where applicable.
  • Published package versions, downstream lockfile identity, and release receipts when affected.
  • Remaining limitations stated explicitly. Partial work does not close this issue.
3 days ago

Additional retained release evidence for the document-picker lifecycle: the exact 0.0.396 UIKit job https://github.com/craft-native/craft/actions/runs/41071220677/job/124140302492 passed all 353 native unit tests but failed NativeRenderUITests/testDocumentPickerScreenReplacementDismissesAndNewScreenCanRetry at its initial Files-navigation-bar observation (line 100). The retained hierarchy still showed Picker ready and the picker controls; the test had not yet invoked its background/interruption handshake. An externally triggered unchanged rerun passed the full UIKit job and native summary. That pass does not identify the original cause; no timeout, cancellation assertion or observation was weakened. The earlier failure log is retained for a controlled presentation/dispatch investigation alongside this content-import work.

8 days ago

Native capability progress on codex/issues-388-400-344:

  • 6783c11a adds Android craft.files.pickFile parity: MIME/UTI/extension filters, multi-select, app-owned copies, per-file limits, cancellation cleanup, and typed failure paths. iOS already had the matching document picker.
  • 677444cc adds Android audio-focus denial, transient pause/resume, ducking, permanent-loss errors, focus disposal, and screen teardown cleanup.
  • f47f7968 adds iOS AVAudioSession interruption pause/resume behavior.

Evidence: Android builder packages/android/src/index.test.ts is 100/100; iOS builder packages/ios/src/index.test.ts is 128/128. These are generator/contract tests; a device/emulator run is still the remaining release evidence before closing this roadmap issue.

8 days ago

Update from the native verification pass:

  • iOS simulator NativeAudioUnitTests: 14/14 passed on iPhone 17 Pro (iOS 26.5).
  • iOS simulator FilesUnitTests: 17/17 passed on iPhone 17 Pro (iOS 26.5).
  • iOS generator suite: 128/128 passed.
  • Android builder suite remains 100/100 passed.
  • d7f56af0 restores the nested-press gesture delegate that a merge had dropped; the focused #403 regression test now passes.

The broad iOS native target still has four unrelated long-input geometry failures, so I am not claiming the entire 240-test target is green. Android device/emulator evidence is still unavailable on this host.

8 days ago

Additional owning-repo checks: bun run typecheck passed for TypeScript, create-craft, and ts-maps; Android builder 100/100 (1,774 expects); iOS generator 128/128 (1,431 expects); targeted iOS simulator audio 14/14 and files 17/17. bunx --bun pickier and git diff --check passed for the pushed iOS fix.

8 days ago

Dependency metadata is now current: the stale blocked by #394 edge has been removed; #394 is closed.

Verification remains:

  • iOS simulator audio 14/14 and files 17/17; iOS generator 128/128.
  • Android builder 100/100; Android device evidence is unavailable on this host.
  • The full SDK run reached 777 passing / 8 skipped with one Play-image emulator timeout; that test passes in isolation (1/1).
  • TypeScript/create-craft/ts-maps typecheck passes.

Implementation commits: 6783c11a, 677444cc, f47f7968. The issue remains open until native device evidence is available.

8 days ago

Follow-up implementation: 8d164c63 closes the remaining Android import-cancellation leak. Video copies now check the request token while streaming; image decoding/writing runs off the UI thread with the same cancellation guard; failed or canceled media writes delete their owned staging directory before the result is suppressed.

The Android generator suite passes 100/100 (1,783 expects), including the new generated-contract assertions. The native guide documents the cleanup guarantee.

8 days ago

Follow-up: 47715ecf aligns iOS native audio control results with the browser and Android contracts. pause, resume, setVolume, and stop now resolve false when the screen does not own that sound id, while stop-all remains successful and teardown stays idempotent. The iOS generator suite passes 128/128 (1,438 expects).

8 days ago

Native receipt after 47715ecf: generated iOS project compiled and the focused simulator run passed NativeAudioUnitTests 14/14 on iPhone 17 Pro (iOS 26.5). The broader 240-test unit target still has the known three long-input keyboard geometry failures; they are unrelated to native audio. Android device evidence remains unavailable on this host.

8 days ago

Pushed c880b08c on codex/issues-388-400-344 and verified it with the iOS generator suite (129 passed).

This closes a remaining ownership edge in iOS content import: if CGImageDestinationCreateWithURL or CGImageDestinationFinalize fails after the app-owned staging directory is created, the directory is now removed instead of leaking in tmp. The guide and generator contract test document/assert the cleanup. Android cancellation cleanup and iOS audio/checkpoint receipts remain covered by the earlier commits.

8 days ago

Latest verification on codex/issues-388-400-344: Android builder/native-shell/checkpoint suites passed 117/117, including generated native audio and file-import contracts; iOS generator suite passed 129/129, and the iOS native audio simulator receipt remains 14/14. Typecheck passed. Device-level Android playback/picker evidence still requires an emulator or device (adb is unavailable in this environment).

6 days ago

Two concrete lifecycle fixes are now on main, in focused commits:

  • 3edd036 (3 files): stopping/replacing/closing an iOS sound during loading settles its pending play with CANCELLED exactly once. Queued readiness callbacks check handle ownership; decode errors retain AUDIO_ERROR. The native regression failed before the fix for both looping and non-looping audio.
  • d49fde7 (4 files): cancelled picker/write/download completions discard newly owned files and their staging directories instead of merely suppressing the callback. Delivered files, unowned paths and persistent media-cache results are not deleted. The native regression reproduced the leak before the fix for single/multiple descriptors and cancellation on either side of queuing delivery.

Local iOS 26.5 receipts: NativeAudioUnitTests 26/26; FilesUnitTests 20/20, no skips. These cover loaded overlapping music/effect interruption state, replacement/close, decode failures, cancellation cleanup, real failed-copy/retry, and file ownership. Picker cleanup tests exercise the native delivery boundary and actual disk files, not automated interaction with a system document/photo picker.

Also passed: iOS builder 132/132, SDK 799 passed / 8 skipped, 397 release tests, typecheck, 43 package-surface checks after package builds, pinned formatting and lint (0 errors, 27 existing warnings). The initial parallel SDK run hit recorder/ADB subprocess deadlines; unchanged focused retry passed 11/11 and unchanged full retry passed 799/799 executed tests.

Existing Android evidence is stronger than the older comments here: downloaded the original API 34/35 artifacts from Mobile E2E 40374785445, verified their SHA256 against GitHub metadata and inspected their XML. Both contain 84 passing tests, zero failures/errors/skips, including preparing-sound replacement/cancellation, explicit pause/focus gain, native owned-file multipart upload, and voice cancellation cleanup. This does not establish all concurrent-audio, denied-picker and cancelled-import cases.

Retained fixture SHA256:

  • NativeAudioUnitTests.swift: a9281a63332bf75e0242b112754dc7b89331f1689b6f30a4aa1e724a6a8d120a
  • FilesUnitTests.swift: 9e8560a29d8b5fabed1a50191f0b5ad6f9f56e35dc1757699fcdf21c53c9d9e1 Android artifact IDs/digests:
  • 12676130423 (API 34): 18c77468d60a143da501ec0b1563a345bd3475ca389fdbb66a94507b38891bf9
  • 12674154222 (API 35): e5a1c28332e6dea2dbf872a520c4639a75253bf9526aa36c98283b511123e6e3

Leaving #397 open: latest-source CI (40500958372) and Mobile E2E (40500958516) are pending; these two new fixes are not published. Remaining acceptance includes the broader Android concurrent-playback/picker cancellation and denied-operation matrix, then publication and downstream game adoption. No native game parity or complete acceptance is claimed.

6 days ago

Android follow-up is pushed to main in two focused commits:

  • d28e116 (4 files): ducked gain now survives direct volume changes and fades; resume cannot bypass transient focus loss. Added native cases for overlapping music/effects, focus denial followed by retry, and closing overlapping pending loads.
  • 288a42f (5 files): cancelled/closed file completions delete undelivered owned files; copy loops observe host closure, not only request-token bookkeeping; denied image reads settle with READ_ERROR. Added real ContentResolver-stream fixtures for document denial/oversize rollback and retry, cancellation/closure mid-copy, and late write-result cleanup.

Local checks passed: Android generator 103/103; SDK 799 passed, 8 skipped; 397 release tests; typecheck; package builds and 43 publish-surface checks; pinned formatting; lint (0 errors, 27 existing warnings); generated native navigation fixture preparation. This machine has no Android SDK or Java runtime, so these are not claimed as new emulator passes.

Latest-source Android API 34/35 capability validation is queued: https://github.com/craft-native/craft/actions/runs/40512438002 (288a42f). Full Mobile E2E: https://github.com/craft-native/craft/actions/runs/40512436733. The older manual audio-only run was superseded by this combined run. Prior d49fde7 Mobile E2E failed in the iOS navigation UI test because XCTest timed out acquiring a background assertion; that is not an Android capability result.

Fixture SHA256:

  • NativeCapabilitiesTest.kt: 3f8ab32abd1a7145f3c8d77d15065de318396fbbca8797221c5a0f79a8d5dbfe
  • NativeImportProvider.kt: f95d3ed8a332b1b3711f2aa5482c792b56aa5f2ba6b50cdaf89a2342ab459574

Test limits: focus denial is injected at the platform request boundary; interruption callbacks are driven directly while exercising real MediaPlayers. File denial comes from a debug-only ContentProvider, not automated system-picker permission UI. No audible-output capture or native game parity is claimed. Keeping #397 open pending emulator receipts, publication and downstream acceptance.

5 days ago

Verified the remaining Android receipts and pushed one additional iOS fix to main: 7f452c5 (3 files).

Newly fixed, not yet published

An explicit pause/resume could bypass an active AVAudioSession interruption, and a sound created on an already-interrupted service could start immediately. The service now tracks session interruption separately from each handle's explicit pause. New sounds may prepare but remain stopped; ending the interruption respects shouldResume and preserves explicit pauses.

Red/green evidence on iPhone 17 Pro / iOS 26.5:

  • Old runtime with new regressions: 26 audio tests passed, both new tests failed for the expected playback/resume assertions.
  • Fixed runtime: 289 native unit tests passed, 0 failed/skipped, including all 28 audio and 20 file tests.
  • SDK: 800 passed / 8 skipped; iOS builder: 142 passed; release tooling: 399 passed; typecheck, pinned formatting, package builds and 43 package checks passed. Lint: 0 errors, 26 existing warnings.
  • Retained local receipts: /tmp/craft-397-interruption-red.xcresult and /tmp/craft-397-interruption-green.xcresult.
  • Audio fixture SHA256: f773deeac78178fa1c7b1c61cfa82144066814952eb194686919d297f6d426db.

Previously shipped Android work is now verified

At 52779dd / v0.0.377, CI, Mobile E2E, and publication all succeeded.

Downloaded original artifacts, verified GitHub SHA256 digests, and inspected the test XML: 90 tests passed on each Android API 34 and 35, with no failures/errors/skips. This includes overlapping ducked playback/fades, focus denial/retry, closing pending sounds, denied/oversized imports, mid-copy cancellation/closure, and undelivered owned-file cleanup. The published @craft-native/android@0.0.377 sound/capability templates are byte-identical to that tested revision; registry tarball integrity also matches.

Artifact IDs / SHA256:

  • API 34: 12797503190 / 9e9bf2573384cfb1e66dd19bd574e0313678070f225d64fe8f2bc0b86e733570
  • API 35: 12798191404 / 35a4dce97740a23aae66102c99e33af0df58a0ee37acd60709894793f77b2888
  • The same release's retained iOS result contains 292 passing tests (284 unit + 8 UI).

Correction to the earlier queued-run comment: manual run 40512438002 exercised API 34 only. The release run above supplies the verified two-API matrix.

Still open

These tests use real native players and file streams, but inject interruption/focus callbacks and denied-provider behavior; they do not establish physical interruption events, system-picker permission UI, captured audible output, or downstream game acceptance. New iOS coverage applies to a service that observed the interruption. The new commit still needs CI and a future approved publication; no release was created in this pass. Keeping #397 open for that remaining acceptance and downstream adoption.

5 days ago

Follow-up pushed to main: b77e4a3 (3 files).

The previous interruption fix was scoped to a service that had received the notification. A new screen created mid-interruption still started playback because its fresh service defaulted to an uninterrupted session. AVAudioSession is shared across the app.

The sound services now share one process-lifetime observer, initialized with the first service, and a weak service registry. New screens inherit an observed active interruption; ending it clears the shared state even between screen lifetimes. Explicitly paused handles still require explicit resume. The registry does not keep old services alive.

Evidence:

  • Pre-fix native audio run: 29 passed, 1 failed. The new-screen regression observed player rate 1 during interruption and an incorrectly successful manual resume.
  • Final full native unit run: 291 passed, 0 failed/skipped, including all 30 audio and 20 file tests, on iPhone 17 Pro / iOS 26.5.
  • The first fixed run exposed two premature lifetime assertions: Foundation's weak-registry enumeration returns an autoreleased strong snapshot. Confirmed with an isolated Foundation reproduction; the final tests drain that temporary snapshot and retain the strict weak-reference release assertions. No playback assertions were removed.
  • SDK 800 passed / 8 skipped; iOS builder 142 passed; release tooling 399 passed; typecheck, pinned formatting, package builds and 43 package checks passed; lint 0 errors / 26 existing warnings.

Retained receipts:

  • /tmp/craft-397-session-red.xcresult
  • /tmp/craft-397-session-green.xcresult (initial lifetime assertions)
  • /tmp/craft-397-session-verified.xcresult (final 291-pass run)
  • Final NativeAudioUnitTests.swift SHA256: 10bb57de4d0a27fe536823945101ea8edd403442e4d94bd5744ceb249f2df45f.

This remains notification-driven simulator coverage with real AVPlayers, not physical interruption or audible-output capture. No new release; #397 remains open for CI, future approved publication and the previously stated downstream acceptance.

5 days ago

Pushed 98a704c directly to main (4 files), fixing iOS owned-file deletion and cancelled-import cleanup.

Previously CraftNativeFiles.remove recursively deleted the requested file's parent directory, swallowed any deletion error, and returned true. The new native regressions reproduced:

  • successful-looking removal after a real filesystem permission denial, while the file remained;
  • deletion of sibling content, including during cancelled-import cleanup;
  • deletion of the shared temporary root and another import when removing a file directly inside that root.

Removal now uses unlink for only the validated owned file and returns false if that operation fails. It attempts non-recursive rmdir only for a non-root parent; siblings cannot be swept away by this cleanup. An optional parent-pruning failure does not turn a successful file deletion into failure. Existing missing/unowned-path rejection remains intact.

Six added native tests cover permission denial/retry through the maintained capability dispatcher, sibling/root preservation, symlinks escaping ownership, cancelled-import cleanup, and successful file deletion despite denied empty-parent pruning. These use actual simulator filesystem permissions, not an injected no-op remover. The generator test now checks bounded deletion, and its existing photo-cleanup assertion is scoped to the photo encoder instead of counting unrelated deletion calls throughout the file.

Validation:

  • Before fix: 20 existing file tests passed, all 4 initial regressions failed as expected.
  • Final iPhone 17 Pro / iOS 26.5 run: 310 passed, 0 failed/skipped (300 unit + 10 UI), including all 26 file tests.
  • iOS generator: 143 passed. SDK final unchanged run: 800 passed / 8 skipped / 0 failed.
  • Two earlier full SDK attempts hit existing subprocess deadlines in different fixtures (ADB reconnect and mock recorder startup). Both focused unchanged retries passed (5 and 6 tests), followed by the fully passing SDK run. No deadlines/assertions were relaxed.
  • Release-tooling/contract tests: 400 passed. Typecheck, pinned Zig formatting, package builds, 43 package checks and lint passed (0 errors / 26 existing warnings).

Retained local receipts:

  • /tmp/craft-397-removal-red.xcresult
  • /tmp/craft-397-removal-green.xcresult

SHA256:

  • CraftNativeFiles.swift: 20d07cab6e5328e7088f122aca4302dac54f4867a0df3f2b13047ae799a1953d
  • FilesUnitTests.swift: 12394e9dd9d8a3d517fa733b6d98131b13fd5edb7b261dcd6804bcb8ba9bbb6f

Remote CI is pending. No release/tag created. This is an iOS cleanup correction, not new Android, system-picker permission UI, physical audio or downstream game acceptance. Cancellation still cannot guarantee deletion when the OS denies it; the documentation now states that limit. Keeping #397 open for its remaining broader acceptance/publication work.

5 days ago

Pushed 2a27861 directly to main (5 files): bounded, cancellable iOS document/video imports.

Previously these imports checked source size once, then used an uninterruptible FileManager.copyItem. Both paths now share a chunked copy:

  • Validate the opened descriptor as a regular file; reject directories, FIFOs/special files and selected symlinks without blocking on a FIFO.
  • Enforce maxBytes on bytes actually read, including when the source grows after the initial size check; at most 64 KiB per chunk and no over-limit bytes written.
  • Poll the existing request cancellation token without consuming it. Cancellation (including screen teardown) stops further copying and removes partial staging; multi-document cancellation/read failure also rolls back earlier successful copies.
  • Preserve arbitrary document content types and the existing MP4/M4V/MOV/WebM video extension allowlist. Final delivery still cleans results cancelled after copying completes.

Six new native tests exercise actual files: regular-file validation, exact/empty/oversize boundaries, deterministic source growth after a copied chunk, mid-copy cancellation through CraftNativeActions.cancel plus retry, pre-cancelled picker admission, and multi-file rollback. Generator coverage checks that both document and video picker delegates use the bounded cancellation path.

Validation on iPhone 17 Pro / iOS 26.5:

  • Focused file suite: 32 passed, 0 failed/skipped.
  • Initial full run: 314 passed / 2 failed / 0 skipped. All 10 UI tests passed. The failures were unchanged tests NativeAudioUnitTests.testInterruptionEndsBetweenScreenServiceLifetimes (weak lifetime assertion) and ScriptThreadUnitTests.testAnimationFramesReachTheScriptThread (duplicate initial zero timestamp).
  • Full native unit suite retried with unchanged sources/assertions: 306 passed / 0 failed/skipped, including both prior failures. This is a passing unit retry plus the initial passing UI suite, not a claim that the initial full run was green.
  • SDK: 800 passed / 8 skipped / 0 failed.
  • iOS generator: 143 passed.
  • Release-tooling/contract suite final unchanged retry: 400 passed. Initial run was 399 passed / 1 failed from the existing managed-Git fixture's five-second subprocess timeout.
  • Typecheck, pinned Zig formatting, npm package builds and 43 package checks passed. Lint: 0 errors / 26 existing warnings. No timeout or assertion was relaxed.

Retained local receipts:

  • /tmp/craft-397-bounded-focused.xcresult
  • /tmp/craft-397-bounded-full.xcresult (includes the two failures)
  • /tmp/craft-397-bounded-units-retry.xcresult

SHA256:

  • CraftNativeFiles.swift: 8d108141fdfb1f008d20ce853bc7ad39d24382581f242a47928885276e6ee40b
  • FilesUnitTests.swift: a12bdb3d80a0908f7b2993ecd5e370b5016999230d246950ea58645d694b1e46

Limits: cancellation does not interrupt an already-running OS read or dismiss the system picker/provider UI. Cleanup remains best effort if the filesystem refuses deletion. These are deterministic native filesystem/cancellation regressions, not automated system-picker permission UI or physical audio acceptance. No Android changes, release or tag. Remote CI is still to be validated; #397 remains open for its broader acceptance and future approved publication/downstream adoption.

Sign in to comment on this issue.