feat(api-gdext): ✨ Introduce multi-slot functionality in the Godot extension API with new functions and structs in lib.rs and corresponding tests in save_envelope.rs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
efdcd23e39
commit
2727d2f249
2 changed files with 90 additions and 5 deletions
|
|
@ -2891,7 +2891,17 @@ pub struct SaveEnvelope {
|
|||
|
||||
impl SaveEnvelope {
|
||||
/// Current envelope version. Bump on every breaking shape change.
|
||||
pub const CURRENT_VERSION: u32 = 1;
|
||||
///
|
||||
/// History:
|
||||
/// - **v1** — initial release: `{sim, presentation}` where
|
||||
/// `PresentationPlayer = {slot, player_name, race_id, gender_preset,
|
||||
/// color, is_human}`.
|
||||
/// - **v2** (Stage 3 of mod-system plan) — `PresentationPlayer` gains
|
||||
/// `controller_id: String` and `controller_hash: [u8; 32]` so saves
|
||||
/// fingerprint which `AiController` impl each slot ran. Dev-time
|
||||
/// v1 saves are rejected at load (per existing `load_from_json`
|
||||
/// policy — disposable saves, regenerate).
|
||||
pub const CURRENT_VERSION: u32 = 2;
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
|
|
@ -3130,6 +3140,21 @@ impl GdGameState {
|
|||
gender_preset,
|
||||
color,
|
||||
is_human,
|
||||
// Preserve existing controller_id if already set via
|
||||
// `set_player_controller` (Stage 3); otherwise leave empty
|
||||
// and let `drive_ai_slot` fall through to scripted:default.
|
||||
controller_id: self
|
||||
.presentation_players
|
||||
.iter()
|
||||
.find(|p| p.slot == slot_u8)
|
||||
.map(|p| p.controller_id.clone())
|
||||
.unwrap_or_default(),
|
||||
controller_hash: self
|
||||
.presentation_players
|
||||
.iter()
|
||||
.find(|p| p.slot == slot_u8)
|
||||
.map(|p| p.controller_hash)
|
||||
.unwrap_or([0u8; 32]),
|
||||
};
|
||||
// Upsert by slot — replace existing entry if present, else
|
||||
// insert in sorted order so the side-table iteration order is
|
||||
|
|
@ -3699,6 +3724,58 @@ impl GdGameState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stage 3 (mod system) — assign an [`AiController`] id to a player
|
||||
/// slot. The id is looked up in `mc_player_api::controllers` at
|
||||
/// dispatch time (`drive_ai_slot`). Built-in: `"scripted:default"`;
|
||||
/// mod-supplied ids (`"learned:duel-v1b"`, …) register through
|
||||
/// `mc_player_api::controllers::register_controller`.
|
||||
///
|
||||
/// Empty id = fall through to `DEFAULT_CONTROLLER_ID` in dispatch.
|
||||
///
|
||||
/// Also mirrors the id into `PresentationPlayer.controller_id` so
|
||||
/// the save envelope fingerprints which controller ran each slot
|
||||
/// (paired with the per-mod hash recorded by the mod loader).
|
||||
///
|
||||
/// Returns `false` (and logs) on out-of-range slot; this is a
|
||||
/// presentation-layer write that never fails on the simulator side.
|
||||
#[func]
|
||||
fn set_player_controller(&mut self, slot: i64, controller_id: GString) -> bool {
|
||||
let pi = slot as usize;
|
||||
if pi >= self.inner.players.len() {
|
||||
godot_error!(
|
||||
"GdGameState::set_player_controller: slot {slot} out of range ({} players)",
|
||||
self.inner.players.len()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let id_str = controller_id.to_string();
|
||||
self.inner.players[pi].controller_id = id_str.clone();
|
||||
let slot_u8 = pi as u8;
|
||||
if let Some(existing) = self
|
||||
.presentation_players
|
||||
.iter_mut()
|
||||
.find(|p| p.slot == slot_u8)
|
||||
{
|
||||
existing.controller_id = id_str;
|
||||
} else {
|
||||
// Lazily seed a presentation entry so save round-trip
|
||||
// includes the controller id even when the GDScript bridge
|
||||
// hasn't yet called `set_player_presentation_json`.
|
||||
let entry = mc_core::PresentationPlayer {
|
||||
slot: slot_u8,
|
||||
controller_id: id_str,
|
||||
..Default::default()
|
||||
};
|
||||
let insert_at = self
|
||||
.presentation_players
|
||||
.iter()
|
||||
.position(|p| p.slot > slot_u8)
|
||||
.unwrap_or(self.presentation_players.len());
|
||||
self.presentation_players.insert(insert_at, entry);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Current turn number.
|
||||
#[func]
|
||||
fn turn(&self) -> i64 {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ fn empty_envelope_round_trips() {
|
|||
};
|
||||
let json = serde_json::to_string(&env).expect("serialize");
|
||||
let back: SaveEnvelope = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(back.save_format_version, 1);
|
||||
assert_eq!(back.save_format_version, 2);
|
||||
assert!(back.presentation.is_empty());
|
||||
assert_eq!(back.sim.turn, 0);
|
||||
assert_eq!(back.sim.era, 0);
|
||||
|
|
@ -48,6 +48,8 @@ fn populated_envelope_round_trips_byte_identical() {
|
|||
gender_preset: "male".into(),
|
||||
color: [51, 102, 255, 255],
|
||||
is_human: true,
|
||||
controller_id: String::new(),
|
||||
controller_hash: [0u8; 32],
|
||||
},
|
||||
PresentationPlayer {
|
||||
slot: 1,
|
||||
|
|
@ -56,6 +58,8 @@ fn populated_envelope_round_trips_byte_identical() {
|
|||
gender_preset: "female".into(),
|
||||
color: [230, 51, 51, 255],
|
||||
is_human: false,
|
||||
controller_id: "scripted:default".into(),
|
||||
controller_hash: [0u8; 32],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -74,7 +78,7 @@ fn populated_envelope_round_trips_byte_identical() {
|
|||
let json2 = serde_json::to_string(&back).expect("re-serialize");
|
||||
assert_eq!(json, json2, "envelope must byte-equal across round-trip");
|
||||
|
||||
assert_eq!(back.save_format_version, 1);
|
||||
assert_eq!(back.save_format_version, 2);
|
||||
assert_eq!(back.sim.turn, 7);
|
||||
assert_eq!(back.sim.era, 2);
|
||||
assert_eq!(back.sim.map_seed, 0xfeed_face);
|
||||
|
|
@ -93,9 +97,13 @@ fn populated_envelope_round_trips_byte_identical() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn version_one_is_locked() {
|
||||
fn version_two_is_locked() {
|
||||
// Lock the wire format version. Future breaking changes must bump
|
||||
// this constant in tandem with the `load_from_json` rejection
|
||||
// logic — this test guards against an accidental silent bump.
|
||||
assert_eq!(SaveEnvelope::CURRENT_VERSION, 1);
|
||||
//
|
||||
// v1 → v2 (Stage 3 of mod-system plan): `PresentationPlayer` gained
|
||||
// `controller_id: String` and `controller_hash: [u8; 32]` so saves
|
||||
// fingerprint which AI controller each slot ran.
|
||||
assert_eq!(SaveEnvelope::CURRENT_VERSION, 2);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue