Skip to content

Commit 8728eb0

Browse files
committed
Port team commands to native
1 parent d53b4f2 commit 8728eb0

27 files changed

Lines changed: 667 additions & 56 deletions

docs/porting/01-slices.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ 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) | wip (not in stable) |
39+
| 7 | [Teams & diplomacy](#7-teams--diplomacy) | review (in stable) |
4040
| 8 | [Project lifecycle](#8-project-lifecycle) — save / load / export / sync / start / stop + scenario-info | wip — core (not in stable) |
4141
| 9 | [Triggers + Variables](#9-triggers--variables) — depends on areas, teams | wip (not in stable) |
4242

@@ -248,6 +248,10 @@ 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
252+
change-player-team are Rust-only, backed by a native `TeamManager` snapshot for
253+
undo/redo.
254+
251255
**Model:**
252256
- [scen_edit/model/team_manager.lua](../../scen_edit/model/team_manager.lua)
253257

docs/porting/review-queue.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ 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.
21+
1222
## Objects (slice 5) — units, features & areas add / remove / move / set-param — *review*
1323

1424
`just test-integration objects` → 4 tests pass. In-editor:

native/src/sbc/lua_bridge.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
//! `RecieveGadgetMessage` (`op == "native"`) decodes the JSON and routes by
66
//! `tag`:
77
//!
8-
//! - `tag: "command"` → reconstruct + execute a `WidgetCommand*` (used by the
9-
//! command-system widget-notify, e.g. `WidgetCommandExecuted`, and by the
10-
//! object managers' add/remove widget mirror).
8+
//! - `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).
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.
@@ -23,3 +23,25 @@ pub fn send(interface: &NativeInterfaceRef, body: serde_json::Value) {
2323
let payload = format!("{MESSAGE_PREFIX}|native|{body}");
2424
let _ = interface.messages().send_lua_uimsg(&payload, "");
2525
}
26+
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+
) {
35+
send(
36+
interface,
37+
serde_json::json!({
38+
"tag": "command",
39+
"data": {
40+
"className": "WidgetNotifyModelCommand",
41+
"target": target,
42+
"event": event,
43+
"args": args,
44+
},
45+
}),
46+
);
47+
}

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 teams;
67
mod textures;
78

89
mod hashable_float;

native/src/sbc/objects/model/feature_s11n/fields.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,16 @@ impl TypedField<FeatureModel> for MidAim {
178178
})
179179
}
180180
fn set(s: &mut FeatureModel, id: i32, mid_aim: &MidAimPos) {
181+
let Some(pos) = Pos::get(s, id) else {
182+
return;
183+
};
184+
let mid = vec3_add(&pos, &mid_aim.mid);
185+
let aim = vec3_add(&pos, &mid_aim.aim);
181186
let _ = s
182187
.interface
183188
.synced_ctrl()
184189
.feature()
185-
.set_feature_mid_and_aim_pos(id, mid_aim.mid.into(), mid_aim.aim.into(), true);
190+
.set_feature_mid_and_aim_pos(id, mid.into(), aim.into(), false);
186191
}
187192
}
188193
inventory::submit! { FieldEntry::of::<MidAim>() }
@@ -407,3 +412,11 @@ impl TypedField<FeatureModel> for Rules {
407412
}
408413
}
409414
inventory::submit! { FieldEntry::of::<Rules>() }
415+
416+
fn vec3_add(a: &Vec3, b: &Vec3) -> Vec3 {
417+
Vec3 {
418+
x: a.x + b.x,
419+
y: a.y + b.y,
420+
z: a.z + b.z,
421+
}
422+
}

native/src/sbc/objects/model/feature_s11n/model.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ impl ObjectHandler for FeatureModel {
161161
}
162162
}
163163

