Skip to main content
Verticopolis engineGitHub

verticopolis_engine/
frame.rs

1//! The flat frame: everything a renderer reads each frame, as one
2//! `Vec<f64>` ([`frame_view`]), and the new log messages ([`log_since`]).
3//!
4//! Read the frame by slot name, never by bare index: [`header`] names the
5//! header slots, [`person`], [`unit`](mod@unit), [`transport`] and [`car`] the slots
6//! of each record, and [`FRAME_HEADER`], [`PERSON_FIXED`], [`UNIT_RECORD`],
7//! [`TRANSPORT_FIXED`] and [`CAR_RECORD`] the widths. [`frame_view`] says
8//! what each value means. The JavaScript binding hands over the same frame as
9//! one `Float64Array`.
10//!
11//! [`crate::view`] is the newer read path: it sends only the rooms that
12//! changed since the last frame, plus a structural snapshot for the floors
13//! and shafts. Both read the same simulation.
14//!
15//! ```
16//! use verticopolis_engine::clock::GameMode;
17//! use verticopolis_engine::frame::{frame_view, header, FRAME_LAYOUT_VERSION};
18//! use verticopolis_engine::sim::{Simulation, STARTING_MONEY};
19//!
20//! let sim = Simulation::new_game(1, GameMode::Classic);
21//! let frame = frame_view(&sim);
22//! assert_eq!(frame[header::MONEY], STARTING_MONEY);
23//! assert_eq!(frame[header::PEOPLE], 0.0);
24//! # let _ = FRAME_LAYOUT_VERSION;
25//! ```
26use serde_json::Value;
27
28use crate::sim::Simulation;
29
30/// The version of the frame's layout, also in [`crate::identity::identity`].
31/// It goes up when a slot moves, a record's width changes, or a slot or code
32/// changes meaning, so a decoder can refuse a layout it was not written for.
33pub const FRAME_LAYOUT_VERSION: u32 = 1;
34
35/// The header slots of the frame view by name, so a native decoder reads
36/// `v[header::MONEY]` instead of a bare index. A group of values starts at
37/// its named slot: `EXPLOSION_FX` is seq, floor, x; `THIEF_FX` is seq,
38/// caught, floor; `TREASURE_FX` is seq, floor, x; `EVENT_COUNTS` is fires,
39/// firesGutRooms, bombs. [`frame_view`] describes each value.
40// Each slot is named for its value, and `frame_view` says what it holds.
41#[allow(missing_docs)]
42pub mod header {
43    pub const MINUTES: usize = 0;
44    pub const TOWER_REVISION: usize = 1;
45    pub const MEAL_OVERLAY_REVISION: usize = 2;
46    pub const LOG_SEQ: usize = 3;
47    pub const MONEY: usize = 4;
48    pub const STAR: usize = 5;
49    pub const WEATHER: usize = 6;
50    pub const SANTA_FX_SEQ: usize = 7;
51    pub const EXPLOSION_FX: usize = 8;
52    pub const THIEF_FX: usize = 11;
53    pub const TREASURE_FX: usize = 14;
54    pub const VIP_FX_SEQ: usize = 17;
55    pub const EVENT_COUNTS: usize = 18;
56    pub const PENDING_CHOICE: usize = 21;
57    pub const ON_HOUR_RUNS: usize = 22;
58    pub const PEOPLE: usize = 23;
59    pub const UNITS: usize = 24;
60    pub const TRANSPORTS: usize = 25;
61}
62
63/// The fixed slots of a person record by name, counted from the record's
64/// start. The route follows them: `FLOOR_COUNT` floors, then `SHAFT_COUNT`
65/// shafts.
66// Each slot is named for its value, and `frame_view` says what it holds.
67#[allow(missing_docs)]
68pub mod person {
69    pub const ID: usize = 0;
70    pub const SEED: usize = 1;
71    pub const STAFF: usize = 2;
72    pub const STATE: usize = 3;
73    pub const FLOOR: usize = 4;
74    pub const X: usize = 5;
75    pub const FY: usize = 6;
76    pub const WAIT: usize = 7;
77    pub const ORIGIN_FLOOR: usize = 8;
78    pub const ORIGIN_UNIT_ID: usize = 9;
79    pub const VENUE_UNIT_ID: usize = 10;
80    pub const MEAL_VENUE_ID: usize = 11;
81    pub const COUNTED_HOTEL_GUEST: usize = 12;
82    pub const ROUTINE: usize = 13;
83    pub const RETURNING: usize = 14;
84    pub const DWELL_SECONDS_LEFT: usize = 15;
85    pub const FLOOR_COUNT: usize = 16;
86    pub const SHAFT_COUNT: usize = 17;
87}
88
89/// Fixed header slots of the frame view, before the three variable sections.
90pub const FRAME_HEADER: usize = header::TRANSPORTS + 1;
91
92/// Fixed slots of a person record, before its route (`floors` then `shafts`).
93pub const PERSON_FIXED: usize = person::SHAFT_COUNT + 1;
94
95/// The slots of a unit record by name.
96// Each slot is named for its value, and `frame_view` says what it holds.
97#[allow(missing_docs)]
98pub mod unit {
99    pub const ID: usize = 0;
100    pub const STATE: usize = 1;
101    pub const OCCUPANTS: usize = 2;
102    pub const CUSTOMERS_IN: usize = 3;
103    pub const HOTEL_CUSTOMERS_IN: usize = 4;
104    pub const OUT_FOR_MEAL: usize = 5;
105}
106
107/// The slots of a transport record by name: `ID` and `CARS`, then `CARS`
108/// car records of [`CAR_RECORD`] slots, each laid out as [`car`].
109// Each slot is named for its value, and `frame_view` says what it holds.
110#[allow(missing_docs)]
111pub mod transport {
112    pub const ID: usize = 0;
113    pub const CARS: usize = 1;
114}
115
116/// The slots of one car in a transport record, counted from the car's start.
117// Each slot is named for its value, and `frame_view` says what it holds.
118#[allow(missing_docs)]
119pub mod car {
120    pub const POSITION: usize = 0;
121    pub const LOAD: usize = 1;
122    pub const DIRECTION: usize = 2;
123}
124
125/// Slots of a unit record.
126pub const UNIT_RECORD: usize = unit::OUT_FOR_MEAL + 1;
127
128/// Slots of a transport record before its cars.
129pub const TRANSPORT_FIXED: usize = transport::CARS + 1;
130
131/// Slots of each car in a transport record.
132pub const CAR_RECORD: usize = car::DIRECTION + 1;
133
134/// The routine code for a routine this layout has no code for yet. A decoder
135/// should refuse a frame that carries it: the engine has gained a routine the
136/// decoder does not know.
137pub const UNKNOWN_ROUTINE: f64 = 255.0;
138
139/// The frame: every value a renderer reads between two structural reads,
140/// flat, so it crosses to another language as one array of numbers.
141///
142/// Header (`FRAME_HEADER` slots): 0 minutes, 1 tower revision, 2 meal
143/// overlay revision, 3 logSeq, 4 money, 5 star, 6 weather (0 clear, 1 cloudy,
144/// 2 rain), 7 santaFxSeq, 8..10 explosionFx (seq, floor, x), 11..13 thiefFx
145/// (seq, caught, floor), 14..16 treasureFx (seq, floor, x), 17 vipFxSeq,
146/// 18..20 event counts (fires, firesGutRooms, bombs), 21 a pending choice
147/// (0 or 1), 22 onHourRuns, 23 people count, 24 unit count, 25 transport
148/// count.
149///
150/// Then `people count` records of `PERSON_FIXED + floors + shafts`: id, seed,
151/// staff (0 or 1), state (a person state code), floor, x, fy, wait, then
152/// the routing slice the suites read: originFloor (always present, so -1 is
153/// a real basement floor), originUnitId, venueUnitId, mealVenueId (each -1
154/// when absent), countedHotelGuest (0 or 1), routine
155/// (0 none, 1 schoolRun, 2 salesCall, 255 a routine with no code yet, which
156/// the decoder rejects), returning (0 or 1), dwellSecondsLeft
157/// (NaN when absent: a drained timer stays negative on the person through
158/// the return leg, so no number is free), the floors count, the shafts
159/// count, then the route's floors and its shafts. Then `unit count` records
160/// of 6: id, state (a room state code), occupants, customersIn,
161/// hotelCustomersIn, outForMeal (an absent counter is -1). Then
162/// `transport count` records of `2 + 3 * cars`: id, cars, then per car
163/// position, load (-1 when the shaft keeps none), direction.
164///
165/// [`crate::view::layout_json`] lists the person and room state codes, the
166/// weather codes and the routine codes by name.
167pub fn frame_view(sim: &Simulation) -> Vec<f64> {
168    use crate::crowd::PState;
169    use crate::sim_loop::Weather;
170    use crate::tower::UnitState;
171    let people = &sim.crowd.people;
172    let units = &sim.tower.units;
173    let transports = &sim.tower.transports;
174    let mut v = Vec::with_capacity(
175        FRAME_HEADER
176            + people
177                .iter()
178                .map(|p| PERSON_FIXED + p.floors.len() + p.shafts.len())
179                .sum::<usize>()
180            + units.len() * UNIT_RECORD
181            + transports
182                .iter()
183                .map(|t| TRANSPORT_FIXED + CAR_RECORD * t.cars as usize)
184                .sum::<usize>(),
185    );
186    let fx = &sim.fx;
187    v.extend_from_slice(&[
188        sim.clock.minutes,
189        sim.tower.revision as f64,
190        sim.tower.meal_overlay_revision as f64,
191        sim.log_seq as f64,
192        sim.money,
193        sim.star as f64,
194        match sim.weather {
195            Weather::Clear => 0.0,
196            Weather::Cloudy => 1.0,
197            Weather::Rain => 2.0,
198        },
199        fx.santa_seq as f64,
200        fx.explosion.seq as f64,
201        fx.explosion.floor as f64,
202        fx.explosion.x,
203        fx.thief.seq as f64,
204        if fx.thief.caught { 1.0 } else { 0.0 },
205        fx.thief.floor as f64,
206        fx.treasure.seq as f64,
207        fx.treasure.floor as f64,
208        fx.treasure.x,
209        fx.vip_seq as f64,
210        sim.events.fires_started as f64,
211        sim.events.fires_gut_rooms as f64,
212        sim.events.bombs_detonated as f64,
213        if sim.events.pending.is_some() {
214            1.0
215        } else {
216            0.0
217        },
218        sim.on_hour_runs as f64,
219        people.len() as f64,
220        units.len() as f64,
221        transports.len() as f64,
222    ]);
223    debug_assert_eq!(v.len(), FRAME_HEADER);
224    for p in people {
225        let state = match p.state {
226            PState::ToShaft => 0.0,
227            PState::Waiting => 1.0,
228            PState::Riding => 2.0,
229            PState::Climbing => 3.0,
230            PState::ToDest => 4.0,
231            PState::Dwelling => 5.0,
232            PState::Done => 6.0,
233        };
234        let id = |v: Option<i64>| v.map_or(-1.0, |n| n as f64);
235        let flag = |b: bool| if b { 1.0 } else { 0.0 };
236        let routine = match p.routine {
237            None => 0.0,
238            Some("schoolRun") => 1.0,
239            Some("salesCall") => 2.0,
240            // The routines are engine-set literals; a new one must be added
241            // here and to `ROUTINES` in frameView.ts. Until then it crosses
242            // as a code the decoder rejects by name. The engine's unit tests
243            // stop here first; a host's build never panics in this read.
244            Some(other) => {
245                if cfg!(test) {
246                    panic!("routine {other} has no frame code");
247                }
248                UNKNOWN_ROUTINE
249            }
250        };
251        v.extend_from_slice(&[
252            p.id as f64,
253            p.seed as f64,
254            flag(p.staff),
255            state,
256            p.floor as f64,
257            p.x,
258            p.fy,
259            p.wait,
260            p.origin_floor as f64,
261            id(p.origin_unit_id),
262            id(p.venue_unit_id),
263            id(p.meal_venue_id),
264            flag(p.counted_hotel_guest),
265            routine,
266            flag(p.returning),
267            p.dwell_seconds_left.unwrap_or(f64::NAN),
268            p.floors.len() as f64,
269            p.shafts.len() as f64,
270        ]);
271        v.extend(p.floors.iter().map(|f| *f as f64));
272        v.extend(p.shafts.iter().map(|s| *s as f64));
273    }
274    let counter = |c: Option<i64>| c.map_or(-1.0, |n| n as f64);
275    for u in units {
276        let state = match u.state {
277            UnitState::Construction => 0.0,
278            UnitState::Empty => 1.0,
279            UnitState::Occupied => 2.0,
280            UnitState::MovingIn => 3.0,
281            UnitState::Vacating => 4.0,
282            UnitState::Asleep => 5.0,
283            UnitState::Dirty => 6.0,
284            UnitState::Infested => 7.0,
285            UnitState::Fire => 8.0,
286            UnitState::Gutted => 9.0,
287        };
288        v.extend_from_slice(&[
289            u.id as f64,
290            state,
291            u.occupants as f64,
292            counter(u.customers_in),
293            counter(u.hotel_customers_in),
294            counter(u.out_for_meal),
295        ]);
296    }
297    for t in transports {
298        v.push(t.id as f64);
299        v.push(t.cars as f64);
300        for i in 0..t.cars as usize {
301            v.push(t.car_positions.get(i).copied().unwrap_or(0.0));
302            v.push(
303                t.car_load
304                    .as_ref()
305                    .and_then(|l| l.get(i).copied())
306                    .unwrap_or(-1.0),
307            );
308            v.push(t.car_dir.get(i).copied().unwrap_or(0) as f64);
309        }
310    }
311    v
312}
313
314// The encoders below write what `frame_view` writes, for the engine view
315// (`view/`), which shares the codes and the transport records. `frame_view`
316// keeps its own copies; `view/frame_tests.rs` and `view/layout_tests.rs`
317// check that the two agree.
318
319/// The `PersonState` index a frame carries for a person's state.
320pub(crate) fn person_state_code(state: crate::crowd::PState) -> f64 {
321    use crate::crowd::PState;
322    match state {
323        PState::ToShaft => 0.0,
324        PState::Waiting => 1.0,
325        PState::Riding => 2.0,
326        PState::Climbing => 3.0,
327        PState::ToDest => 4.0,
328        PState::Dwelling => 5.0,
329        PState::Done => 6.0,
330    }
331}
332
333/// The `UnitState` index a frame carries for a unit's state.
334pub(crate) fn unit_state_code(state: crate::tower::UnitState) -> f64 {
335    use crate::tower::UnitState;
336    match state {
337        UnitState::Construction => 0.0,
338        UnitState::Empty => 1.0,
339        UnitState::Occupied => 2.0,
340        UnitState::MovingIn => 3.0,
341        UnitState::Vacating => 4.0,
342        UnitState::Asleep => 5.0,
343        UnitState::Dirty => 6.0,
344        UnitState::Infested => 7.0,
345        UnitState::Fire => 8.0,
346        UnitState::Gutted => 9.0,
347    }
348}
349
350/// The weather code a frame carries: 0 clear, 1 cloudy, 2 rain.
351pub(crate) fn weather_code(weather: crate::sim_loop::Weather) -> f64 {
352    use crate::sim_loop::Weather;
353    match weather {
354        Weather::Clear => 0.0,
355        Weather::Cloudy => 1.0,
356        Weather::Rain => 2.0,
357    }
358}
359
360/// A unit's frame fields after its id: state, occupants, customersIn,
361/// hotelCustomersIn, outForMeal (an absent counter is -1).
362pub(crate) fn unit_fields(u: &crate::tower::Unit) -> [f64; 5] {
363    let counter = |c: Option<i64>| c.map_or(-1.0, |n| n as f64);
364    [
365        unit_state_code(u.state),
366        u.occupants as f64,
367        counter(u.customers_in),
368        counter(u.hotel_customers_in),
369        counter(u.out_for_meal),
370    ]
371}
372
373/// The slots `push_transports` writes for these transports.
374pub(crate) fn transports_len(transports: &[crate::tower::Transport]) -> usize {
375    transports
376        .iter()
377        .map(|t| TRANSPORT_FIXED + CAR_RECORD * t.cars as usize)
378        .sum()
379}
380
381/// Every transport's record: id, cars, then per car position, load (-1 when
382/// the shaft keeps none) and direction.
383pub(crate) fn push_transports(v: &mut Vec<f64>, transports: &[crate::tower::Transport]) {
384    for t in transports {
385        v.push(t.id as f64);
386        v.push(t.cars as f64);
387        for i in 0..t.cars as usize {
388            v.push(t.car_positions.get(i).copied().unwrap_or(0.0));
389            v.push(
390                t.car_load
391                    .as_ref()
392                    .and_then(|l| l.get(i).copied())
393                    .unwrap_or(-1.0),
394            );
395            v.push(t.car_dir.get(i).copied().unwrap_or(0) as f64);
396        }
397    }
398}
399
400/// The log messages numbered after `seq`, oldest first, as JSON. The newest
401/// message's number is the frame's [`header::LOG_SEQ`], so a host passes the
402/// last number it showed and gets only the new ones.
403pub fn log_since(sim: &Simulation, seq: i64) -> Value {
404    let len = sim.log.len() as i64;
405    let first = sim.log_seq - len + 1;
406    Value::Array(
407        sim.log
408            .iter()
409            .enumerate()
410            .map(|(i, e)| (first + i as i64, e))
411            .filter(|(n, _)| *n > seq)
412            .map(|(n, e)| {
413                serde_json::json!({ "seq": n, "minute": e.minute, "text": e.text, "kind": e.kind.as_str() })
414            })
415            .collect(),
416    )
417}
418
419#[cfg(test)]
420#[path = "frame_tests.rs"]
421mod tests;