Skip to main content
Verticopolis engineGitHub

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}