Skip to main content
Verticopolis engineGitHub

verticopolis_engine/view/
signature.rs

1// The tower signature: undo's per-command fingerprint. It reads the fields
2// the web game's undo signature string reads (`towerStateSig`), so a capture
3// and a commit compare equal when those fields are equal, whatever was read
4// in between. Money, the other half of undo's check, stays a value compare
5// beside it. A full state digest would answer too, but costs about 108 ms on
6// the large tower.
7use crate::sim::Simulation;
8use crate::tower::Tower;
9
10impl Simulation {
11    /// A fingerprint of the tower, for undo: read it before and after a
12    /// command, and the command changed the tower exactly when the two differ
13    /// (barring a 64-bit collision). Money is not part of it; compare that
14    /// beside it. It walks every room and shaft, so read it once per
15    /// command; it is too slow to read every frame.
16    ///
17    /// ```
18    /// use verticopolis_engine::clock::GameMode;
19    /// use verticopolis_engine::facilities::Kind;
20    /// use verticopolis_engine::sim::Simulation;
21    ///
22    /// let mut sim = Simulation::new_game(1, GameMode::Classic);
23    /// let before = sim.tower_signature();
24    /// sim.build(Kind::Lobby, 1, 100);
25    /// assert_ne!(sim.tower_signature(), before);
26    /// ```
27    pub fn tower_signature(&self) -> u64 {
28        fingerprint(&self.tower)
29    }
30}
31
32/// A 64-bit multiply-rotate hash over the fields, written word by word.
33/// Deterministic and seedless: it is compared only with itself in the same
34/// engine, and never saved or hashed.
35struct Fingerprint(u64);
36
37impl Fingerprint {
38    const K: u64 = 0x9e37_79b9_7f4a_7c15;
39
40    fn word(&mut self, w: u64) {
41        self.0 = (self.0.rotate_left(5) ^ w).wrapping_mul(Self::K);
42    }
43
44    fn int(&mut self, n: i64) {
45        self.word(n as u64);
46    }
47
48    /// A number as `towerStateSig` prints it: `-0` prints as `0`, and every
49    /// NaN as `NaN`.
50    fn num(&mut self, x: f64) {
51        let x = if x == 0.0 { 0.0 } else { x };
52        self.word(if x.is_nan() {
53            f64::NAN.to_bits()
54        } else {
55            x.to_bits()
56        });
57    }
58
59    /// An optional value, tagged so an absent one differs from any present.
60    fn opt<T>(&mut self, v: Option<T>, f: impl FnOnce(&mut Self, T)) {
61        match v {
62            None => self.word(0),
63            Some(t) => {
64                self.word(1);
65                f(self, t);
66            }
67        }
68    }
69
70    fn text(&mut self, s: &str) {
71        let bytes = s.as_bytes();
72        self.word(bytes.len() as u64);
73        let mut chunks = bytes.chunks_exact(8);
74        for c in &mut chunks {
75            self.word(u64::from_le_bytes(c.try_into().unwrap()));
76        }
77        let rest = chunks.remainder();
78        if !rest.is_empty() {
79            let mut last = [0u8; 8];
80            last[..rest.len()].copy_from_slice(rest);
81            self.word(u64::from_le_bytes(last));
82        }
83    }
84
85    /// A list as `towerStateSig` joins it: an absent list reads as an empty
86    /// one.
87    fn list<T: Copy>(&mut self, v: Option<&[T]>, f: impl Fn(&mut Self, T)) {
88        let v = v.unwrap_or(&[]);
89        self.word(v.len() as u64);
90        for &x in v {
91            f(self, x);
92        }
93    }
94}
95
96/// The fields `towerStateSig` reads, in its order: per unit its kind, floor,
97/// x, label, rent, No Rate flag, film policy and subtype; per transport its
98/// kind, x, bottom, top, cars, skipped floors and schedule (`schedSig`'s
99/// five player-set fields).
100fn fingerprint(tower: &Tower) -> u64 {
101    let mut h = Fingerprint(0);
102    h.word(tower.units.len() as u64);
103    for u in &tower.units {
104        h.word(u.kind as u64);
105        h.int(u.floor);
106        h.int(u.x);
107        h.text(&u.label);
108        h.opt(u.rent, Fingerprint::num);
109        h.word(u.no_rate as u64);
110        h.opt(u.extras().film_policy, Fingerprint::text);
111        h.opt(u.subtype, Fingerprint::text);
112    }
113    h.word(tower.transports.len() as u64);
114    for t in &tower.transports {
115        h.word(t.kind as u64);
116        h.int(t.x);
117        h.int(t.bottom);
118        h.int(t.top);
119        h.int(t.cars);
120        h.list(t.skip_floors.as_deref(), Fingerprint::num);
121        h.opt(t.schedule.as_ref(), |h, s| {
122            h.list(s.weekday.as_deref(), Fingerprint::int);
123            h.list(s.weekend.as_deref(), Fingerprint::int);
124            h.opt(s.waiting_car_response, Fingerprint::num);
125            h.opt(s.standard_floor_departure, Fingerprint::num);
126            h.list(s.home_floors.as_deref(), Fingerprint::int);
127        });
128    }
129    h.0
130}