The literal thing nothing on this dev machine can verify: whether the
build actually runs on Windows at all. Solved by having real CI do it —
ubuntu-latest and windows-latest both build the native Box3D/miniaudio
shims from source via CMake, build and test the full .NET solution, stage
a shippable configuration (engine.windowing/assets/render/input/physics/
audio + physics-demo-game — engine.editor deliberately excluded, that's
M4's actual "done when"), and then run it headless against samples/
PhysicsDemo for 200 frames, asserting DemoBox settled at y≈1.0 in the
resulting dump. Real physics, real scene load, real plugin loading,
proven on both platforms, not just built.
Required restructuring the native Content items into per-OS ItemGroups
(native/linux-x64/ vs native/win-x64/, selected via
$([MSBuild]::IsOSPlatform(...))): linux-x64's .so is committed (built and
verified here); win-x64's .dll is never committed — nothing here can
build or run one to verify — and only ever exists as something the
Windows job produces fresh, in-place, right before `dotnet build`.
Caught one real bug dry-running this exact staging locally before
trusting it to a workflow run: PluginHost.Load calls Assembly.
LoadFromAssemblyPath, which throws on a relative path — the verification
step's `--plugins ../../plugins` failed immediately with "is not an
absolute path." Every manual verification earlier this session happened
to always pass an absolute --plugins path, which is exactly why this
never surfaced before. Fixed by resolving to an absolute path before
invoking Engine.Host.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
native/audio-native/lingua_audio.c wraps miniaudio (vendored at pinned
release 0.11.25 — dual public domain/MIT-0, confirmed from the license
statement in the file itself) the same way lingua_physics.c wraps Box3D:
ma_engine_config/ma_sound_config are large structs with optional
callbacks, so nothing but plain int/float/bool/UTF-8-path scalars crosses
the P/Invoke boundary, and handles are this shim's own array-index
handles into a fixed ma_sound table, not miniaudio's own pointer types.
Same DllImport-not-LibraryImport choice too, for the same reason: zero
AllowUnsafeBlocks needed anywhere in this plugin.
One real addition beyond mirroring the physics shim: Lingua_Audio_Init
takes a useNullBackend flag. miniaudio's null backend runs the exact same
load/decode/mix/loop pipeline against a device that discards its output —
real coverage of this shim's logic without depending on real audio
hardware being present (most CI runners have none) or making an
automated test run produce actual sound, which nobody expects. Real
gameplay (AudioPlugin) always requests the real backend; only
Engine.Audio.Tests asks for the null one.
engine.audio adds an AudioSource component (ClipPath/Volume/Loop/
PlayOnAwake) and AudioWorld, which — same shape as PhysicsWorld, same
reason: no destruction event exists to hook — diffs Query<AudioSource>()
against its own tracked set every Stage.Update, loading a clip the first
time it sees one, applying Volume/Loop changes only when they actually
changed, and unloading sounds whose GameObject is gone. IAudioService
exposes Play/Stop/IsPlaying for gameplay code to trigger a sound instead
of it only ever firing on PlayOnAwake.
5 new tests, all against the null backend, all passed after fixing one
real bug they caught: AudioWorld originally re-attempted loading (and
re-warned about) a missing clip on every single Sync call instead of
once — MissingClip_WarnsAndDoesNotThrow failed on the first run with 2
warnings instead of 1, fixed by tracking failed-load GameObjects the same
way PhysicsWorld already tracks missing-collider ones.
Full suite: 86 tests. Not yet verified: actual audible playback through a
real device — the null-backend tests prove this plugin's own logic, but
nobody has listened to real output yet. Worth doing deliberately, not as
a surprise mid-session — flagging rather than just doing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N