Skip to main content

banished_game/
events.rs

1//! The town event log (`Game`) and the stock warnings that post to it. See docs/EVENT-LOG.md.
2// Game +0x138
3
4use crate::work::stock::{stock_table, RawMaterial, Storage, STOCK_SLOTS};
5
6/// `DAT_140178910`: a warning waits 120 s before it can repeat.
7const WARNING_COOLDOWN: i64 = 120_000_000;
8
9/// Event types: entry `type` of `Dialog/StringTable.rsc:events`.
10pub mod kind {
11    /// A citizen starved.
12    pub const STARVED: i32 = 0;
13    /// A citizen froze.
14    pub const FROZE: i32 = 1;
15    /// A citizen died of old age.
16    pub const OLD_AGE: i32 = 2;
17    /// A child was born.
18    pub const BORN: i32 = 8;
19    /// A child became an adult.
20    pub const ADULT: i32 = 9;
21    /// A child became a student.
22    pub const STUDENT: i32 = 10;
23    /// The town is low on logs.
24    pub const LOW_LOGS: i32 = 17;
25    /// The storage for goods is nearly full.
26    pub const NEAR_CAPACITY_GOODS: i32 = 23;
27    /// The storage for raw materials is nearly full.
28    pub const NEAR_CAPACITY_RAW: i32 = 24;
29    /// A tornado touched down.
30    pub const TORNADO: i32 = 30;
31    /// A fire started.
32    pub const FIRE: i32 = 31;
33}
34
35/// One log entry (0x28 bytes: colour, formatted text, type, severity, cell, category, icon).
36/// The port keeps the format arguments; the text is formatted with the string tables when
37/// shown, from the month and year the event happened in.
38#[derive(Debug, Clone, PartialEq)]
39pub struct EventEntry {
40    /// The event type.
41    pub kind: i32,
42    /// How important the event is.
43    pub severity: i32,
44    /// ARGB from the severity.
45    ///
46    /// Original: `FUN_14009a2d0`.
47    pub color: u32,
48    /// The month (`@0`, an index into `:seasons`).
49    pub month: usize,
50    /// The year (`@1`).
51    pub year: i32,
52    /// `@2`, `@3`, `@4`.
53    pub args: [String; 3],
54    /// The entity's map-rectangle centre cell, or -1.
55    ///
56    /// Original: `FUN_14009a500`.
57    pub cell: i32,
58    /// Notification category.
59    ///
60    /// Original: `FUN_14009a5a0`.
61    pub category: i32,
62    /// Popup icon (12, 13 for types 0x1a-0x1d), -1 otherwise.
63    pub icon: i32,
64    /// The text as formatted when the event was posted, for entries loaded from a save (the
65    /// original keeps only the text); shown as is.
66    ///
67    /// Original: `FUN_14009a790`.
68    pub text: Option<String>,
69}
70
71/// The event log: its entries and which categories notify the player.
72#[derive(Debug, Clone)]
73pub struct EventLog {
74    /// The entries.
75    pub entries: Vec<EventEntry>,
76    // +0x10..+0x15
77    /// Notify flags per group (old age, deaths, disasters, storage, town, other), set by the Event
78    /// Log's checkboxes. Default as in every inspected save: old-age deaths off, the rest on.
79    pub notify: [bool; 6],
80    /// Set when an event asks the game to slow down (option `AutoSlow`, types 11, 12, 30, 31).
81    pub slow_request: bool,
82}
83
84impl Default for EventLog {
85    fn default() -> Self {
86        EventLog {
87            entries: Vec::new(),
88            notify: [false, true, true, true, true, true],
89            slow_request: false,
90        }
91    }
92}
93
94/// The colour of an event's severity, `0xAARRGGBB`.
95///
96/// Original: the colours of `FUN_14009a2d0`.
97pub fn severity_color(severity: i32) -> u32 {
98    match severity {
99        1 => 0xffff_ff00,
100        2 => 0xff80_80ff,
101        3 => 0xffff_6060,
102        4 => 0xffea_7517,
103        _ => 0xffff_ffff,
104    }
105}
106
107/// The category of an event type, which the log filters by.
108///
109/// Original: `FUN_14009a5a0`.
110pub fn category(kind: i32) -> i32 {
111    match kind {
112        0..=7 => 0,
113        0xb => 1,
114        0xc => 2,
115        0xd => 5,
116        0xe => 8,
117        0x11..=0x16 => 6,
118        0x17 | 0x18 => 7,
119        0x19 => 9,
120        0x1a | 0x1c => 10,
121        0x1b | 0x1d => 11,
122        0x1e => 4,
123        0x1f => 3,
124        _ => -1,
125    }
126}
127
128impl EventLog {
129    /// Add an event to the log.
130    ///
131    /// Original: `FUN_14009a2d0(log, type, severity, entity, @2, @3, @4)`.
132    #[allow(clippy::too_many_arguments)]
133    pub fn add(
134        &mut self,
135        kind: i32,
136        severity: i32,
137        cell: i32,
138        month: usize,
139        year: i32,
140        args: [String; 3],
141        auto_slow: bool,
142    ) {
143        let icon = match kind {
144            0x1a | 0x1c => 12,
145            0x1b | 0x1d => 13,
146            _ => -1,
147        };
148        self.entries.push(EventEntry {
149            text: None,
150            kind,
151            severity,
152            color: severity_color(severity),
153            month,
154            year,
155            args,
156            cell,
157            category: category(kind),
158            icon,
159        });
160        if kind > 10 && (kind < 0xd || (0x1e..=0x1f).contains(&kind)) && auto_slow {
161            self.slow_request = true;
162        }
163    }
164}
165
166// Game +0xf0; +0x170; +0x180
167/// The stock manager's warning state (`Game`: slots, capacity records).
168#[derive(Debug, Clone)]
169pub struct StockWarnings {
170    // +0x0; +0x4
171    /// The count of each slot.
172    ///
173    /// Original: `FUN_14008f3a0`.
174    pub count: [i32; STOCK_SLOTS],
175    /// The count of each slot at the last check.
176    pub previous: [i32; STOCK_SLOTS],
177    // +0x8; the defaults come from FUN_140095f00.
178    /// The production limit of each stock slot (the Town Hall changes them).
179    pub limits: [i32; STOCK_SLOTS],
180    // +0x18
181    /// Warning cooldown.
182    pub cooldown: [i64; STOCK_SLOTS],
183    // +0x180
184    /// records: produced goods and logs/stone/iron, `(used, capacity, cooldown)`.
185    pub capacity: [(i32, i32, i64); 2],
186}
187
188impl Default for StockWarnings {
189    fn default() -> Self {
190        let mut limits = [1000; STOCK_SLOTS];
191        limits[..12].copy_from_slice(&[200, 200, 200, 200, 50, 5000, 100, 50, 500, 200, 200, 1000]);
192        StockWarnings {
193            count: [0; STOCK_SLOTS],
194            previous: [0; STOCK_SLOTS],
195            limits,
196            cooldown: [0; STOCK_SLOTS],
197            capacity: [(0, 0, 0); 2],
198        }
199    }
200}
201
202/// `FUN_14008f3a0`'s capacity split: materials with flags `0x1c40` (logs, stone, iron) are raw.
203const RAW_FLAGS: u32 = 0x1c40;
204
205impl StockWarnings {
206    /// Shift counts to `previous`, recount, and total used/capacity by kind.
207    ///
208    /// Original: `FUN_14008f3a0`.
209    pub fn recompute(&mut self, storages: &[Storage], materials: &[RawMaterial]) {
210        self.previous = self.count;
211        self.count = stock_table(storages, materials);
212        let mut used = [0i32; 2];
213        let mut cap = [0i32; 2];
214        for s in storages {
215            let (mut goods, mut raw) = (false, false);
216            for item in &s.items {
217                let m = &materials[item.material];
218                let size = m.size * item.count;
219                if m.flags & RAW_FLAGS == 0 {
220                    used[0] += size;
221                    goods = true;
222                } else {
223                    used[1] += size;
224                    raw = true;
225                }
226            }
227            if raw {
228                cap[1] += s.capacity;
229            }
230            if goods {
231                cap[0] += s.capacity;
232            }
233        }
234        for i in 0..2 {
235            self.capacity[i].0 = used[i];
236            self.capacity[i].1 = cap[i];
237        }
238    }
239
240    /// Low reserve (types 17..22) when a slot falls below 5% of its limit, near capacity (23 goods,
241    /// 24 logs/stone/iron) above 95%, each at most every 120 s. Returns the event types to post at
242    /// severity 3.
243    ///
244    /// Original: `FUN_1400915d0`.
245    pub fn check(&mut self, dt: i64) -> Vec<i32> {
246        let mut out = Vec::new();
247        for i in 0..STOCK_SLOTS {
248            self.cooldown[i] -= dt;
249            if self.cooldown[i] >= 1 {
250                continue;
251            }
252            self.cooldown[i] = 0;
253            if i >= 6 {
254                continue;
255            }
256            let (mut count, mut previous, mut limit) =
257                (self.count[i], self.previous[i], self.limits[i]);
258            // Firewood includes coal (slot 9).
259            if i == 3 {
260                count += self.count[9];
261                previous += self.previous[9];
262                limit += self.limits[9];
263            }
264            if count < limit / 20 && limit / 20 <= previous {
265                out.push(kind::LOW_LOGS + i as i32);
266                self.cooldown[i] = WARNING_COOLDOWN;
267            }
268        }
269        for (i, (used, cap, cooldown)) in self.capacity.iter_mut().enumerate() {
270            *cooldown -= dt;
271            if *cooldown >= 1 {
272                continue;
273            }
274            *cooldown = 0;
275            if (*cap / 20) * 19 < *used {
276                out.push(kind::NEAR_CAPACITY_GOODS + i as i32);
277                *cooldown = WARNING_COOLDOWN;
278            }
279        }
280        out
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn low_reserve_fires_on_crossing_only() {
290        let mut w = StockWarnings::default();
291        w.previous[0] = 10;
292        w.count[0] = 9;
293        assert_eq!(w.check(0), vec![kind::LOW_LOGS]);
294        // Cooling down.
295        assert!(w.check(1_000_000).is_empty());
296        // No crossing: both below the threshold.
297        w.cooldown[0] = 0;
298        w.previous[0] = 5;
299        w.count[0] = 4;
300        assert!(w.check(0).is_empty());
301    }
302
303    #[test]
304    fn colours_and_categories() {
305        assert_eq!(severity_color(3), 0xffff6060);
306        assert_eq!(category(17), 6);
307        assert_eq!(category(23), 7);
308    }
309}