164-
/// Apply a set of fields. Mirrors Lua `_SetAllFields`: rotation goes in
165-
/// before the loop (and again inside it), then is restored after a move.
164+
/// Apply a set of fields. Rotation and position affect how mid/aim offsets
165+
/// are interpreted, so restore them first and apply `midAimPos` last.
166166
fn set_fields(&mut self, model_id: i32, fields: &[FieldValue]) {
167167
let Some(spring_id) = self.ids.spring_id(model_id) else {
168168
return;
@@ -176,13 +176,23 @@ impl ObjectHandler for FeatureModel {
176176
if let Some(rot) = field_dyn(fields, "rot") {
177177
self.apply_one(spring_id, "rot", rot);
178178
}
179-
for field in fields {
180-
self.apply_one(spring_id, field.name, &*field.value);
179+
if let Some(pos) = field_dyn(fields, "pos") {
180+
self.apply_one(spring_id, "pos", pos);
181181
}
182182

183183
if let Some(rot) = restore_rot {
184184
self.apply_rotation(spring_id, rot);
185185
}
186+
187+
for field in fields {
188+
if matches!(field.name, "pos" | "rot" | "midAimPos") {
189+
continue;
190+
}
191+
self.apply_one(spring_id, field.name, &*field.value);
192+
}
193+
if let Some(mid_aim) = field_dyn(fields, "midAimPos") {
194+
self.apply_one(spring_id, "midAimPos", mid_aim);
195+
}
186196
}
187197

188198
fn field_value(&self, model_id: i32, name: &str) -> Option<Box<dyn Any>> {

native/src/sbc/objects/model/unit_s11n/fields.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,16 @@ impl TypedField<UnitModel> for MidAim {
177177
})
178178
}
179179
fn set(s: &mut UnitModel, id: i32, mid_aim: &MidAimPos) {
180+
let Some(pos) = Pos::get(s, id) else {
181+
return;
182+
};
183+
let mid = vec3_add(&pos, &mid_aim.mid);
184+
let aim = vec3_add(&pos, &mid_aim.aim);
180185
let _ = s.interface.synced_ctrl().unit().set_unit_mid_and_aim_pos(
181186
id,
182-
mid_aim.mid.into(),
183-
mid_aim.aim.into(),
184-
true,
187+
mid.into(),
188+
aim.into(),
189+
false,
185190
);
186191
}
187192
}
@@ -765,3 +770,11 @@ impl TypedField<UnitModel> for Commands {
765770
}
766771
}
767772
inventory::submit! { FieldEntry::of::<Commands>() }
773+
774+
fn vec3_add(a: &Vec3, b: &Vec3) -> Vec3 {
775+
Vec3 {
776+
x: a.x + b.x,
777+
y: a.y + b.y,
778+
z: a.z + b.z,
779+
}
780+
}

native/src/sbc/objects/model/unit_s11n/model.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,9 @@ impl ObjectHandler for UnitModel {
213213
}
214214
}
215215

216-
/// Apply a set of fields. Mirrors Lua `_SetAllFields`: rotation in before the
217-
/// loop (and again inside it), the health family in one aggregated call, then
218-
/// rotation restored after a move.
216+
/// Apply a set of fields. Rotation and position affect how mid/aim offsets
217+
/// are interpreted, so restore them first and apply `midAimPos` last. The
218+
/// health family still goes through one aggregated call.
219219
fn set_fields(&mut self, model_id: i32, fields: &[FieldValue]) {
220220
let Some(spring_id) = self.ids.spring_id(model_id) else {
221221
return;
@@ -229,18 +229,24 @@ impl ObjectHandler for UnitModel {
229229
if let Some(rot) = field_dyn(fields, "rot") {
230230
self.apply_one(spring_id, "rot", rot);
231231
}
232+
if let Some(pos) = field_dyn(fields, "pos") {
233+
self.apply_one(spring_id, "pos", pos);
234+
}
235+
236+
if let Some(rot) = restore_rot {
237+
self.apply_rotation(spring_id, rot);
238+
}
232239

233240
self.apply_health(spring_id, fields);
234241

235242
for field in fields {
236-
if is_health_field(field.name) {
243+
if is_health_field(field.name) || matches!(field.name, "pos" | "rot" | "midAimPos") {
237244
continue;
238245
}
239246
self.apply_one(spring_id, field.name, &*field.value);
240247
}
241-
242-
if let Some(rot) = restore_rot {
243-
self.apply_rotation(spring_id, rot);
248+
if let Some(mid_aim) = field_dyn(fields, "midAimPos") {
249+
self.apply_one(spring_id, "midAimPos", mid_aim);
244250
}
245251
}
246252

native/src/sbc/objects/tests/field_mutations.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ pub fn values_for_descriptors(
1717
) -> Vec<FieldMutation> {
1818
descriptors
1919
.iter()
20-
.map(|descriptor| FieldMutation {
21-
field: descriptor.name,
22-
value: value_for_descriptor(descriptor, current.get(descriptor.name), seed),
20+
.filter_map(|descriptor| {
21+
let current_value = current.get(descriptor.name)?;
22+
if descriptor.name == "health"
23+
|| descriptor.name == "midAimPos"
24+
|| descriptor.name == "team"
25+
{
26+
return None;
27+
}
28+
Some(FieldMutation {
29+
field: descriptor.name,
30+
value: value_for_descriptor(descriptor, Some(current_value), seed),
31+
})
2332
})
2433
.collect()
2534
}
@@ -53,6 +62,9 @@ fn value_for_descriptor(
5362
"native_set_param_label": format!("case-{seed}")
5463
}),
5564
FieldValueType::String => serde_json::json!(format!("native-set-param-{seed}")),
65+
FieldValueType::Vec3 if descriptor.name == "rot" => {
66+
vec3_offset(current, 0.1 + n % 3.0 * 0.01, 0.2 + n % 5.0 * 0.01, 0.3)
67+
}
5668
FieldValueType::Vec3 => vec3_offset(current, 7.0 + n % 11.0, 3.0, 5.0 + n % 13.0),
5769
}
5870
}
@@ -155,17 +167,17 @@ fn blocking(current: Option<&Value>, seed: u64) -> Value {
155167
})
156168
}
157169

158-
fn collision(current: Option<&Value>, seed: u64) -> Value {
170+
fn collision(current: Option<&Value>, _seed: u64) -> Value {
159171
serde_json::json!({
160-
"scaleX": positive_child(current, "scaleX", 8.0) + 1.0,
161-
"scaleY": positive_child(current, "scaleY", 16.0) + 2.0,
162-
"scaleZ": positive_child(current, "scaleZ", 8.0) + 3.0,
172+
"scaleX": positive_child(current, "scaleX", 8.0),
173+
"scaleY": positive_child(current, "scaleY", 16.0),
174+
"scaleZ": positive_child(current, "scaleZ", 8.0),
163175
"offsetX": child_number(current, "offsetX").unwrap_or(0.0) + 0.5,
164176
"offsetY": child_number(current, "offsetY").unwrap_or(0.0) + 0.25,
165177
"offsetZ": child_number(current, "offsetZ").unwrap_or(0.0) + 0.75,
166178
"vType": child_number(current, "vType").unwrap_or(1.0) as i32,
167-
"testType": 1 + (seed % 2) as i32,
168-
"axis": 1 + (seed % 3) as i32
179+
"testType": child_number(current, "testType").unwrap_or(1.0) as i32,
180+
"axis": child_number(current, "axis").unwrap_or(1.0) as i32
169181
})
170182
}
171183

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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::teams::model::team_manager::{Color, Team, TeamSide};
7+
use crate::sbc::teams::TeamManager;
8+
9+
/// Adds a team. 1:1 with `scen_edit/command/add_team_command.lua`: execute adds
10+
/// the team (recording the assigned id), unexecute removes it.
11+
#[derive(Deserialize, Debug)]
12+
pub struct AddTeamCommand {
13+
name: Option<String>,
14+
color: Option<Color>,
15+
#[serde(rename = "allyTeam")]
16+
ally_team: Option<i32>,
17+
side: Option<TeamSide>,
18+
#[serde(skip)]
19+
new_team_id: Option<i32>,
20+
}
21+
22+
impl Command for AddTeamCommand {
23+
fn execute(&mut self, ctx: &mut Context) {
24+
let team = Team {
25+
name: self.name.clone().unwrap_or_default(),
26+
color: self.color.unwrap_or_default(),
27+
ally_team: self.ally_team.unwrap_or_default(),
28+
side: self.side.clone().unwrap_or_default(),
29+
..Default::default()
30+
};
31+
let id = ctx.model::<TeamManager>().add_team(team, None);
32+
self.new_team_id = Some(id);
33+
}
34+
35+
fn unexecute(&mut self, ctx: &mut Context) {
36+
if let Some(id) = self.new_team_id {
37+
ctx.model::<TeamManager>().remove_team(id);
38+
}
39+
}
40+
}
41+
42+
register_command!(AddTeamCommand, "AddTeamCommand");

0 commit comments

Comments
 (0)