Skip to content

Commit ab54f6b

Browse files
committed
Port scenario info command to native
1 parent b9df1e5 commit ab54f6b

15 files changed

Lines changed: 311 additions & 64 deletions

File tree

docs/porting/01-slices.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ spread across the slices they belong to, not deferred as a catch-all.
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) | review (in stable) |
3838
| 6 | [Areas](#6-areas) | wip (not in stable) |
39-
| 7 | [Teams & diplomacy](#7-teams--diplomacy) | review (in stable) |
40-
| 8 | [Project lifecycle](#8-project-lifecycle) — save / load / export / sync / start / stop + scenario-info | wip — core (not in stable) |
39+
| 7 | [Teams & diplomacy](#7-teams--diplomacy) | done (stable) |
40+
| 8 | [Project lifecycle](#8-project-lifecycle) — save / load / export / sync / start / stop + scenario-info | review (scenario info in stable) |
4141
| 9 | [Triggers + Variables](#9-triggers--variables) — depends on areas, teams | wip (not in stable) |
4242

4343
Notes from review:
@@ -248,7 +248,7 @@ Map regions used by triggers and editor tools.
248248
Teams, allyteams, player-team assignment. Engine team ops bound via the `teams` /
249249
`synced_ctrl` APIs.
250250

251-
**Status:** review (in stable) — add / remove / update team, set ally state, and
251+
**Status:** done (stable) — add / remove / update team, set ally state, and
252252
change-player-team are Rust-only, backed by a native `TeamManager` snapshot for
253253
undo/redo.
254254

@@ -272,6 +272,10 @@ Touches filesystem and project state; export commands that read engine pixel dat
272272
are gated on the same atlas GL gap as slice 4. `resend_command.lua` (deferred from
273273
slice 0) lands here — it depends on s11n.
274274

275+
**Status:** review (scenario info in stable) — `SetScenarioInfoCommand` is
276+
Rust-only with native undo/redo and widget-side model sync. Broader project
277+
save/load/export remains wip.
278+
275279
**Model:**
276280
- [scen_edit/model/project.lua](../../scen_edit/model/project.lua)
277281
- [scen_edit/model/model.lua](../../scen_edit/model/model.lua) (root)

docs/porting/review-queue.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ Items Claude has finished implementing, awaiting human gates. Top of the list =
99

1010
Each item is a one-line description and the steps to verify it in-game (read the diff for the code). Claude appends; user removes items as they're reviewed → tested → committed. Oldest first.
1111

12-
## Teams & diplomacy (slice 7) — add / remove / update teams, alliances, player assignment — *review*
13-
14-
`just test-integration teams` → team integration tests pass. In-editor:
15-
16-
1. Add a team with a visible color/name and confirm it appears in the players window.
17-
2. Update that team's color/name and confirm the UI and engine state follow it.
18-
3. Toggle diplomacy between two allyteams and confirm the alliance state changes.
19-
4. Move a player to another team if a test player is available.
20-
5. **Ctrl+Z / Ctrl+Y** each change — undo restores, redo re-applies.
12+
## Project lifecycle (slice 8) — scenario info metadata — *review*
13+
14+
`just build-native && just test-integration project` → scenario-info integration
15+
test passes. In-editor:
16+
17+
1. Open **Misc → Info**.
18+
2. Edit name, description, version, and author.
19+
3. Confirm the fields update without Lua-side execution errors.
20+
4. **Ctrl+Z / Ctrl+Y** the edit group — undo restores the previous metadata,
21+
redo reapplies it.
22+
5. Save/export once if convenient and confirm the metadata shown in the UI is
23+
the edited value.
2124

2225
## Objects (slice 5) — units, features & areas add / remove / move / set-param — *review*
2326

native/src/sbc/lua_bridge.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//! `tag`:
77
//!
88
//! - `tag: "command"` → reconstruct + execute a `Widget*Command` (used by the
9-
//! command-system widget-notify, e.g. `WidgetCommandExecuted`, by object
10-
//! manager mirror updates, and by generic model listener notifications).
9+
//! command-system widget-notify, e.g. `WidgetCommandExecuted`, and by model
10+
//! mirror updates).
1111
//!
1212
//! Everything goes through one channel + one widget router, so adding a new
1313
//! notification is just another `{tag, ...}` shape — no per-manager plumbing.
@@ -24,24 +24,13 @@ pub fn send(interface: &NativeInterfaceRef, body: serde_json::Value) {
2424
let _ = interface.messages().send_lua_uimsg(&payload, "");
2525
}
2626

27-
/// Fire a model-manager listener on the widget side via the normal widget-command
28-
/// transport: `SB.model[target]:callListeners(event, unpack(args))`.
29-
pub fn notify_model(
30-
interface: &NativeInterfaceRef,
31-
target: &str,
32-
event: &str,
33-
args: Vec<serde_json::Value>,
34-
) {
27+
/// Send a widget-side command over the native message channel.
28+
pub fn widget_command(interface: &NativeInterfaceRef, data: serde_json::Value) {
3529
send(
3630
interface,
3731
serde_json::json!({
3832
"tag": "command",
39-
"data": {
40-
"className": "WidgetNotifyModelCommand",
41-
"target": target,
42-
"event": event,
43-
"args": args,
44-
},
33+
"data": data,
4534
}),
4635
);
4736
}

native/src/sbc/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod commands_api;
33
mod heightmap;
44
mod map_settings;
55
mod objects;
6+
mod project;
67
mod teams;
78
mod textures;
89

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod set_scenario_info_command;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use serde::Deserialize;
2+
3+
use crate::sbc::command_system::command::Command;
4+
use crate::sbc::command_system::context::Context;
5+
use crate::sbc::command_system::registry::register_command;
6+
use crate::sbc::project::model::scenario_info_manager::{ScenarioInfo, ScenarioInfoPatch};
7+
use crate::sbc::project::ScenarioInfoManager;
8+
9+
/// Sets scenario metadata. 1:1 with
10+
/// `scen_edit/command/set_scenario_info_command.lua`: execute snapshots the full
11+
/// old state (once) then applies the patch; unexecute restores the snapshot.
12+
#[derive(Deserialize, Debug)]
13+
pub struct SetScenarioInfoCommand {
14+
data: ScenarioInfoPatch,
15+
#[serde(skip)]
16+
old: Option<ScenarioInfo>,
17+
}
18+
19+
impl Command for SetScenarioInfoCommand {
20+
fn execute(&mut self, ctx: &mut Context) {
21+
// Snapshot once: re-running (redo) must restore to the same pre-command
22+
// state on a later undo, matching Lua's `if not self.oldData` guard.
23+
if self.old.is_none() {
24+
self.old = Some(ctx.model::<ScenarioInfoManager>().serialize());
25+
}
26+
ctx.model::<ScenarioInfoManager>().set(&self.data);
27+
}
28+
29+
fn unexecute(&mut self, ctx: &mut Context) {
30+
if let Some(old) = self.old.clone() {
31+
ctx.model::<ScenarioInfoManager>().restore(old);
32+
}
33+
}
34+
}
35+
36+
register_command!(SetScenarioInfoCommand, "SetScenarioInfoCommand");

native/src/sbc/project/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod commands;
2+
pub(crate) mod model;
3+
mod tests;
4+
5+
pub(crate) use model::scenario_info_manager::ScenarioInfoManager;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod scenario_info_manager;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::any::Any;
2+
3+
use serde::{Deserialize, Serialize};
4+
use spring_native::prelude::NativeInterfaceRef;
5+
6+
use crate::sbc::command_system::model::{Model, ModelFactory};
7+
use crate::sbc::lua_bridge;
8+
9+
inventory::submit! { ModelFactory { make: |iface| Box::new(ScenarioInfoManager::new(iface)) } }
10+
11+
/// Project-level scenario metadata (name / description / version / author).
12+
/// Mirrors `scen_edit/model/scenario_info.lua`. Pure project state; on change it
13+
/// notifies the widget's mirror so the UI updates.
14+
#[derive(Debug, Clone, Serialize, Deserialize)]
15+
pub struct ScenarioInfo {
16+
pub name: String,
17+
pub description: String,
18+
pub version: String,
19+
pub author: String,
20+
}
21+
22+
impl Default for ScenarioInfo {
23+
fn default() -> Self {
24+
ScenarioInfo {
25+
name: String::new(),
26+
description: String::new(),
27+
version: "1".to_string(),
28+
author: String::new(),
29+
}
30+
}
31+
}
32+
33+
/// Partial-update payload: any field omitted leaves the current value untouched
34+
/// (mirrors Lua's `data.x or self.x` merge in `ScenarioInfo:Set`).
35+
#[derive(Debug, Clone, Default, Deserialize)]
36+
pub struct ScenarioInfoPatch {
37+
pub name: Option<String>,
38+
pub description: Option<String>,
39+
pub version: Option<String>,
40+
pub author: Option<String>,
41+
}
42+
43+
pub struct ScenarioInfoManager {
44+
interface: NativeInterfaceRef,
45+
info: ScenarioInfo,
46+
}
47+
48+
impl Model for ScenarioInfoManager {
49+
fn as_any_mut(&mut self) -> &mut dyn Any {
50+
self
51+
}
52+
}
53+
54+
impl ScenarioInfoManager {
55+
pub fn new(interface: NativeInterfaceRef) -> Self {
56+
ScenarioInfoManager {
57+
interface,
58+
info: ScenarioInfo::default(),
59+
}
60+
}
61+
62+
/// Merge a partial patch over the current info (each absent field keeps its
63+
/// current value).
64+
pub fn set(&mut self, patch: &ScenarioInfoPatch) {
65+
if let Some(name) = &patch.name {
66+
self.info.name = name.clone();
67+
}
68+
if let Some(description) = &patch.description {
69+
self.info.description = description.clone();
70+
}
71+
if let Some(version) = &patch.version {
72+
self.info.version = version.clone();
73+
}
74+
if let Some(author) = &patch.author {
75+
self.info.author = author.clone();
76+
}
77+
self.notify();
78+
}
79+
80+
/// Replace the whole info (used by undo to restore a prior full snapshot).
81+
pub fn restore(&mut self, info: ScenarioInfo) {
82+
self.info = info;
83+
self.notify();
84+
}
85+
86+
pub fn serialize(&self) -> ScenarioInfo {
87+
self.info.clone()
88+
}
89+
90+
fn notify(&self) {
91+
lua_bridge::widget_command(
92+
&self.interface,
93+
serde_json::json!({
94+
"className": "WidgetSetScenarioInfoCommand",
95+
"data": self.info,
96+
}),
97+
);
98+
}
99+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod test_scenario_info;

0 commit comments

Comments
 (0)