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
Lingua Engine
A modular, plugin-first game engine built around one idea: the kernel is a shared language, not a shared implementation. Everything the engine can do — rendering, physics, audio, even the editor itself — is a plugin that speaks that language. The kernel only defines the vocabulary plugins use to understand each other.
Built for Linux and Windows, in C#/.NET, with two goals that shape every design decision:
- Fast iteration. No Unity-style domain reload. Plugins hot-reload their
compiled code without resetting game state, because state never lives in
plugin code to begin with — see
docs/kernel-contract.md. - A small, frozen kernel. Everything else — including the parts most engines treat as core — is a plugin, versioned and replaceable per project.
How this gets built
Most of the code here — kernel and plugins alike — is written by an LLM
coding agent rather than by hand. That's not incidental: it's a design
input. It's why the object model is GameObject/Component instead of a
hand-rolled ECS, why registration is verbose and explicit instead of
convention-based, and why the engine has a headless, scriptable
introspection surface no classic editor bothers with — see
docs/kernel-contract.md.
Status
M0 done. The kernel — World (GameObject/Component, type-indexed
queries), Schedule (stage execution, conflict batching, debug-mode access
enforcement), PluginHost (two-ALC load/unload, verified leak-free over
200 cycles), and a headless CLI (engine run --headless ... --dump) — all
exist and are tested. The full agent loop from
docs/kernel-contract.md#7
runs end to end.
M1 done. engine.windowing, engine.render (a real shader-drawn
triangle, not just a clear color), and engine.input all exist over
Silk.NET. The milestone's actual claim — edit a plugin's code, rebuild just
it, reload it while a real window stays open, see the change with no app
restart — is proven against a live GL context: two PNGs of the same
running window, before and after a live reload, orange triangle then green,
same process the whole time. IScreenCapture (engine.render) reads the
frame back from the GPU and writes it to a file with a hand-rolled PNG
encoder — no SixLabors.ImageSharp (its license isn't MIT/Apache) and no
desktop screenshot tool, so this is checkable without a screen at all,
exactly the introspection story docs/kernel-contract.md#7 argues for.
The kernel is closed. All four questions the original design left open
— Time/Log's home, whether the Event Bus is real infrastructure or
event-components, whether frame stages are fixed or plugin-extensible, and
the data-oriented-fast-path question — are resolved, each with working code
behind it, not just an answer written into the doc. Time and the Event
Bus (Publish/Subscribe, leak-safe the same way Schedule already is)
both shipped; sandbox.echo subscribes to PluginLoaded for real, so the
200-cycle leak test now proves EventBus doesn't leak too, not just
Schedule. See the resolutions in
docs/kernel-contract.md — one of the four
(the fast path) is deliberately still open, but with a concrete trigger
condition instead of a deadline, not left vague.
M2 done. World actually saves and loads now (SceneFormat,
replacing the old introspection-only WorldDumper — there was never a
real reason for "what an agent reads to check a frame" and "what a scene
file is" to be different shapes). Verified beyond round-trip unit tests:
two separate CLI runs against the same scene file, second one picking up
right where the first left off, component state and all.
engine.assets hot-reloads textures from disk — the actual "done when"
for M2. engine.render's triangle became a textured quad; swap the PNG
file on disk while the app is running and the picture changes with no
restart, no manual reload command, just a FileSystemWatcher noticing
and IEventBus carrying TextureReloaded from engine.assets to
engine.render. Verified the same honest way as M1 — real screenshots,
before and after, same running process — plus two things caught and fixed
along the way rather than papered over: a PNG decoder was needed (no
SixLabors.ImageSharp, same licensing reason as the encoder — it's a
second, independent implementation of the format, tested against all five
PNG filter types, not just the one this codebase's own writer produces),
and a real hang, not a hypothetical one: SwapBuffers blocking forever
once VSync had nothing to wait on — reproduced by locking the screen,
fixed by turning VSync off, since nothing here needs frame pacing yet.
M3 done. The editor is engine.editor, a plugin like any other — no
special-cased editor layer in the kernel. An ImGui overlay (Silk.NET.OpenGL.
Extensions.ImGui) draws over the live scene; getting it to actually appear
in the same frame (not delayed by one) needed splitting engine.render's
old Draw-then-SwapBuffers system in two, so a new Stage.Present could run
the swap after every Stage.Render system — this plugin's draw and
engine.editor's ImGui pass both — had drawn into the same back buffer.
See the "Frame stages" resolution in
docs/kernel-contract.md for why adding a stage
was still the right call under a "fixed, kernel-defined" rule.
Hierarchy and Inspector both work off reflection, not per-component-type
code: HierarchyPanel walks IWorld.Roots directly, InspectorPanel
enumerates a selected GameObject's Transform and every attached
Component's public fields via FieldInfo, live-editable for
int/float/bool/string/Vector3. A brand new component type in any plugin
gets an Inspector for free the moment it's attached.
Play/Stop is IWorld.Snapshot()/Restore() — a scene-format dump taken on
Enter, restored on Exit — plus Engine.Host skipping Stage.Update outside
Play. Nothing here is a domain reload; see
docs/kernel-contract.md §5.
M3's actual "done when," entering Play in under 100ms, is proven twice: a
kernel-level timed test and a real editor run logging 13ms.
The gizmo is a real 3-axis translate handle, not a flat 2D overlay — chosen
deliberately over a scoped 2D version specifically so a screen-space drag
means something: it projects the selected GameObject's world position
through the actual camera's View/Projection and reads the drag back the
same way. The underlying math (GizmoMath) has no GL or ImGui dependency
and is unit-tested on its own — including the case a screenshot can't
easily catch, dragging an object parented under a non-uniformly-scaled
parent.
No physics yet — see the build order (M0–M4) in
docs/kernel-contract.md for what's next.
Design and implementation are argued over in the same place: the doc is still the thing to disagree with before code changes to match.