native/physics-native/lingua_physics.c wraps Box3D at a specific pinned commit (47d7f7c — Box3D has no 1.0 release yet, and its only tag, v0.1.0, already diverges from this API, confirmed by diffing headers rather than assuming). The wrapper is deliberately narrow: Box3D's own b3WorldDef/ b3BodyDef/b3ShapeDef are large structs with function pointers, and b3BoxHull's own doc comment says it "has data hanging off the end and cannot be directly copied" — none of that crosses the P/Invoke boundary. Every exported Lingua_* function takes and returns only plain int32/float/ bool scalars, and handles are this shim's own array-index handles, not Box3D's id structs. C# binds them with classic DllImport rather than the newer LibraryImport specifically because LibraryImport's generated marshalling needs AllowUnsafeBlocks even for an all-scalar signature like every one of these — DllImport needs none, keeping this plugin inside the kernel's "no unsafe in the v1 hot path" rule with no exception required. engine.physics adds Rigidbody/BoxCollider/SphereCollider components and PhysicsWorld, which diffs Query<Rigidbody>() against its own tracked set every Stage.FixedUpdate (there's no destruction event to hook) to create and destroy native bodies, steps Box3D once per invocation — Engine.Host's accumulator decides how many times that runs per frame, not this plugin — and writes each body's resulting transform back to GameObject.Transform. IPhysicsService exposes ApplyLinearImpulse/Get/SetLinearVelocity for gameplay code. Verified twice: a standalone C smoke test against the native shim alone (a box dropped from y=5 onto a static ground settles at y≈1.0, exactly where the two half-heights sum to), and the full pipeline through Engine. Host — a headless run with a real scene, --dump showing the same box settling at y=0.9999 after physics, scene load, and Stage.FixedUpdate all went through the real kernel. 5 new automated tests in Engine.Physics. Tests cover the same settling behavior for both shapes, a missing-collider warning that fires once and doesn't throw, cleanup after a GameObject is destroyed mid-simulation, and that an applied impulse actually changes velocity — all passed on the first run. Physics-enabled GameObjects must be root-level for now: PhysicsWorld writes Box3D's world-space transform straight into LocalPosition/ LocalRotation, correct only when local and world space are the same thing. A parented rigidbody needs the same parent-WorldMatrix-inverse handling GizmoMath.WorldToLocalPosition already does for the gizmo — real, not-yet-done work, not silently wrong. Full suite: 81 tests. 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.