Skip to main content

banished_game/work/
tracker.rs

1//! What the town makes and uses: the stock manager's counters for each slot (used and made since
2//! the last sample) and each workplace's tracker (what it made this year and last). See
3//! docs/TOWN-INFO.md.
4//!
5//! Original: the trackers, `FUN_140059930`.
6// Game +0xf0 → +0x170; +0xc; +0x10
7
8use crate::town::Sim;
9
10// +0x28; +0x38; +0x48
11/// A workplace's tracker: what it made this year (material and amount), what it made last year,
12/// and the year these belong to.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Tracker {
15    /// What it made this year: `(raw material, amount)`.
16    pub this_year: Vec<(usize, i32)>,
17    /// What it made last year.
18    pub last_year: Vec<(usize, i32)>,
19    /// The year these belong to.
20    pub year: u32,
21}
22
23impl Tracker {
24    /// In a new year this year's list becomes last year's; then the material's amount grows, or it
25    /// joins the list.
26    ///
27    /// Original: `FUN_140059930`.
28    pub fn add(&mut self, year: u32, material: usize, amount: i32) {
29        if year != self.year {
30            self.last_year = std::mem::take(&mut self.this_year);
31            self.year = year;
32        }
33        if let Some(e) = self.this_year.iter_mut().find(|e| e.0 == material) {
34            e.1 += amount;
35            return;
36        }
37        self.this_year.push((material, amount));
38    }
39}
40
41// +0xc; +0x10
42/// The stock manager's slot counters since the last sample (of each slot).
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct StockCounters {
45    /// The amount used, per slot.
46    pub used: [i32; 21],
47    /// The amount made, per slot.
48    pub made: [i32; 21],
49}
50
51impl Default for StockCounters {
52    fn default() -> Self {
53        StockCounters {
54            used: [0; 21],
55            made: [0; 21],
56        }
57    }
58}
59
60impl Sim<'_, '_> {
61    /// `FUN_14008e3c0(stock, owner, update, material, amount)`: the owner's TrackerComponent
62    /// (when `update`) and the material's slot count what was made.
63    pub(crate) fn count_made(
64        &mut self,
65        owner: Option<usize>,
66        update: bool,
67        material: usize,
68        n: i32,
69    ) {
70        if let (Some(o), true) = (owner, update) {
71            let year = self.year;
72            if let Some(t) = self.trackers.get_mut(&o) {
73                t.add(year, material, n);
74            }
75        }
76        if let Some(slot) = crate::work::produce::production_group(self.materials[material].flags) {
77            self.counters.made[slot] += n;
78        }
79    }
80
81    /// The `+0xc` count of the material's slot (`FUN_14008e460`): eaten, burned, used to build,
82    /// visited or traded away.
83    pub(crate) fn count_used(&mut self, material: usize, n: i32) {
84        if let Some(slot) = crate::work::produce::production_group(self.materials[material].flags) {
85            self.counters.used[slot] += n;
86        }
87    }
88}
89
90// +0x150
91/// The series the stock manager samples: 8 of the citizens, then per stock slot its count, used and
92/// made.
93pub const SERIES: usize = 8 + 3 * 21;
94/// A sample every 75 s.
95///
96/// Original: `FUN_140091780`.
97pub const SAMPLE_SECONDS: f32 = 75.0;
98
99// +0x3c; +0x44; +0x208; +0x50; +0x80; +0x210; +0x118; +0x1e4
100/// What a sample reads of each citizen (age against, school, health, happiness, education against,
101/// coat).
102#[derive(Debug, Clone, Copy, Default, PartialEq)]
103pub struct CitizenSample {
104    /// Whether the citizen is an adult.
105    pub adult: bool,
106    /// Whether it attends school.
107    pub school: bool,
108    /// The health.
109    pub health: f32,
110    /// The happiness.
111    pub happiness: f32,
112    /// Whether it is educated.
113    pub educated: bool,
114    /// Whether it has a coat.
115    pub coat: bool,
116}
117
118// +0x198; +0x150
119/// The Town Info graphs' history (StockManager) and the time to the next sample.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Statistics {
122    /// The history of each graph.
123    pub series: Vec<Vec<i32>>,
124    /// The time to the next sample.
125    pub timer: i64,
126}
127
128impl Default for Statistics {
129    fn default() -> Self {
130        Statistics {
131            series: vec![Vec::new(); SERIES],
132            timer: 0,
133        }
134    }
135}
136
137/// `cvttss2si`: out of range (and NaN) is `i32::MIN`.
138fn truncate(v: f32) -> i32 {
139    if v.is_nan() || v >= 2_147_483_648.0 || v < -2_147_483_648.0 {
140        i32::MIN
141    } else {
142        v as i32
143    }
144}
145
146impl Statistics {
147    /// The timer counts down `dt`; below 1 a sample is taken and the next is 75 s off.
148    ///
149    /// Original: `FUN_14008fc00`, `FUN_140091780` (sampling).
150    pub fn update(
151        &mut self,
152        dt: i64,
153        citizens: &[CitizenSample],
154        health_max: f32,
155        happiness_max: f32,
156        stock: &[i32; 21],
157        counters: &mut StockCounters,
158    ) -> bool {
159        self.timer -= dt;
160        if 0 < self.timer {
161            return false;
162        }
163        self.sample(citizens, health_max, happiness_max, stock, counters);
164        self.timer = crate::scheduler::ticks(SAMPLE_SECONDS);
165        true
166    }
167
168    /// The population (all, adults, students, children), the average health and happiness and the
169    /// share educated (of the adults) and with coats, in percent; each stock slot's count, used and
170    /// made; then the used and made counts restart.
171    ///
172    /// Original: `FUN_14008fc00`.
173    pub fn sample(
174        &mut self,
175        citizens: &[CitizenSample],
176        health_max: f32,
177        happiness_max: f32,
178        stock: &[i32; 21],
179        counters: &mut StockCounters,
180    ) {
181        let (mut adults, mut students, mut children) = (0, 0, 0);
182        let (mut health, mut happiness) = (0.0f32, 0.0f32);
183        let (mut educated, mut coats) = (0, 0);
184        for c in citizens {
185            if !c.adult || c.school {
186                if c.school {
187                    students += 1;
188                } else {
189                    children += 1;
190                }
191            } else {
192                adults += 1;
193            }
194            health += c.health;
195            happiness += c.happiness;
196            if c.educated && c.adult && !c.school {
197                educated += 1;
198            }
199            if c.coat {
200                coats += 1;
201            }
202        }
203        let (mut h, mut j, mut e, mut k) = (0, 0, 0, 0);
204        if !citizens.is_empty() {
205            let n = citizens.len() as f32;
206            h = truncate(((health / n) / health_max) * 100.0);
207            j = truncate(((happiness / n) / happiness_max) * 100.0);
208            e = truncate((educated as f32 / adults as f32) * 100.0);
209            k = truncate((coats as f32 / n) * 100.0);
210        }
211        let people = [
212            citizens.len() as i32,
213            adults,
214            students,
215            children,
216            h,
217            e,
218            j,
219            k,
220        ];
221        for (s, v) in self.series.iter_mut().zip(people) {
222            s.push(v);
223        }
224        for slot in 0..21 {
225            self.series[8 + 3 * slot].push(stock[slot]);
226            self.series[9 + 3 * slot].push(counters.used[slot]);
227            self.series[10 + 3 * slot].push(counters.made[slot]);
228        }
229        *counters = StockCounters::default();
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn a_tracker_keeps_this_year_and_last() {
239        let mut t = Tracker::default();
240        t.add(0, 3, 5);
241        t.add(0, 3, 2);
242        t.add(0, 4, 1);
243        assert_eq!(t.this_year, vec![(3, 7), (4, 1)]);
244        t.add(1, 4, 9);
245        assert_eq!(t.last_year, vec![(3, 7), (4, 1)]);
246        assert_eq!(t.this_year, vec![(4, 9)]);
247    }
248
249    #[test]
250    fn a_sample_counts_the_town() {
251        let adult = CitizenSample {
252            adult: true,
253            health: 2.0,
254            happiness: 1.0,
255            educated: true,
256            coat: true,
257            ..Default::default()
258        };
259        let child = CitizenSample {
260            health: 2.0,
261            happiness: 1.0,
262            ..Default::default()
263        };
264        let student = CitizenSample {
265            school: true,
266            ..child
267        };
268        let mut stats = Statistics::default();
269        let mut counters = StockCounters::default();
270        counters.made[2] = 7;
271        counters.used[2] = 3;
272        let stock = [1; 21];
273        assert!(stats.update(
274            1,
275            &[adult, child, student, adult],
276            4.0,
277            2.0,
278            &stock,
279            &mut counters
280        ));
281        let last: Vec<i32> = stats.series.iter().map(|s| *s.last().unwrap()).collect();
282        assert_eq!(&last[..8], &[4, 2, 1, 1, 50, 100, 50, 50]);
283        assert_eq!(&last[8 + 6..8 + 9], &[1, 3, 7]);
284        assert_eq!(counters, StockCounters::default());
285        // The next sample is 75 s off; with no one, the shares are 0.
286        assert!(!stats.update(
287            crate::scheduler::ticks(74.0),
288            &[],
289            4.0,
290            2.0,
291            &stock,
292            &mut counters
293        ));
294        assert!(stats.update(
295            crate::scheduler::ticks(1.0),
296            &[],
297            4.0,
298            2.0,
299            &stock,
300            &mut counters
301        ));
302        assert_eq!(stats.series[4], vec![50, 0]);
303    }
304}