fix(simulator): 🐛 Replace HashMap with BTreeMap in EcologyEngine’s continuation state to ensure deterministic serialization order for species registry
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2de6051c5f
commit
b0069856f8
1 changed files with 46 additions and 9 deletions
|
|
@ -1048,7 +1048,14 @@ impl EcologyEngine {
|
|||
pub fn continuation_state(&self) -> EcologyContinuationState {
|
||||
EcologyContinuationState {
|
||||
tile_populations: self.tile_populations.clone(),
|
||||
species_registry: self.species_registry.clone(),
|
||||
// Collect the runtime HashMap into a BTreeMap so serialization order
|
||||
// is deterministic (byte-stable saves) — HashMap iteration order
|
||||
// varies across instances and would break round-trip byte-equality.
|
||||
species_registry: self
|
||||
.species_registry
|
||||
.iter()
|
||||
.map(|(id, sp)| (*id, sp.clone()))
|
||||
.collect(),
|
||||
tick_count: self.tick_count,
|
||||
}
|
||||
}
|
||||
|
|
@ -1071,21 +1078,51 @@ impl EcologyEngine {
|
|||
|
||||
/// Serializable snapshot of an [`EcologyEngine`]'s mutable continuation state.
|
||||
///
|
||||
/// Round-trips via serde (all three fields are serde types: `PopulationSlot`,
|
||||
/// `Species`, `u64`). `BTreeMap`/`HashMap` of these serialize deterministically
|
||||
/// for `BTreeMap`; the registry is a `HashMap` but its contents are restored by
|
||||
/// key so order is irrelevant to correctness. Persisted as the opaque-JSON
|
||||
/// `worldsim_state` payload in `mc_save::SaveFile`.
|
||||
/// Round-trips via serde, byte-stably: `tile_populations` serializes as an
|
||||
/// ordered sequence of pairs (serde_json rejects tuple map keys), and
|
||||
/// `species_registry` is a `BTreeMap` so its JSON key order is deterministic.
|
||||
/// Persisted as the opaque-JSON `worldsim_state` payload in `mc_save::SaveFile`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EcologyContinuationState {
|
||||
/// Per-tile fauna populations.
|
||||
/// Per-tile fauna populations. Serialized as an ordered sequence of
|
||||
/// `((col, row), slots)` pairs rather than a JSON object: serde_json cannot
|
||||
/// encode a map with tuple keys as object keys ("key must be a string"), and
|
||||
/// this representation round-trips losslessly while staying deterministic
|
||||
/// (BTreeMap iteration is sorted).
|
||||
#[serde(with = "tile_populations_serde")]
|
||||
pub tile_populations: BTreeMap<(i32, i32), Vec<PopulationSlot>>,
|
||||
/// Full species registry, including procedurally-emerged species.
|
||||
pub species_registry: HashMap<u32, Species>,
|
||||
/// Full species registry, including procedurally-emerged species. A
|
||||
/// `BTreeMap` (not the engine's runtime `HashMap`) so JSON key order is
|
||||
/// deterministic and saves are byte-stable.
|
||||
pub species_registry: BTreeMap<u32, Species>,
|
||||
/// Emergence-throttle tick counter.
|
||||
pub tick_count: u64,
|
||||
}
|
||||
|
||||
/// Serde adapter for `EcologyContinuationState::tile_populations`: represents the
|
||||
/// tuple-keyed `BTreeMap<(i32, i32), Vec<PopulationSlot>>` as an ordered sequence
|
||||
/// of `((col, row), slots)` pairs so it survives JSON serialization (serde_json
|
||||
/// rejects non-string map keys). Deterministic: BTreeMap iterates in sorted order.
|
||||
mod tile_populations_serde {
|
||||
use super::{BTreeMap, PopulationSlot};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
map: &BTreeMap<(i32, i32), Vec<PopulationSlot>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let pairs: Vec<(&(i32, i32), &Vec<PopulationSlot>)> = map.iter().collect();
|
||||
pairs.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<BTreeMap<(i32, i32), Vec<PopulationSlot>>, D::Error> {
|
||||
let pairs: Vec<((i32, i32), Vec<PopulationSlot>)> = Vec::deserialize(deserializer)?;
|
||||
Ok(pairs.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic hash for seed dispersal RNG — seeded from tick, tile coords, and species ID.
|
||||
/// Returns a u64 that can be converted to [0, 1) by dividing by u64::MAX.
|
||||
fn seed_dispersal_hash(tick: u64, col: i32, row: i32, species_id: u32) -> u64 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue