Skip to main content

concinnity_core/behavior/system/
state.rs

1// Persisted behavior state: the world variables plus which `once` behaviors
2// have fired, written by the `save` node and restored at world start.
3// Variables are keyed by their authored names, stable across world edits and
4// across a re-cook that reassigns slots. Fired flags are keyed by (asset id,
5// content hash) so a save from an edited world degrades safely: a behavior
6// whose id or content changed just loses its flag (and may fire once more),
7// never inherits another's.
8//
9// Per-entity locals are never saved: a spawned entity has no identity that
10// survives a re-cook, so there is nothing stable to key them by.
11//
12// Where the state is kept is the host's: a file, a preferences blob, nothing at
13// all. This module owns the shape and the keying; a `BehaviorStore` owns the
14// medium.
15
16use alloc::collections::BTreeMap;
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use crate::components::{Behavior, BehaviorLiteral};
21
22/// The behavior state a world carries between runs.
23#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
24pub struct BehaviorState {
25    /// World variables by authored name.
26    #[serde(default)]
27    pub vars: BTreeMap<String, BehaviorLiteral>,
28    /// `(asset id, content hash)` of every `once` behavior that has fired.
29    #[serde(default)]
30    pub fired: Vec<(u32, u64)>,
31}
32
33/// Where a host keeps persisted behavior state.
34///
35/// Read once when the world starts and written after any tick a `save` node ran
36/// in. A world whose host installs no store runs its behaviors and persists
37/// nothing.
38pub trait BehaviorStore: core::fmt::Debug + Send {
39    /// The stored state, or `None` when nothing was stored or it could not be
40    /// read.
41    fn read(&self) -> Option<BehaviorState>;
42
43    /// Store `state`, replacing whatever was there. Only the implementor knows
44    /// what the write was to, so reporting a failure is its job.
45    fn write(&self, state: &BehaviorState);
46}
47
48/// Content hash of a behavior definition. Asset identity is excluded (its serde
49/// skip), so a restored fired flag applies to the behavior it was saved for and
50/// to no other.
51pub fn def_hash(def: &Behavior) -> u64 {
52    let bytes = postcard::to_allocvec(def).unwrap_or_default();
53    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
54    for byte in bytes {
55        hash ^= byte as u64;
56        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
57    }
58    hash
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::components::BehaviorSource;
65    use crate::ecs::asset_id::AssetId;
66    use alloc::string::ToString;
67
68    #[test]
69    fn def_hash_tracks_content_not_identity() {
70        let a = Behavior {
71            asset_id: AssetId(1),
72            on: BehaviorSource::Tick,
73            ..Default::default()
74        };
75        let same_content = Behavior {
76            asset_id: AssetId(9),
77            ..a.clone()
78        };
79        assert_eq!(def_hash(&a), def_hash(&same_content));
80
81        let edited = Behavior {
82            on: BehaviorSource::Variable("v".to_string()),
83            ..a.clone()
84        };
85        assert_ne!(def_hash(&a), def_hash(&edited));
86    }
87}