verticopolis_engine/view/meta.rs
1//! Unit metadata: each unit's label and subtype. Neither moves
2//! `tower.revision`, so they have their own revision, `tower.meta_revision`,
3//! and one setter each that every engine write goes through (the save loader
4//! and a new unit's construction aside). A setter that changes a value bumps
5//! the revision and logs the unit's id, so a view fetches the metadata of
6//! just the units that changed since its last read.
7//!
8//! Every tower starts at meta revision 1 with an empty log, so a host that
9//! has read nothing yet (it passes no `since`, or 0) gets a full listing,
10//! including the labels and subtypes a loaded save carried.
11use serde_json::{json, Value};
12
13use crate::tower::Tower;
14
15/// The most ids the log holds before it drops its older half. A view reads
16/// the log every frame the meta revision moves, so it never falls this far
17/// behind in play; a reader that does gets a full listing.
18pub(super) const LOG_CAP: usize = 4096;
19
20/// The meta revision a tower starts at.
21pub(crate) const FIRST_META_REVISION: i64 = 1;
22
23/// The ids the setters changed, oldest first, each with the meta revision
24/// its change made. `floor` is the oldest revision the log answers from: it
25/// answers "what changed after `since`" only for `since >= floor`, which
26/// starts at the tower's first meta revision and rises as the log drops
27/// entries.
28pub struct MetaLog {
29 entries: Vec<(i64, i64)>,
30 pub(super) floor: i64,
31}
32
33impl MetaLog {
34 /// An empty log for a tower at its first meta revision.
35 pub(crate) fn new() -> MetaLog {
36 MetaLog {
37 entries: Vec::new(),
38 floor: FIRST_META_REVISION,
39 }
40 }
41
42 fn push(&mut self, revision: i64, id: i64) {
43 if self.entries.len() >= LOG_CAP {
44 let drop = self.entries.len() / 2;
45 self.floor = self.entries[drop - 1].0;
46 self.entries.drain(..drop);
47 }
48 self.entries.push((revision, id));
49 }
50
51 /// The ids changed after `since`, oldest change first, each once, or
52 /// `None` when the log no longer reaches back to `since`.
53 fn since(&self, since: i64) -> Option<Vec<i64>> {
54 if since < self.floor {
55 return None;
56 }
57 let start = self.entries.partition_point(|&(rev, _)| rev <= since);
58 let mut seen = std::collections::HashSet::new();
59 Some(
60 self.entries[start..]
61 .iter()
62 .map(|&(_, id)| id)
63 .filter(|id| seen.insert(*id))
64 .collect(),
65 )
66 }
67}
68
69impl Tower {
70 /// Set the label of the unit at `i` in `units`, an index the caller
71 /// holds. The one write path for a label besides the save loader and
72 /// `place`, which gives a new unit its catalog name. Returns whether the
73 /// label changed; only a change bumps `meta_revision` and logs the unit.
74 pub(crate) fn set_unit_label_at(&mut self, i: usize, label: String) -> bool {
75 if self.units[i].label == label {
76 return false;
77 }
78 if let Some(j) = self.journal.as_mut() {
79 j.unit_edit(i, &self.units[i]);
80 }
81 let u = &mut self.units[i];
82 u.label = label;
83 let id = u.id;
84 self.note_meta(id);
85 true
86 }
87
88 /// Set the subtype of the unit with this id: the one write path for a
89 /// subtype besides the save loader. Returns whether it changed; only a
90 /// change bumps `meta_revision` and logs the unit. False for an unknown
91 /// id.
92 pub(crate) fn set_unit_subtype(&mut self, id: i64, subtype: Option<&'static str>) -> bool {
93 let Some(&i) = self.by_id.get(&id) else {
94 return false;
95 };
96 if self.units[i].subtype == subtype {
97 return false;
98 }
99 if let Some(j) = self.journal.as_mut() {
100 j.unit_edit(i, &self.units[i]);
101 }
102 let u = &mut self.units[i];
103 u.subtype = subtype;
104 self.note_meta(id);
105 true
106 }
107
108 /// `u.label = label`, the editor's rename: trimmed as JavaScript trims,
109 /// an empty name falling back to the catalog name. False for an unknown
110 /// id; true otherwise, the name unchanged included.
111 pub fn set_label(&mut self, id: i64, label: &str) -> bool {
112 let Some(&i) = self.by_id.get(&id) else {
113 return false;
114 };
115 let trimmed = crate::load::js_trim(label);
116 let name = if trimmed.is_empty() {
117 self.units[i].kind.facility().name.to_string()
118 } else {
119 trimmed.to_string()
120 };
121 self.set_unit_label_at(i, name);
122 true
123 }
124
125 fn note_meta(&mut self, id: i64) {
126 self.meta_revision += 1;
127 self.view.meta_log.push(self.meta_revision, id);
128 }
129}
130
131/// One unit's metadata as the listing carries it: `[id, label, subtype]`,
132/// the subtype `null` when the unit has none.
133fn record(id: i64, label: &str, subtype: Option<&str>) -> Value {
134 json!([id, label, subtype])
135}
136
137/// A meta revision read from a raw JavaScript number: a whole number from 0
138/// up to 2^53, or `None` for anything else (a fraction, a negative, NaN, an
139/// infinity, a number past exact integers, or no number at all), which
140/// `meta_since` answers with a full listing.
141pub fn whole_revision(raw: Option<f64>) -> Option<i64> {
142 const EXACT: f64 = 9_007_199_254_740_992.0;
143 raw.filter(|s| s.fract() == 0.0 && (0.0..=EXACT).contains(s))
144 .map(|s| s as i64)
145}
146
147/// The metadata a view needs after `since`, as
148/// `{ "revision", "full", "units": [[id, label, subtype], ...] }`
149/// (`layout::META_RECORD` names the record's fields).
150///
151/// For a `since` the log can answer (from the log's floor up to
152/// `meta_revision`), `full` is false and `units` lists every unit changed
153/// after it that still exists, once each, with its current values. For any
154/// other `since` (`None`, a revision this tower never reached, or one older
155/// than the log answers from, 0 included), `full` is true and `units` lists
156/// every unit whose label differs from its catalog name or that has a
157/// subtype, in unit-list order: a unit it leaves out has its catalog name and
158/// no subtype. `revision` is `meta_revision`, the `since` of the next read.
159/// A removed unit is never listed, and an id is never given to another unit
160/// (`Tower::next_id` only grows), so what a host still holds for a removed
161/// id goes unused. A `since` belongs to one engine: a load or an attach
162/// builds a tower that starts again at revision 1, so a host on a new
163/// engine asks with nothing or 0, as it reads a fresh structure.
164pub fn meta_since(tower: &Tower, since: Option<i64>) -> Value {
165 let (full, ids) = meta_changed(tower, since);
166 let units: Vec<Value> = ids
167 .iter()
168 .filter_map(|id| tower.get_unit(*id))
169 .map(|u| record(u.id, &u.label, u.subtype))
170 .collect();
171 json!({
172 "revision": tower.meta_revision,
173 "full": full,
174 "units": units,
175 })
176}
177
178/// The ids `meta_since` lists, with whether the listing is full: the units
179/// changed after `since` when the log still reaches it, else every unit
180/// whose label or subtype is not its kind's default.
181pub fn meta_changed(tower: &Tower, since: Option<i64>) -> (bool, Vec<i64>) {
182 let reachable = since.filter(|&s| s <= tower.meta_revision);
183 match reachable.and_then(|s| tower.view.meta_log.since(s)) {
184 Some(ids) => (
185 false,
186 ids.into_iter()
187 .filter(|id| tower.get_unit(*id).is_some())
188 .collect(),
189 ),
190 None => (
191 true,
192 tower
193 .units
194 .iter()
195 .filter(|u| u.subtype.is_some() || u.label != u.kind.facility().name)
196 .map(|u| u.id)
197 .collect(),
198 ),
199 }
200}