You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Slice 1: port terrain brushes to Rust + command-system infra
Port the four terrain brush commands (shape / level / smooth / metal) plus
brush-settings to Rust and flip them to Rust-only via nativeCommandsOnly.
Command system:
- Heightmap brushes wrap their per-point writes in
TerrainControl::set_height_map_func so the engine recalcs the touched area
(face normals + LOS/pathing); raw add_height_map changes data but never
recalcs. Metal needs no recalc.
- Undo/redo for Rust-only commands forward UndoCommand/RedoCommand to the native
manager (Lua wasn't driving Rust's undo stack). Those control commands are now
braced structs so they deserialize from the {className} payload.
- Native dispatch gated to the gadget (synced) state: every command reaches the
gadget first, so the single shared native manager sees it once. Prevents
cross-executed commands (e.g. SetMultipleCommandModeCommand) hitting it twice
and desyncing the streaming/undo state.
Infrastructure:
- IO worker shell (boxed dyn IoJob / dyn IoOutcome seam, no job types yet) for
off-engine-thread file/image work.
- In-engine integration test harness (SBC_TEST_SPEC-gated), driven by the native
Update callin (replaces the old Lua widget:Update poll_io driver).
- Terrain brush tests assert the recalc signal (get_ground_normal), not just
get_ground_height; terrain_drag_stroke covers streaming-mode grouping.
Docs: command-system + async-io design docs, porting conventions (stepdown,
comments), slice/review-queue status, and a todo.md for deferred work.
Known issue: set_height_map_func recalc doesn't fire on undo (stale normals) —
engine-side, tracked separately; terrain_shape_brush is red until it lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
terrain's `TerrainManager`) construct through `sbc.rs`.
73
73
74
74
**Lua files**:
75
75
-[scen_edit/command/command.lua](../../scen_edit/command/command.lua) — base class
@@ -78,12 +78,18 @@ through `sbc.rs`.
78
78
79
79
The bridge sends every command to Rust via `Spring.InvokeNativeModule(json.encode(msg:serialize()))`. Lua execution **also** runs (parallel) unless the class name appears in `nativeCommandsOnly`. Slice 0 leaves that allowlist empty — feature slices populate it as they land.
80
80
81
-
**Deferred to slice 1 (first feature slice that flips a command to Rust-only):**
81
+
**Still open — required for full 1:1 once Rust is the *only* executor of undo/redo:**
82
82
83
-
These exist on the Lua side and are required for full 1:1 parity once Rust is the only executor. Until then Lua handles them; slice 0 leaves them out by design, not by omission.
83
+
These exist on the Lua side and are needed once Rust owns the undo stack. They
84
+
are **not** yet ported. Slice 1 did not need them: flipping a command via
85
+
`nativeCommandsOnly` only suppresses Lua's `cmd:execute()`
86
+
([command_manager.lua](../../scen_edit/command/command_manager.lua) line ~129) —
87
+
the `undoListAdd` / `notify` path (line ~133) is **not** gated, so during the
88
+
parallel period Lua still owns undo/redo bookkeeping and the widget notify. These
89
+
land with the slice that first moves the undo stack itself to Rust.
84
90
85
91
-`__cmd_id` allocation on every executed command (Rust counterpart to Lua's `idCount`).
86
-
- Widget notify after `execute` / `undo` / `redo` / `clear_*` / `undo_list_add` (when it pops oldest) — Lua dispatches `WidgetCommandExecuted` / `WidgetCommandUndo` / `WidgetCommandRedo` / `WidgetCommandClearUndoStack` / `WidgetCommandClearRedoStack` / `WidgetCommandRemoveFirstUndo` to the widget. Rust needs the equivalent via `send_lua_uimsg`, matching `scen_edit/message/message_manager.lua`'s prefix-framed wire format.
92
+
- Widget notify after `execute` / `undo` / `redo` / `clear_*` / `undo_list_add` (when it pops oldest) — Lua dispatches `WidgetCommandExecuted` / `WidgetCommandUndo` / `WidgetCommandRedo` / `WidgetCommandClearUndoStack` / `WidgetCommandClearRedoStack` / `WidgetCommandRemoveFirstUndo` to the widget. Rust needs the equivalent via `send_lua_uimsg` (a `lua_bridge` module — exists in wip, not yet in stable), matching `scen_edit/message/message_manager.lua`'s prefix-framed wire format.
87
93
-`display()` method on commands (Lua returns `self.className`; `CompoundCommand` returns the first sub-command's display). Required for the `display` field of `WidgetCommandExecuted`.
88
94
89
95
**Out of scope for slice 0** (handled by their feature slices):
@@ -96,20 +102,26 @@ These exist on the Lua side and are required for full 1:1 parity once Rust is th
96
102
97
103
Brush-based heightmap and metal-map editing. Engine-side via `Spring.SetHeightMap` / `Spring.SetMetalAmount` / `Spring.AddHeightMap`, all bound natively.
98
104
99
-
**Status:** done in wip — 4 brush commands + brush-settings, flipped to Rust-only via `nativeCommandsOnly`. Needs in-game verification (the slice-0 widget-notify path is exercised here).
105
+
**Status:** in stable, awaiting review — four brush commands + brush-settings,
106
+
flipped to Rust-only. Also lands shared infra: the IO-worker shell (no job types
107
+
yet), the in-engine test harness, and the Lua `poll_io` driver.
108
+
109
+
The three heightmap brushes wrap their writes in `TerrainControl::set_height_map_func`
110
+
so the engine recalcs (without it, heights change in data but the terrain doesn't
111
+
move) — see `SBC_PORT_MISSING_BINDINGS.md`. Metal needs no recalc.
-[abstract_terrain_modify_command.lua](../../scen_edit/command/abstract_terrain_modify_command.lua) — shared base; its brush-stamp logic became [brush_modify.rs](../../native/src/sbc/commands/heightmap/brush_modify.rs) (composition via closures, not inheritance) + [brush_filter_generator.rs](../../native/src/sbc/commands/heightmap/brush_filter_generator.rs)
-[set_heightmap_brush_command.lua](../../scen_edit/command/set_heightmap_brush_command.lua) → [set_heightmap_brush_command.rs](../../native/src/sbc/commands/set_heightmap_brush_command.rs) (registers the greyscale brush shape in [terrain_manager.rs](../../native/src/sbc/commands/heightmap/terrain_manager.rs); **not** flipped to Rust-only — Lua still needs the shape for its own preview)
113
125
114
126
---
115
127
@@ -126,8 +138,10 @@ crate), replacing what the spring-launcher used to do over IPC.
- Engine thread (on drain): apply results (e.g. `set_height_map` for import).
354
-
355
-
Drain point: there's no native per-frame callin (see the engine note, Category 0),
356
-
so a throttled `widget:Update` pokes the plugin via
357
-
`InvokeNativeModule(json.encode({tag="poll_io"}))` and the plugin drains completed
358
-
jobs on that call. `widget:Update` (not `gadget:GameFrame`) because GameFrame
359
-
doesn't fire while paused and SBC is effectively always paused.
360
-
361
-
## Where things go in Rust
340
+
The async-IO model (heightmap import/export, compile, image export) is a design
341
+
concern, not a slice — see [docs/design/async-io.md](../design/async-io.md). The
342
+
worker shell landed with slice 1; the first job types land with slice 2.
362
343
363
-
- Per-command Rust file: [native/src/sbc/commands/](../../native/src/sbc/commands/)`<slice>/`. Each file owns its serde struct, the `inventory::submit!` registration, and the execute/unexecute logic.
364
-
- Per-manager Rust file: a per-slice `model`/manager module.
365
-
- The command-system internals + the single public `commands_api` surface live under [native/src/sbc/commands/command_system/](../../native/src/sbc/commands/command_system/).
366
-
- Cross-slice managers (texture undo stack, heightmap) live where the first slice that needs them puts them; later slices reuse.
344
+
Where ported code lives in the tree is a porting convention — see
Copy file name to clipboardExpand all lines: docs/porting/conventions.md
+39-1Lines changed: 39 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -102,6 +102,14 @@ The registry is built at startup from `inventory::iter`. Adding a command = one
102
102
103
103
If `inventory` becomes problematic (e.g. platforms where life-before-`main` link sections misbehave), the fallback is a `build.rs` that scans `commands/*.rs` and generates the registry. Same property: command files stay self-contained.
104
104
105
+
**Where ported code lives in the tree:**
106
+
107
+
- Per-command file under [native/src/sbc/commands/](../../native/src/sbc/commands/)`<slice>/` — owns its serde struct, its `inventory::submit!` registration, and its execute/unexecute logic.
108
+
- Per-manager file: a per-slice `model`/manager module.
109
+
- The command-system internals + the single public `commands_api` surface live under [native/src/sbc/commands/command_system/](../../native/src/sbc/commands/command_system/).
110
+
- Cross-slice managers (texture undo stack, heightmap) live where the first slice that needs them puts them; later slices reuse.
111
+
- Each slice is a directory; `mod.rs` files only wire submodules, never hold code.
112
+
105
113
| Layer | Self-contained unit | Registration |
106
114
|-------|--------------------|--------------|
107
115
| Commands | one file per command |`inventory` keyed by class name |
@@ -142,10 +150,40 @@ If any check fails, fix it. **Don't pad the queue.** Three clean items beat ten
0 commit comments