Skip to content

Commit c8bd0b3

Browse files
gajopclaude
andcommitted
Map settings slice + dev tooling, command logging, and fixes
Map settings (slice 3): sun direction, sun lighting, atmosphere, water params, map-rendering, and global-LOS ported as Rust-only commands under native/src/sbc/map_settings/, with integration tests. Water params now handle RGBA colours (the editor's colour pickers send 4 components, not 3) and water textures (texture/foamTexture/normalTexture), applied and undone via the new SetWaterTexture/GetWaterTexture engine bindings; colours and textures snapshot for undo. Dev tooling: - .env (gitignored) holds engine paths; .env.example is the template. launch.sh, run_sbc.py, and the justfile read SBC_ENGINE_DIR from it, so no personal paths are committed. - run_sbc.py's prepare() is the single source of truth for launching SBC; launch.sh is a thin wrapper over its --manual mode. - justfile: one `just lint` (fmt + clippy + lua + check); test-smoke folded into test-integration. - Persistent fontconfig cache (~/.cache/sbc-fontcache) so launches stop rebuilding it every run (~20s saved after the first). - springignore.txt: keep native/tools/docs out of the VFS scan and archive hash. Logging: every command Lua sends to Rust is appended to $SBC_COMMAND_LOG (<write_dir>/commands.jsonl), logged at the route() entry point so a command that fails to deserialize can be inspected as exact bytes. Fixes: - json.lua: coerce numeric table keys to strings in encodeString; the UTF-8 rewrite regressed brush commands ("attempt to get length of a number"). - command_manager.lua: _SafeCall now reports the failing command and the real error instead of generic spam that recursed into a C stack overflow. Docs: commands.md (setup + just commands), the stepdown-ordering rule in conventions.md, and queue/slice updates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d0eb026 commit c8bd0b3

30 files changed

Lines changed: 1381 additions & 91 deletions

‎.env.example‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Local dev config. Copy to .env and edit. .env is gitignored — never committed.
2+
3+
# Absolute path to your built spring engine install dir (the one containing the
4+
# `spring` binary). Used by tools/dev/launch.sh and tools/smoke/run_sbc.py.
5+
SBC_ENGINE_DIR=/path/to/spring-bar/build-linux/install
6+
7+
# Engine build + rust dirs, used only by the engine-build just recipes
8+
# (build-engine, build-engine-bindings). Optional unless you build the engine.
9+
SBC_ENGINE_BUILD_DIR=/path/to/spring-bar/build-linux
10+
SBC_ENGINE_RUST_DIR=/path/to/spring-bar/rust

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ env/
1010
# don't track local (dev) file
1111
*.local
1212

13+
# local dev config (engine path etc.); .env.example is the tracked template
14+
.env
15+
1316
# Rust build artifacts
1417
native/target/

‎docs/commands.md‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Commands
2+
3+
Local developer commands live in the repository [justfile](../justfile).
4+
The manual Spring run uses [tools/dev/launch.sh](../tools/dev/launch.sh),
5+
[tools/dev/script.txt](../tools/dev/script.txt), and
6+
[tools/dev/springsettings.cfg](../tools/dev/springsettings.cfg).
7+
8+
## Setup
9+
10+
One-time, before anything that launches the engine (`just run`, `just test-*`):
11+
copy [.env.example](../.env.example) to `.env` and point `SBC_ENGINE_DIR` at your
12+
built engine install dir (the one containing the `spring` binary). `.env` is
13+
gitignored — your paths never get committed.
14+
15+
```sh
16+
cp .env.example .env
17+
$EDITOR .env # set SBC_ENGINE_DIR (+ engine build/rust dirs if you build the engine)
18+
```
19+
20+
`launch.sh`, `run_sbc.py`, and the engine-build just recipes all read these from
21+
`.env` (or the real environment) — no hardcoded paths.
22+
23+
## Common
24+
25+
```sh
26+
just run # build native plugin and start Spring
27+
just build # build native plugin
28+
just lint # all lints
29+
just test-unit # native Rust unit tests
30+
just test-integration # full integration suite (boots the engine)
31+
just test-all # unit tests + integration suite
32+
```
33+
34+
## Run Spring
35+
36+
```sh
37+
just run
38+
```
39+
40+
This builds the native plugin, creates an isolated temporary write directory,
41+
links this checkout as `games/SpringBoard Core.sdd`, copies the saved Spring
42+
settings/start script, sets `SPRING_NATIVE_MODULE`, and starts the local engine.
43+
It prints the write directory and infolog path before Spring starts. The saved
44+
settings include the same packet/bandwidth limits as `dist_cfg/springsettings.json`.
45+
46+
## Build
47+
48+
```sh
49+
just build-native
50+
just build
51+
just build-engine
52+
just build-engine-bindings
53+
```
54+
55+
## Lint
56+
57+
```sh
58+
just lint
59+
just fmt
60+
just clippy
61+
just lint-lua
62+
```
63+
64+
## Test
65+
66+
```sh
67+
just test-unit
68+
just test-integration
69+
just test-integration textures
70+
just test-integration textures,gfx
71+
just test-all
72+
```
73+
74+
`SBC_TEST_TAGS` is passed through by `test-integration` when a tag argument is
75+
given (it narrows the in-engine registered tests; the other suite files always run).

‎docs/porting/01-slices.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ spread across the slices they belong to, not deferred as a catch-all.
3131
|--:|-------|--------|
3232
| 0 | [Command infrastructure](#0-command-infrastructure) — trait + dispatch + undo/redo + compound + bridge + widget-notify | done (stable) |
3333
| 1 | [Terrain](#1-terrain) — shape / level / smooth / metal brushes | done (stable; flipped to Rust-only; heightmap recalc fixed via `set_height_map_func`) |
34-
| 2 | [Heightmap](#2-heightmap) — load (sync) + import / export (async IO) | review (in stable; first `IoJob` types + command IO seam; dispatch parallel) |
35-
| 3 | [Map settings](#3-map-settings) — sun / atmosphere / water / map-rendering | wip (not in stable) |
34+
| 2 | [Heightmap](#2-heightmap) — load + save + import / export (async IO) | done (stable; native IO seam, 16-bit PNG, raw-f32 save/load by path, import undoable) |
35+
| 3 | [Map settings](#3-map-settings) — sun / atmosphere / water / map-rendering | review (in stable; all setters Rust-only with `Gfx`-snapshot undo) |
3636
| 4 | [Textures](#4-textures) — diffuse / shading / terrain texture / cache + grass + DNTS | review (in stable; Rust owns paint + cache + stroke close + undo/redo) |
3737
| 5 | [Objects](#5-objects) — units & features add / remove / set / move (needs s11n) | wip (not in stable) |
3838
| 6 | [Areas](#6-areas) | wip (not in stable) |
@@ -139,11 +139,14 @@ General map rendering config: sun lighting, atmosphere, water, map-rendering
139139
params. These are **map-wide config**, distinct from scenario *info* (project
140140
metadata, which is in slice 8). Should land early.
141141

142-
**Status:** implemented in wip, not yet in stable — all setters with partial-opts,
143-
undo snapshots via the `Gfx` getters. These are `_execute_unsynced`: they reach
144-
the single native module from the widget side (where `Gfx` is valid), so no bridge
145-
change is needed. `merge_command.lua` (coalesces successive edits into one undo
146-
step) lands here.
142+
**Status:** review (in stable) — all six setters Rust-only, partial-opts, with
143+
`Gfx`-getter-snapshot undo (map-rendering & global-LOS have no undo, matching
144+
their Lua). Dispatched to the gadget like every other command (`commandManager:
145+
execute(cmd)`); native does the `Gfx`/`unsynced_ctrl` work from the gadget-invoked
146+
main thread, where `Gfx` is valid — same as the textures slice — so the Lua
147+
`_execute_unsynced` flag is bypassed for these (native-only) and no bridge change
148+
is needed. Water re-selects the current water mode after applying so the renderer
149+
reloads. Lua command files unchanged — only the `nativeCommandsOnly` flip.
147150

148151
**Commands:**
149152
- [set_sun_lighting_command.lua](../../scen_edit/command/set_sun_lighting_command.lua)

‎docs/porting/04-cleanup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ Everything else — `scen_edit/`, the libraries under `libs_sb/` that we no long
2828

2929
## Exit criteria
3030

31-
`find . -name "*.lua" | wc -l` is small (entry stubs + Spring-required files only). `cargo check && cargo clippy` clean. App still works end-to-end.
31+
`find . -name "*.lua" | wc -l` is small (entry stubs + Spring-required files only). `just lint` clean. App still works end-to-end.

‎docs/porting/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Phase 1 ports the model and command layers together in slices (one feature end-t
2626

2727
Phase 2 is in progress independently of the Rust port (some Chili→RmlUi work landed earlier).
2828

29-
Pending review items: [review-queue.md](review-queue.md). Rules: [conventions.md](conventions.md). Deferred improvements: [todo.md](todo.md).
29+
Pending review items: [review-queue.md](review-queue.md). Rules: [conventions.md](conventions.md). Commands: [commands.md](../commands.md). Deferred improvements: [todo.md](todo.md).
3030

3131
## Design docs
3232

‎docs/porting/conventions.md‎

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ description: How the Lua → Rust port is carried out — process, structure, qu
99

1010
The work happens in **two side-by-side directories**, both checkouts of this repo:
1111

12-
- **wip** — `/home/gajop/projects/spring-projects/SBC.sdd` (this dir, branch `rust`). Claude works here. Permanent dirty tree, fat and growing. **Claude never commits here, ever.** Loses no work because nothing gets trimmed away.
13-
- **stable** — `/home/gajop/worktrees/SBC.sdd/SBC-rust-stable.sdd` (git worktree, branch `SBC-rust-stable.sdd`). User reviews and tests here. Only the slimmed-down, review-ready slice exists here. All commits happen here. User pushes from here.
12+
- **wip** — `/home/gajop/projects/spring-projects/SBC.sdd` (this dir, branch `rust-wip`). Claude works here. Permanent dirty tree, fat and growing. **Claude never commits here, ever.** Loses no work because nothing gets trimmed away.
13+
- **stable** — `/home/gajop/worktrees/SBC.sdd/SBC-rust-stable.sdd` (git worktree, branch `rust-stable`). User reviews and tests here. Only the slimmed-down, review-ready slice exists here. All commits happen here. User pushes from here.
1414

1515
Both share the same `.git`. Sync runs two directions:
1616

1717
- **wip → stable**: `cp`, one file at a time. The normal forward flow — a finished slice is copied into stable and trimmed there.
18-
- **stable → wip**: `git rebase`. Review-time edits (rename, reorder, fix) are committed in stable, and wip rebases `rust` onto `SBC-rust-stable.sdd` to absorb them. Never `cp` backwards — the rebase is what carries reviewed edits home, so they survive the next forward `cp`.
18+
- **stable → wip**: `git rebase`. Review-time edits (rename, reorder, fix) are committed in stable, and wip rebases `rust-wip` onto `rust-stable` to absorb them. Never `cp` backwards — the rebase is what carries reviewed edits home, so they survive the next forward `cp`.
1919

2020
### The loop
2121

2222
For each review item:
2323

24-
1. **Build & test in wip** — full fat tree. `cargo check`, `cargo test`, and the integration tests (`uv run pytest` in `tools/smoke/`).
24+
1. **Build & test in wip** — full fat tree. `just check`, `just test-unit`, and `just test-integration`.
2525
2. **Copy → trim in stable** — `cp` the files in (one at a time, no wildcards), trim to the minimal version for this slice. Never trim in wip.
2626
3. **Test in stable** — same suite, run inside stable.
2727
4. **Review** — add a row to [review-queue.md](review-queue.md). User reviews → tests → authorizes; the user may edit files in stable during review. Claude commits in stable, user pushes.
@@ -39,7 +39,7 @@ For each review item:
3939
wip has no commits of its own, just a dirty tree, so park it across the rebase:
4040

4141
1. `git stash --include-untracked`
42-
2. `git rebase SBC-rust-stable.sdd`
42+
2. `git rebase rust-stable`
4343
3. `git stash pop`, then resolve conflicts toward stable's reviewed version.
4444

4545
Between slices only, never mid-slice. If the tree won't stash cleanly, finish or discard the local change first.
@@ -55,7 +55,7 @@ Between slices only, never mid-slice. If the tree won't stash cleanly, finish or
5555
1. **Review** — user reads the Rust code.
5656
2. **Manual test** — user runs SBC, exercises the feature.
5757

58-
Claude does lint (`cargo fmt`, `cargo clippy -D warnings`), unit tests, and the integration tests. Claude never marks anything done.
58+
Claude does `just lint`, `just test-unit`, and `just test-integration`. Claude never marks anything done.
5959

6060
**Commits.** All commits happen in **stable**, never in wip. Claude runs `git commit -m` in stable, only after user authorizes. Small commits: one port (or a small batch of related ports) per commit. The dispatch flip can ride along or come in a follow-up.
6161

@@ -104,11 +104,14 @@ If `inventory` becomes problematic (e.g. platforms where life-before-`main` link
104104

105105
**Where ported code lives in the tree:**
106106

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.
107+
The layout is **feature-first**: each slice is a directory `native/src/sbc/<slice>/`
108+
holding its own `commands/`, `model/`, and `tests/`.
109+
110+
- Per-command file under [native/src/sbc/](../../native/src/sbc/)`<slice>/commands/` — owns its serde struct, its `register_command!` registration, and its execute/unexecute logic.
111+
- Per-manager/model file: under `<slice>/model/`, self-registered as a `ModelFactory` (`inventory`) and reached type-erased via `ctx.model::<T>()`.
112+
- The command-system internals + the single public `commands_api` surface live under [native/src/sbc/command_system/](../../native/src/sbc/command_system/) (domain-agnostic — no per-feature edits).
113+
- Cross-slice models (texture undo stack, heightmap/terrain) live in the slice that first needs them; later slices reuse.
114+
- `mod.rs` files only wire submodules, never hold code.
112115

113116
| Layer | Self-contained unit | Registration |
114117
|-------|--------------------|--------------|
@@ -155,18 +158,17 @@ new shared subsystem.
155158

156159
## Build invariant
157160

158-
`cargo check` in `native/` must pass at every commit (in **stable**). The Lua app must still run at every commit. Both halves of the parallel impl exist; only flip one at a time. If a port needs an unfinished dependency, gate behind `cfg`, don't break the build.
161+
`just check` must pass at every commit (in **stable**). The Lua app must still run at every commit. Both halves of the parallel impl exist; only flip one at a time. If a port needs an unfinished dependency, gate behind `cfg`, don't break the build.
159162

160163
The wip dir has no commit invariant — it builds when it builds. If wip is temporarily broken because a refactor is mid-flight, that's fine; the stable side is unaffected.
161164

162165
## Quality bar — pre-handoff
163166

164167
Target: user review + manual test under 2 minutes per item. To get there, every item passes these *before* entering the queue. No "I'll add tests later."
165168

166-
- `cargo fmt --check` clean
167-
- `cargo clippy --all-targets -- -D warnings` zero output (warnings fixed or `#[allow]`-ed with a one-line reason)
168-
- `cargo test` green, including new tests for new logic
169-
- Integration tests green (`uv run pytest` in `tools/smoke/`)
169+
- `just lint` clean — fmt, clippy (`-D warnings`; fix or `#[allow]` with a one-line reason), lua, and `check`
170+
- `just test-unit` green, including new tests for new logic
171+
- `just test-integration` green
170172
- Self-review: re-read the diff as if reviewing it; cut dead code, fix bad names
171173
- No `unwrap()`/`panic!()` outside tests; use `?` or `expect("specific reason")`
172174
- No leftover `TODO`s
@@ -190,9 +192,12 @@ the things it calls, then their callees, with private leaf helpers last. A reade
190192
scrolling top-to-bottom meets each function *before* the helpers it depends on —
191193
the file reads like a newspaper (headline first, detail below).
192194

193-
- In a command file: the `impl Command` block (`execute` / `unexecute` — what the
194-
framework calls) goes first, then its `stamp`/private methods, then leaf helpers
195-
(`brush`, `generate_*`), then free functions, then the `register_command!` line.
195+
- In a command file: the public command (its struct + `impl Command`, the
196+
`execute` / `unexecute` the framework calls) goes first; everything it leans on —
197+
`stamp`/private methods, support types (`Atmosphere`, `Water`), leaf helpers
198+
(`brush`, `generate_*`), free functions — goes below it. Put `register_command!`
199+
wherever reads cleanly (top with the command or bottom of the file); it carries no
200+
ordering meaning. The rule is only: the public command on top, helpers underneath.
196201
- Keep one `impl` block per type — don't split a type's methods across several
197202
`impl` blocks to satisfy ordering; order the methods *within* the block instead.
198203
- Free helper functions go below the code that calls them; a leaf used by several
@@ -237,7 +242,7 @@ To confirm a command actually crosses the bridge into the native plugin:
237242
1. In [native/log4rs.yaml](../../native/log4rs.yaml), set the `rust_plugin::sbc`
238243
logger to `level: debug` (the existing commented-out `command_runner` logger
239244
names a module that doesn't exist — `rust_plugin::sbc` is the right target).
240-
2. Rebuild: `cd native && cargo build --release`.
245+
2. Rebuild: `just build`.
241246
3. Boot SBC and exercise *any* command (e.g. one terrain brush stroke):
242247
`cd tools/smoke && uv run python -m run_sbc` prints the write dir, or launch
243248
the editor manually.

‎docs/porting/review-queue.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Each row has: link to the Rust file, the Lua file it replaces, a one-line descri
1111

1212
| # | Item | Files | State | What to test |
1313
|--:|------|-------|-------|--------------|
14-
| 2 | **Heightmap load + image import/export** (slice 2). Whole-map load + 16-bit-greyscale PNG import/export on the background IO worker, all Rust-only (`nativeCommandsOnly`). Load reads the `.data` file (LE `f32`) by **path** — the path crosses the bridge, not the bytes. Import/export replace the spring-launcher round-trip. Lands the command IO seam: `Context::submit_io` → drained into the worker after execute → `SBC::drain_io`; `wait_for_io`/`wait_for_file` test helpers. | New: [`heightmap/commands/`](../../native/src/sbc/heightmap/commands/)`{load_map,import_heightmap,export_heightmap}_command.rs`, [`heightmap/model/heightmap_io.rs`](../../native/src/sbc/heightmap/model/heightmap_io.rs). Seam: [context.rs](../../native/src/sbc/command_system/context.rs), [sbc.rs](../../native/src/sbc/sbc.rs), [tests_api.rs](../../native/src/sbc/tests/tests_api.rs), `image` dep. Lua: [load_map_command.lua](../../scen_edit/command/load_map_command.lua) (path, not bytes), [load_project_command_widget.lua](../../scen_edit/command/project/load_project_command_widget.lua) (passes the path), flip in [command_manager.lua](../../scen_edit/command/command_manager.lua). | review | `cd tools/smoke && uv run pytest` → `heightmap_load` + `heightmap_import` + `heightmap_roundtrip` pass (load applies heights from a written `.data`; import/export at 16-bit tolerance). In-editor (now Rust-only): **load a saved project** → terrain restores; import a PNG → terrain matches; export → 16-bit PNG that re-imports. Export reads **live** engine heights (Lua read a saved file). |
14+
| 3 | **Map settings** (slice 3). Sun direction + sun lighting, atmosphere (sky/fog), water params, map-rendering (splat scales/mults, void water/ground), and global-LOS — all Rust-only (`nativeCommandsOnly`). Each setter applies via `UnsyncedCtrl`/`SyncedCtrl` and snapshots the keys it touches via the matching `Gfx::Get*` for undo (map-rendering and global-LOS have no undo, matching their Lua). Water re-selects the current water mode after applying (`Display::GetWaterMode` + `Messages::SendCommands("water", N)`) so the renderer picks up new params — the native equivalent of Lua's `SendCommands('water '..GetWaterMode())`. Partial opts (editors send one key at a time). | New: [`map_settings/`](../../native/src/sbc/map_settings/)`commands/{set_sun_parameters,set_sun_lighting,set_atmosphere,set_water_params,set_map_rendering_params,set_global_los}_command.rs`, [`map_settings/tests/test_map_settings.rs`](../../native/src/sbc/map_settings/tests/test_map_settings.rs). Wiring: `mod map_settings;` in [sbc/mod.rs](../../native/src/sbc/mod.rs), flips in [command_manager.lua](../../scen_edit/command/command_manager.lua). **Lua command files unchanged** — only the dispatch flip. | review | `just test-integration map_settings` → 6 tests pass (each sets a value, reads it back via `Gfx::Get*`, undoes where applicable). In-editor: Lighting / Sky / Water / Terrain-settings editors → change sun direction, a ground color, fog color, a water param, splat scales → visible change; **Ctrl+Z** restores sun / lighting / atmosphere / water (map-rendering & global-LOS intentionally don't undo). |
1515

1616
## States
1717

0 commit comments

Comments
 (0)