Skip to content

Commit 8dcf68b

Browse files
gajopclaude
andcommitted
Add native Rust command system (Phase 0 + Slice 0)
Stand up the native plugin crate and the synced-side command system that the Lua→Rust port builds on, plus the pytest smoke harness. Native crate (native/): - command system under commands/command_system/: Command trait + CompoundCommand, CommandManager (undo/redo via CommandHistory, command streaming via StreamingCommands), inventory-based registry with parse_json_command, the four control commands (undo/redo/clear/ set-multiple-command-mode), and Context/CommandManagerIntent so commands drive the manager without aliasing it mid-execute. - commands_api.rs is the single public surface; transport (the {tag,data} envelope decode) lives at the SBC boundary in sbc.rs. - log/ routes the `log` macros to the engine console via a custom log4rs spring_echo appender, with a plain-logger fallback. Smoke tests (tools/smoke/): boot SBC against the dev engine once and assert no crashes/warnings/errors, LuaUI + native plugin loaded, init log fired. Lua bridge: command_manager.lua forwards every command to the plugin via Spring.InvokeNativeModule; nativeCommandsOnly allowlist is empty (Lua still executes everything in parallel). Plus two small Lua guards to reach zero warnings/errors in the smoke suite. Docs: restructure porting docs into feature slices, add the command-system design doc and conventions; clear the review queue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8526ae6 commit 8dcf68b

48 files changed

Lines changed: 2712 additions & 272 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ env/
99

1010
# don't track local (dev) file
1111
*.local
12+
13+
# Rust build artifacts
14+
native/target/

LuaUI/widgets/dbg_dev_console.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ local function ToggleFilterSinceLastReload()
9797
end
9898

9999
local function UpdateFilterProblems()
100+
if not btnFilterProblems then
101+
return
102+
end
100103
if totalErrors == 0 then
101104
btnFilterProblems:SetCaption("Problems(" .. color.blue .. "0\b)")
102105
else
@@ -580,6 +583,9 @@ function CheckForLuaFilePath(text)
580583
end
581584

582585
function NewConsoleLine(text)
586+
if not log then
587+
return
588+
end
583589
-- avoid creating insane numbers of children (chili can't handle it)
584590
-- if #log.children > cfg.msgCap then
585591
-- log:RemoveChild(log.children[1])
@@ -668,6 +674,9 @@ function ShowSinceReload()
668674
end
669675

670676
function widget:Update()
677+
if not btnToggleCheating then
678+
return
679+
end
671680
btnToggleCheating.checked = Spring.IsCheatingEnabled()
672681
btnToggleGlobalLOS.checked = Spring.GetGlobalLos(Spring.GetMyAllyTeamID())
673682
btnToggleGodMode.checked = Spring.IsGodModeEnabled()

docs/design/command-system.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
name: Command system
3+
description: How SpringBoard turns editor actions into commands and executes them
4+
---
5+
6+
# Command system
7+
8+
A **command** is a serializable object with an `execute` and (if reversible) an
9+
`unexecute`. The undo/redo stack replays them.
10+
11+
## The two Lua states
12+
13+
Editor logic runs in two states:
14+
15+
- **Lua Widget** -- unsynced, per-client: the UI.
16+
- **Lua Gadget** -- synced, identical across clients: authoritative state.
17+
18+
Both run a command manager. A command is always raised in the widget and always
19+
sent to the gadget. A gadget command executes there; a widget command is sent
20+
back to the widget and executes there.
21+
22+
## Executing a command
23+
24+
Wherever a command executes, that state dispatches it to the Rust command system,
25+
and may also run a Lua implementation. The Rust system runs a command if it has a
26+
Rust implementation; the Lua implementation runs unless the command is marked
27+
Rust-only. The two are independent: a command can run in Rust, in Lua, or in
28+
both.
29+
30+
```mermaid
31+
flowchart TD
32+
Raise["Lua Widget: command raised"] --> Gadget["Lua Gadget: receives command"]
33+
Gadget --> Kind{"meant for<br/>which state?"}
34+
35+
Kind -->|"gadget"| GExec["Lua Gadget: execute"]
36+
GExec --> GDispatch["dispatch to Rust"]
37+
GDispatch --> GNativeQ{"has a Rust<br/>implementation?"}
38+
GNativeQ -->|"yes"| GNative["Rust runs it"]
39+
GNativeQ -->|"no"| GIgnore["ignored"]
40+
GExec --> GLuaQ{"Rust-only?"}
41+
GLuaQ -->|"no"| GLua["Lua Gadget runs it"]
42+
GLuaQ -->|"yes"| GSkip["Lua skipped"]
43+
44+
Kind -->|"widget"| Back["Lua Widget: receives it back"]
45+
Back --> WExec["Lua Widget: execute"]
46+
WExec --> WDispatch["dispatch to Rust"]
47+
WDispatch --> WNativeQ{"has a Rust<br/>implementation?"}
48+
WNativeQ -->|"yes"| WNative["Rust runs it"]
49+
WNativeQ -->|"no"| WIgnore["ignored"]
50+
WExec --> WLuaQ{"Rust-only?"}
51+
WLuaQ -->|"no"| WLua["Lua Widget runs it"]
52+
WLuaQ -->|"yes"| WSkip["Lua skipped"]
53+
```
54+
55+
## Inside the Rust command system
56+
57+
When the Rust system runs a command:
58+
59+
```mermaid
60+
sequenceDiagram
61+
participant Manager
62+
participant Command
63+
participant Context
64+
65+
Note over Manager: resolve incoming data to a Command
66+
Manager->>Command: execute(context)
67+
Command->>Command: act on the world
68+
Command->>Context: (control commands only) record intent
69+
Command-->>Manager: return
70+
Manager->>Manager: record on undo/redo stacks (if reversible)
71+
Manager->>Context: apply recorded intents (undo / redo / etc.)
72+
```
73+
74+
### Resolving a command
75+
76+
A command arrives as data identifying its type. The manager looks up the handler
77+
for that type, which reconstructs the command object. Handlers register
78+
themselves, so adding a command is a self-contained addition rather than an edit
79+
to a central list. A few known types are intentionally ignored on the synced
80+
side and resolve to nothing.
81+
82+
### Executing and undo/redo
83+
84+
The manager runs the command, then settles the stacks:
85+
86+
- A reversible command goes onto the undo stack -- or, in multiple-command mode,
87+
into a group that collapses into one undoable unit when the mode ends.
88+
- The undo stack is capped (oldest evicted). Any new command clears the redo
89+
stack.
90+
91+
### Commands and the manager: intents
92+
93+
A few commands operate on the manager itself (undo, redo, clear, toggle
94+
multiple-command mode) rather than on the world. Such a command can't call the
95+
manager while the manager is mid-execute running it. Instead it records an
96+
**intent**, which the manager applies once the command returns. This expresses
97+
the reentrant manager-calls-command-calls-manager relationship without shared-mutability
98+
wrappers, and keeps ownership flat. Ordinary commands record no intent.

docs/porting/01-commands.md

Lines changed: 0 additions & 159 deletions
This file was deleted.

0 commit comments

Comments
 (0)