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}