Import Oxide engine (Stages 0–10) under MIT license

Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
+257
View File
@@ -0,0 +1,257 @@
# Oxide — Session Handoff
Drop this file into a new session and say "continue from HANDOFF.md". It
captures where we are, how we work, and exactly what to do next. Authoritative
roadmap is `PLAN.md`; project rules are `CLAUDE.md`; this is the "current
state + how to continue" snapshot.
_Last updated: 2026-06-17. **`main` and `dev` are aligned.** **Stage 9 — Physics
is ✅ COMPLETE on `main`.** **Stage 10 — Scripting, Live Reload & Editor Terminal:
core ✅ on `main`** (all eye-checked & approved 2026-06-17 in the `untitled`
project): the `oxide-script` crate (`Script` + `.rhai` loader + sandboxed
`ScriptEngine`), the `init`/`update(dt)` lifecycle via `ScriptHost`, the
`Vec3`/transform engine API, live reload (`examples/script_spin`), editor
integration (`Script` addable; play-loop shares the editor
`AssetServer`/`AssetDatabase` so a live `.rhai` edit updates a *playing* scene),
the **Console** (captures the `log` stream — script `print`/errors), a **command
terminal** (`$``sh -c`), and an **interactive PTY terminal** (`portable-pty` +
`vt100`; tabbed, auto-closes a tab when its program exits, Tab/arrows/Esc routed
to the program — runs shells / TUIs / `claude`), and the **richer script API**
(scripts now `spawn_entity`/`despawn` and `add_component`/`set_component`/
`remove_component` on any entity via RON through the reflection registry —
closing the last PLAN round-trip criterion; pure-logic + headless tests → on
`main`). **What's left in Stage 10:** the maintainer's editor-UX batch —
Unity-style file explorer, native New/Open-Project dialog (typing the path by
hand is too hard), a **New Script** button on the `Script` inspector,
open-script-in-editor. See §5 to start. Parked, not blockers: editor-authored
joint **components** need a serializable entity-reference type._
---
## 1. Where we are
- **Phase 1 / Stages 58.7: ✅ on `main`.** Engine core, editor framework,
input + gizmos, comprehensive UI, Reflection v2 + asset database (Stage 8.5),
and editor Play Mode (Stage 8.7).
- **Phase 1 / Stage 9 — Physics: ✅ complete on `main`.** All pieces 18c done
and promoted (eye-checked & approved 2026-06-17).
- **Phase 1 / Stage 10 — Scripting: 🚧 core done on `main`** (crate, lifecycle,
live reload, editor integration, Console, command terminal, interactive PTY
terminal — all eye-checked & approved; **richer script API**
spawn/despawn/component-edit — pure-logic, on `main`). One follow-up remains
(the editor-UX batch — GUI, needs eye-check). Piece table + what's next in §5;
details in `docs/scripting.md`.
### Stage 9 — piece status (all on `main`)
| # | Piece | Where |
|---|-------|-------|
| 1 | Component data model + module wiring: `RigidBody`/`RigidBodyKind`, `Collider`/`ColliderShape`, `PhysicsModule`, `PhysicsSettings` | `physics/` (the `oxide-physics` crate) |
| 2 | Rapier-backed sim: build the world from components, step on `FixedUpdate`, write transforms back; by-entity forces/velocities/sleep | `physics/src/world.rs` |
| 3 | Collision groups/masks via `LayerMask`, sensors, collision/trigger events (enter/stay/exit) | `physics/src/world.rs` |
| 4 | Scene queries: raycast, sphere-cast, point/overlap with `LayerMask` filtering | `physics/src/world.rs` |
| 5 | Joints/constraints: fixed, spherical, revolute, prismatic (programmatic API) | `physics/src/world.rs` |
| 6 | Kinematic capsule character controller: move-and-slide, step offset, slope limit, grounded | `physics/src/{character,world}.rs` |
| 7 | `examples/physics_stack` + `examples/character_capsule` (headless console demos) | `examples/` |
| 8a | Editor integration: register RigidBody/Collider/CharacterController (addable, reflected) + enums; wire `PhysicsModule` into the play `App` (physics = first real consumer of Play; Stop reverts via snapshot) | `editor/src/state.rs`, `editor/src/main.rs` |
| 8b | **Collider wireframe gizmos**: box/sphere/capsule/cylinder outlines over the viewport; green=solid, amber=sensor, selected drawn thicker; matches the sim (ignores `Transform::scale`); **View ▸ Show Colliders** toggle (on by default) | `editor/src/shell.rs` |
| 8c | **Raycast debug probe**: `PhysicsWorld::sync_to_scene` (query the edited scene with no Play) + **View ▸ Raycast Probe** — click freezes a camera→cursor ray into the world (orbit to see it as a 3D line), cyan ray + magenta hit dot + normal whisker, status hint on cast. Overlay `view_proj` decoupled from selection (colliders/probe show with nothing selected) | `physics/src/world.rs`, `editor/src/{shell,main}.rs` |
| — | Bug fix: namespace every inspector field widget per component (`ui.push_id(component_name)`) so a field name shared by two components (e.g. both `MeshRenderer` and `Collider` have `shape`) can't collide egui ids and block edits | `editor/src/shell.rs` |
The `oxide-physics` crate: the **ECS is the source of truth**; the rapier world
is a transient resource rebuilt from components each step, so play-mode
snapshot/restore works for free. Full usage + internals in `docs/physics.md`.
### Key design decisions (also in memory + PLAN.md)
- **Layers = Unity "Model A"** ([[layer-group-model-decision]]): an entity is on
**one** `Layer` (single index, render/physics filter slot); multi-category
needs are served by **Groups** = the multi-valued `Tags` component + a
project-level `GroupRegistry`. **Do NOT reintroduce multi-valued `Layer`
membership.** Physics `Collider.membership`/`filter` are `LayerMask`s.
- **Component multiplicity = hybrid** ([[component-multiplicity-decision]]):
one component of a type per entity (`hecs` archetypal). For "multiple things on
one node": a **local-offset field** for naturally-single positioned things, and
**child entities** for genuine multiples. NOT internal multi-instance lists.
- **`AssetRef<T>` not `Handle<T>` in components**: a component stores a
serializable `AssetRef<T>` (= `Option<AssetUid>`); `AssetRef::resolve(db,
server)` yields the process-local `Handle<T>` at runtime. The inspector
recognises both spellings via `asset_ref_target`.
---
## 2. How we work (follow this exactly)
- **Build each stage as small, independently-tested pieces.** One piece = one
green commit. Split a piece into a pure-logic part (→ `main`) and a GUI part
(→ `dev`) when cleaner.
- **Local gate before every commit:**
`cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test`.
(`#![deny(warnings)]` is on in the editor — clippy lints are hard errors, e.g.
`too_many_arguments` fires at >7 params; bundle args into tuples/structs.)
- **Branch promotion rule (from CLAUDE.md):**
- Pure-logic, fully proven by automated tests → commit to `dev` **and**
fast-forward `main`. Continue without asking. (Headless GPU pixel tests count
as proof — run them yourself.)
- Interactive GUI / rendering-feel / shell polish (anything a human must
**look at**) → push to `dev` only, **ask the maintainer to run + eye-check**,
promote after explicit approval.
- Decision rule: "Can an automated test prove this without a human looking?"
Yes → `main`-eligible. No → stop at `dev`.
- **Promotion commands** (from `dev`, after committing + pushing dev):
`git checkout main -q && git merge --ff-only dev -q && git push origin main && git checkout dev -q`
- **Commit messages** end with: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
- **Document as you build:** keep `PLAN.md`/`README.md`/`docs/` current in the
**same** change. Physics docs live in `docs/physics.md`.
### egui gotcha
egui's bundled font has a **limited glyph set**. Confirmed-rendering: `🗑 ⚙ ✏
▾ … ↺`. Confirmed-tofu (avoid): `✕` (U+2715), `⧉`, `⣿` (braille). Reuse an icon
already in the codebase or test it before shipping; prefer plain text if unsure.
Also: egui assigns widget ids from a hash of the call path + a salt — when the
same logical widget appears twice (two components with a same-named field), scope
it under `ui.push_id(unique_key)` or it errors and edits silently break.
---
## 3. Conventions (match the existing code)
- Heavy rustdoc: each public type/fn gets a one-line summary + the "why".
Module-level `//!` docs explain the piece's role.
- Unit tests in `#[cfg(test)] mod tests` at the bottom of each file; integration
tests in `tests/src/lib.rs`. Even for visual pieces, factor the math into pure
helpers and unit-test those (e.g. `collider_wire_segments`/`push_arc` in 8b),
so only the painting itself needs the eye-check.
- Serialization is `serde` + RON throughout. New optional fields use
`#[serde(default, skip_serializing_if = ...)]` so older documents still parse.
- Errors are `thiserror` enums with specific variants.
- Reflection: **public fields only**; `#[reflect(skip)]` drops a public field;
`#[reflect(min=, max=)]` on an f32 → slider. Register editor-visible types in
`oxide_editor::state::register_builtin_types`; addable ones via
`register_addable::<T>` (needs `Default`); enums via `register_enum::<E>`.
- New deferred ideas → `PLAN.md` backlog, not the current piece.
---
## 4. Key architecture facts
```rust
// Per-field reflection on any component (zero per-type editor code):
#[derive(Reflect, Serialize, Deserialize)]
struct Timer { pub repeating: bool, pub duration: f32, #[reflect(skip)] pub elapsed: f32 }
registry.register_reflected::<Timer>("Timer"); // editor + scripts
// Inspector renders generically: components_on -> field_infos -> get_field/set_field,
// edits routed through SetFieldCmd (undoable, drag-coalesced).
```
- **`oxide_engine::reflect`**: whole-value (`get_ron`/`set_ron`) + per-field
(`Reflect` + `register_reflected`) layers. `register_addable` adds an "Add
Component" constructor; `register_enum` lists variants for combo widgets.
- **`oxide_engine::prefab`**: `Prefab { name, components }`,
`ComponentSpec { type_name, ron }`; editor seeds built-ins (Empty/Cube/…).
- **`oxide_engine::layer`**: single-valued `Layer { index }`; `LayerMask`
(filter); `LayerRegistry` (names); `Tags` + `GroupRegistry` (groups).
- **Editor inspector** (`oxide_editor::shell::ShellTabViewer`): node-baked
section (Layer/Groups/Transform) then modular components (drag-reorder,
enable/disable, remove, Add Component menu). `field_widget` dispatches on
`FieldInfo.type_name`.
- **Viewport overlay** (`editor/src/shell.rs`): the host feeds a
`ViewportOverlay { view_proj, gizmo_size }` each frame; the Viewport tab paints
2D over the 3D scene via `project(world, &view_proj, tab_rect) -> Pos2`.
Transform gizmo handles, the play-state border, **and the new collider
wireframes** all paint this way. Reuse `project` + `painter.line_segment` for
any new world-space overlay (e.g. piece 8c's raycast viz).
- **Play loop** (`oxide_editor::main::drive_play` + `oxide_engine::scene::
SceneSnapshot`): owns a play `App` (DefaultModules + PhysicsModule +
ScriptModule; **`app.assets` is set to a clone of the editor's `AssetServer`**
and the project `AssetDatabase` is inserted as a resource so scripts resolve +
live-reload reaches a playing scene), built on Play / dropped on Stop, **swaps
`state.scene` in/out per tick** so the editor scene stays the single source of
truth. Snapshot covers reflected components + `Tags`/`DisabledComponents`. Undo
cleared on Play/Stop.
- **Host input** (`oxide_editor::main`): viewport orbit/pan/zoom + WASD gated on
`cursor_over_viewport && !pointer_over_floating`.
---
## 5. Exactly what to do next — finish Stage 10 (Scripting)
**Stage 10's core is done and on `main`** (all eye-checked & approved). The
`oxide-script` crate is the worked-out twin of `oxide-physics` (same
"ECS-as-truth, transient runtime resource, snapshot-for-free" shape). Full usage
+ internals: `docs/scripting.md`. What shipped (all on `main`):
| Piece | Where |
|-------|-------|
| `oxide-script` crate: `Script`, `ScriptAsset` + `.rhai` loader (`AssetKind::Script`), sandboxed `ScriptEngine` (compile/run, op cap, `ScriptError`), `ScriptModule` | `script/` |
| Lifecycle: `ScriptHost` + `run_scripts` (Update schedule); `init`/`update(dt)`; **engine API** (`bridge.rs`) — scripts read/write their `Transform` via a staged shared context (`position`/`translate`/`rotate_*`/`Vec3`), `rhai` `f32_float` | `script/src/{host,engine,bridge}.rs` |
| **Live reload**: `ScriptHost` holds each script's `Handle<ScriptAsset>` alive → watcher's in-place reload recompiles (no restart); `examples/script_spin` | `script/src/host.rs`, `examples/` |
| **Editor integration**: `Script` addable in `register_builtin_types`; `drive_play` adds `ScriptModule` + **shares the editor `AssetServer`** + a db snapshot → live `.rhai` edits update a *playing* scene | `editor/src/{state,main}.rs` |
| **Console**: capturing logger mirrors the `log` stream into a ring buffer the panel renders (script `print` + paused-script errors via `target:"oxide_script"`) | `editor/src/console.rs`, `shell.rs::console` |
| **Command terminal**: `$` prompt → `sh -c <cmd>` in the project root, streams stdout/stderr into the Console | `editor/src/terminal.rs` |
| **Interactive PTY terminal**: `PanelKind::Terminal` — tabbed, auto-closes a tab when its program exits, Tab/arrows/Esc routed to the program; runs shells / TUIs / `claude` | `editor/src/pty.rs`, `shell.rs::terminal_panel` |
### Remaining for Stage 10
**1. Richer script API — ✅ DONE, on `main`.** Scripts now `spawn_entity()` /
`spawn_entity(name)`, `despawn(e)`, and `add_component`/`set_component`/
`remove_component` on any entity, plus `entity()` for their own. Implemented as a
**deferred command buffer** in `bridge.rs` (`EntityHandle` + `ScriptCommand`):
the `rhai` functions can't borrow the ECS (must be `Send + Sync`), so each call
buffers a command the host drains in `ScriptHost::apply_commands` and applies via
the reflection registry (`app.types`, `set_ron`/`add_default`/`remove`). `spawn_entity`
returns a **provisional** handle resolvable in the same frame (`spawn` is a rhai
reserved word, hence the longer name). Component edits go through RON, closing the
last PLAN round-trip criterion. Headless tests in `host.rs`/`bridge.rs`; docs in
`docs/scripting.md` ("Spawning entities and editing components").
**2. Maintainer's editor-UX batch (GUI → `dev` + eye-check).** Details in
PLAN.md "Editor-UX follow-ups" + [[editor-file-explorer-preference]]. The
maintainer asked for these after the terminal work:
- **"New Script" button** on the `Script` inspector — write a `.rhai` template
into `assets/scripts/`, register it in the db, auto-assign it to the component.
There is **no in-editor script creation today** (scripts are authored as
files). Good small first one to pull forward.
- **Open a script in an editor** — double-click / button to open a `.rhai` in an
in-editor text view or launch `$EDITOR` / a configured external editor; edits
flow back through live reload (you can also just run `$EDITOR file` in the new
PTY terminal).
- **Unity-style file explorer** in the Project panel (create/rename/move folders,
drag-in OS import, context menus) over the existing `AssetDatabase`.
- **Native New/Open-Project dialog** (`rfd`, must work on Wayland + X11) —
typing the project path by hand "is very hard to use".
**Reuse, don't reinvent:** `oxide-physics`/`oxide-script` are the template for
"new crate → `Module` + schedule → reflected/addable components → editor
registration → play-loop integration". The play loop
(`oxide_editor::main::drive_play`) runs scripts alongside physics and shares the
editor's asset server, so live reload reaches a playing scene.
**Terminal design note:** the embedded terminal is a general PTY widget
(`portable-pty` + `vt100`); run any program (incl. `claude`) from a *Shell* tab.
A "one-app launcher" was considered and rejected — it needs the same
PTY+VT+render+input stack, so the general widget is strictly better.
**Don't do these unless asked (parked in PLAN.md):** editor authoring of joint
**components** (needs a serializable entity-reference type); standalone game
"Launch" (deferred to Stage 16).
---
## 6. Quick orientation commands
```sh
cargo test # full suite (all green at handoff)
cargo test -p oxide-script # the scripting crate (lifecycle, live reload)
cargo test -p oxide-editor --lib pty # PTY terminal pure helpers
cargo run -p oxide-examples --bin script_spin # headless live-reload demo
cargo run -p oxide-editor # the editor (Console + Terminal panels)
git log --oneline -15 # recent per-piece commits
sed -n '/## Stage 10/,/## Stage 11/p' PLAN.md # Stage 10 spec + status
```
Memory files (auto-loaded each session) track decisions: `MEMORY.md` index →
`layer-group-model-decision.md` → `component-multiplicity-decision.md` →
`run-on-target-pc.md` (headless-vs-eye-check rule) →
`workflow-auto-continue.md` (don't pause between pure-logic pieces).