Skip to main content

banished_formats/game/
work.rs

1//! Workplaces and production: quarries, herbalists, foresters, fishing, hunting, fields, orchards, pastures, livestock, recipes and professions.
2
3use super::*;
4
5// +0x20; +0x28; +0x2c
6/// `QuarryDescription`: the pit the quarry digs, a picking mesh in model space whose vertices set
7/// the terrain, no lower than a floor that goes from `shallow` (when built) to `deep` (at the end of its
8/// production).
9///
10/// Original: `FUN_140055c80`.
11#[derive(Debug, Clone, PartialEq)]
12pub struct QuarryDescription {
13    /// The floor at the end of its production.
14    pub deep: f32,
15    /// The floor when built.
16    pub shallow: f32,
17    /// The picking mesh, in model space.
18    pub mesh: Option<String>,
19}
20
21/// Read a [`QuarryDescription`] from the bytes of its resource.
22pub fn quarry_description(data: &[u8]) -> Result<QuarryDescription> {
23    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
24        if h.type_id != type_id("QuarryDescription") {
25            return format_err(format!(
26                "expected QuarryDescription, found {:#010x}",
27                h.type_id
28            ));
29        }
30        r.block(|r, _| {
31            Ok(QuarryDescription {
32                deep: r.f32()?,
33                shallow: r.f32()?,
34                mesh: r.external_ref()?,
35            })
36        })
37    })?;
38    Ok(value)
39}
40
41// +0x20; +0x28; +0x30
42/// `HerbalistDescription`: the natural-resource templates a gatherer or herbalist harvests (count)
43/// and the stock-manager production group its limit is checked against (5 food, 6 herbs).
44///
45/// Original: `FUN_14004bc40`.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct HerbalistDescription {
48    /// The natural-resource templates it harvests.
49    pub resources: Vec<Option<String>>,
50    /// The stock-manager group its limit is checked against.
51    pub group: i32,
52}
53
54/// Read a [`HerbalistDescription`] from the bytes of its resource.
55pub fn herbalist_description(data: &[u8]) -> Result<HerbalistDescription> {
56    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
57        if h.type_id != type_id("HerbalistDescription") {
58            return format_err(format!(
59                "expected HerbalistDescription, found {:#010x}",
60                h.type_id
61            ));
62        }
63        r.block(|r, _| {
64            let n = r.i32()?;
65            let resources = (0..n.max(0))
66                .map(|_| r.external_ref())
67                .collect::<Result<Vec<_>>>()?;
68            Ok(HerbalistDescription {
69                resources,
70                group: r.i32()?,
71            })
72        })
73    })?;
74    Ok(value)
75}
76
77// +0x20; +0x28
78/// `ForesterDescription`: the tree template it plants and, from version 1, the stock-manager slot
79/// of its limit (logs, 0).
80///
81/// Original: `FUN_140124f50`.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct ForesterDescription {
84    /// The tree template it plants.
85    pub tree: Option<String>,
86    /// The stock-manager slot of its limit (version 1).
87    pub group: i32,
88}
89
90/// Read a [`ForesterDescription`] from the bytes of its resource.
91pub fn forester_description(data: &[u8]) -> Result<ForesterDescription> {
92    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
93        if h.type_id != type_id("ForesterDescription") {
94            return format_err(format!(
95                "expected ForesterDescription, found {:#010x}",
96                h.type_id
97            ));
98        }
99        r.block(|r, version| {
100            let tree = r.external_ref()?;
101            let group = if version > 0 { r.i32()? } else { 0 };
102            Ok(ForesterDescription { tree, group })
103        })
104    })?;
105    Ok(value)
106}
107
108// +0x20; +0x2c; +0x28
109/// `FishingDescription`: the catch's raw-material template, the cell types counted as fishing water
110/// and, from version 1, the stock-manager slot of its limit.
111///
112/// Original: `FUN_140124640`.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct FishingDescription {
115    /// The catch's raw-material template.
116    pub output: Option<String>,
117    /// The cell types counted as fishing water.
118    pub mask: u32,
119    /// The stock-manager slot of its limit (version 1).
120    pub group: i32,
121}
122
123/// Read a [`FishingDescription`] from the bytes of its resource.
124pub fn fishing_description(data: &[u8]) -> Result<FishingDescription> {
125    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
126        if h.type_id != type_id("FishingDescription") {
127            return format_err(format!(
128                "expected FishingDescription, found {:#010x}",
129                h.type_id
130            ));
131        }
132        r.block(|r, version| {
133            let output = r.external_ref()?;
134            let mask = r.u32()?;
135            let group = if version > 0 { r.i32()? } else { 0 };
136            Ok(FishingDescription {
137                output,
138                mask,
139                group,
140            })
141        })
142    })?;
143    Ok(value)
144}
145
146// +0x20; +0x24
147/// `HuntDescription` (versioned): the hunting time a kill needs (× 300 s of a full lodge's work)
148/// and, from version 1, the stock slot of its limit.
149///
150/// Original: `FUN_140127e30`.
151#[derive(Debug, Clone, Copy, PartialEq)]
152pub struct HuntDescription {
153    /// The hunting time a kill needs, in units of 300 seconds of a full lodge's work.
154    pub time: f32,
155    /// The stock slot of its limit (version 1).
156    pub group: i32,
157}
158
159/// Read a [`HuntDescription`] from the bytes of its resource.
160pub fn hunt_description(data: &[u8]) -> Result<HuntDescription> {
161    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
162        if h.type_id != type_id("HuntDescription") {
163            return format_err(format!(
164                "expected HuntDescription, found {:#010x}",
165                h.type_id
166            ));
167        }
168        r.block(|r, version| {
169            let time = r.f32()?;
170            let group = if version > 0 { r.i32()? } else { 0 };
171            Ok(HuntDescription { time, group })
172        })
173    })?;
174    Ok(value)
175}
176
177// +0x24; +0x20
178/// `CropFieldDescription` (versioned): a tending visit's growth and, from version 1, the stock
179/// slot.
180///
181/// Original: `FUN_140120740`.
182pub fn crop_field_description(data: &[u8]) -> Result<(f32, i32)> {
183    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
184        if h.type_id != type_id("CropFieldDescription") {
185            return format_err(format!(
186                "expected CropFieldDescription, found {:#010x}",
187                h.type_id
188            ));
189        }
190        r.block(|r, version| {
191            let tend = r.f32()?;
192            let group = if version > 0 { r.i32()? } else { 0 };
193            Ok((tend, group))
194        })
195    })?;
196    Ok(value)
197}
198
199// +0x24; +0x28; +0x2c; +0x20
200/// `OrchardDescription` (versioned): tree spacing, a tending visit's ripening and, from version 1,
201/// the stock slot.
202///
203/// Original: `FUN_140052c60`.
204pub fn orchard_description(data: &[u8]) -> Result<((i32, i32), f32, i32)> {
205    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
206        if h.type_id != type_id("OrchardDescription") {
207            return format_err(format!(
208                "expected OrchardDescription, found {:#010x}",
209                h.type_id
210            ));
211        }
212        r.block(|r, version| {
213            let spacing = (r.i32()?, r.i32()?);
214            let tend = r.f32()?;
215            let group = if version > 0 { r.i32()? } else { 0 };
216            Ok((spacing, tend, group))
217        })
218    })?;
219    Ok(value)
220}
221
222// +0x20. The default limit of 5 comes from the factory FUN_140037100.
223/// A `PastureDescription`. From version 1, it has the stock slot of the limit of its products (5
224/// by default).
225///
226/// Original: `FUN_14012d6f0`.
227pub fn pasture_description(data: &[u8]) -> Result<i32> {
228    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
229        if h.type_id != type_id("PastureDescription") {
230            return format_err(format!(
231                "expected PastureDescription, found {:#010x}",
232                h.type_id
233            ));
234        }
235        r.block(|r, version| if version > 0 { r.i32() } else { Ok(5) })
236    })?;
237    Ok(value)
238}
239
240// +0x20; +0x2c; +0x28
241/// `LivestockDescription` (versioned): the raw material a pasture gathers from the animal (none for
242/// cattle), every × 300 s, and from version 1 the stock slot.
243///
244/// Original: `FUN_140129430`.
245#[derive(Debug, Clone, Default, PartialEq)]
246pub struct LivestockDescription {
247    /// The raw material a pasture gathers from the animal (none for cattle).
248    pub product: Option<String>,
249    /// The time between products, in units of 300 seconds.
250    pub interval: f32,
251    /// The stock slot (version 1).
252    pub group: i32,
253}
254
255/// Read a [`LivestockDescription`] from the bytes of its resource.
256pub fn livestock_description(data: &[u8]) -> Result<LivestockDescription> {
257    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
258        if h.type_id != type_id("LivestockDescription") {
259            return format_err(format!(
260                "expected LivestockDescription, found {:#010x}",
261                h.type_id
262            ));
263        }
264        r.block(|r, version| {
265            let product = r.external_ref()?;
266            let interval = r.f32()?;
267            let group = if version > 0 { r.i32()? } else { 0 };
268            Ok(LivestockDescription {
269                product,
270                interval,
271                group,
272            })
273        })
274    })?;
275    Ok(value)
276}
277
278// +0x20; +0x28
279/// `WorkPlaceDescription`: the profession its workers take (none for storage barns and houses) and
280/// a second reference.
281///
282/// Original: `FUN_1401348f0`.
283#[derive(Debug, Clone, Default)]
284pub struct WorkPlaceDescription {
285    /// The profession its workers take (none for storage barns and houses).
286    pub profession: Option<String>,
287    /// Not identified yet.
288    pub reference_28: Option<String>,
289}
290
291/// Read a [`WorkPlaceDescription`] from the bytes of its resource.
292pub fn workplace_description(data: &[u8]) -> Result<WorkPlaceDescription> {
293    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
294        if h.type_id != type_id("WorkPlaceDescription") {
295            return format_err(format!(
296                "expected WorkPlaceDescription, found {:#010x}",
297                h.type_id
298            ));
299        }
300        r.block(|r, _| {
301            Ok(WorkPlaceDescription {
302                profession: r.external_ref()?,
303                reference_28: r.external_ref()?,
304            })
305        })
306    })?;
307    Ok(value)
308}
309
310/// `WorkDescription` (`Template\X.rsc:work`).
311///
312/// Original: `FUN_140133d80`.
313#[derive(Debug, Clone, Default)]
314pub struct WorkDescription {
315    // +0x24
316    /// Most workers.
317    ///
318    /// Original: `FUN_140133a90`.
319    pub max_workers: i32,
320    /// The fewest workers the player may set.
321    pub value_28: i32,
322    /// The most workers the player may set.
323    pub value_2c: i32,
324    // +0x34; +0x30
325    /// Scale the most workers by the footprint's area over `area` cells.
326    pub per_area: bool,
327    /// The area, in cells, that `per_area` divides by.
328    pub area: i32,
329    /// Whether the player may change the most workers in the building's window.
330    pub flag_20: bool,
331}
332
333/// Read a [`WorkDescription`] from the bytes of its resource.
334pub fn work_description(data: &[u8]) -> Result<WorkDescription> {
335    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
336        if h.type_id != type_id("WorkDescription") {
337            return format_err(format!(
338                "expected WorkDescription, found {:#010x}",
339                h.type_id
340            ));
341        }
342        r.block(|r, _| {
343            Ok(WorkDescription {
344                max_workers: r.i32()?,
345                value_28: r.i32()?,
346                value_2c: r.i32()?,
347                per_area: r.bool()?,
348                area: r.i32()?,
349                flag_20: r.bool()?,
350            })
351        })
352    })?;
353    Ok(value)
354}
355
356/// One production recipe of a [`ConsumeProduceDescription`]: what a workplace uses and what it
357/// makes.
358///
359/// Original: the 0x28-byte record read by `FUN_140033050`.
360#[derive(Debug, Clone, Default)]
361pub struct ProduceRecipe {
362    // +0x00/+0x08
363    /// Input raw-material templates and counts.
364    pub inputs: Vec<(Option<String>, i32)>,
365    // +0x10
366    /// The output raw-material template.
367    pub output: Option<String>,
368    // +0x18; +0x170
369    /// Stock group (manager slot of the limit check).
370    pub group: i32,
371    // +0x20
372    /// The UI label key.
373    pub label: String,
374}
375
376/// `ConsumeProduceDescription` (`Template\X.rsc:consumeproduce`).
377///
378/// Original: `FUN_14011e5b0`.
379#[derive(Debug, Clone, Default)]
380pub struct ConsumeProduceDescription {
381    // +0x20
382    /// String table of the recipe labels.
383    pub string_table: Option<String>,
384    // +0x28/+0x30
385    /// The recipes.
386    pub recipes: Vec<ProduceRecipe>,
387    // +0x40
388    /// Work cycles per production.
389    pub cycles: i32,
390    // +0x50
391    /// Work type (tool mesh, PerformWork animation `type + 9` below 11, walk mode).
392    pub work_type: u32,
393    // +0x38 / +0x3c
394    /// The type of a message sent to the building's component of that type after each
395    /// production.
396    pub message_type: u32,
397    /// The value of that message.
398    pub message_value: i32,
399    // +0x44
400    /// Seconds per work cycle.
401    pub duration: f32,
402    // +0x48
403    /// Productions before the workplace is exhausted (−1: never).
404    pub lifetime: i32,
405    // +0x4c
406    /// Initial recipe selector (−1: weighted random).
407    pub selector: i32,
408}
409
410/// Read a [`ConsumeProduceDescription`] from the bytes of its resource.
411pub fn consume_produce_description(data: &[u8]) -> Result<ConsumeProduceDescription> {
412    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
413        if h.type_id != type_id("ConsumeProduceDescription") {
414            return format_err(format!(
415                "expected ConsumeProduceDescription, found {:#010x}",
416                h.type_id
417            ));
418        }
419        r.block(|r, _| {
420            let string_table = r.external_ref()?;
421            let n = r.count()?;
422            let mut recipes = Vec::with_capacity(n);
423            for _ in 0..n {
424                let k = r.count()?;
425                let inputs = (0..k)
426                    .map(|_| Ok((r.external_ref()?, r.i32()?)))
427                    .collect::<Result<_>>()?;
428                recipes.push(ProduceRecipe {
429                    inputs,
430                    output: r.external_ref()?,
431                    group: r.i32()?,
432                    label: r.string()?,
433                });
434            }
435            Ok(ConsumeProduceDescription {
436                string_table,
437                recipes,
438                cycles: r.i32()?,
439                work_type: r.u32()?,
440                message_type: r.u32()?,
441                message_value: r.i32()?,
442                duration: r.f32()?,
443                lifetime: r.i32()?,
444                selector: r.i32()?,
445            })
446        })
447    })?;
448    Ok(value)
449}
450
451/// `Profession`: a job citizens can be assigned to.
452///
453/// Original: `FUN_1400ae3e0`.
454#[derive(Debug, Clone)]
455pub struct Profession {
456    // +0x20; +0x28
457    /// The string table.
458    pub string_table: Option<String>,
459    /// The icon's sprite sheet.
460    pub sprite_sheet: Option<String>,
461    // +0x30
462    /// Name key, also the icon's sprite name (`ProfessionBuilder`).
463    pub name: String,
464    // +0x38, +0x40
465    /// The tooltip's key.
466    pub tooltip: String,
467    /// The death-message key of an accident at its work.
468    pub death: String,
469    /// The sort priority in the professions list.
470    pub value_48: i32,
471    /// Works anywhere (laborers and builders).
472    pub flag_4c: bool,
473    /// With a workplace, the citizen does not catch diseases (the doctor).
474    pub flag_4d: bool,
475    /// The accident odds' years.
476    pub value_54: f32,
477    /// The accident odds' factor.
478    pub value_58: f32,
479    /// How much a worker carries (the laborer's is what home stocking carries at least).
480    pub value_50: i32,
481    /// Whether it is in the professions list (*inferred*).
482    pub flag_4e: bool,
483}
484
485/// Read a [`Profession`] from the bytes of its resource.
486pub fn profession(data: &[u8]) -> Result<Profession> {
487    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
488        if h.type_id != type_id("Profession") {
489            return format_err(format!("expected Profession, found {:#010x}", h.type_id));
490        }
491        r.block(|r, _| {
492            Ok(Profession {
493                string_table: r.external_ref()?,
494                sprite_sheet: r.external_ref()?,
495                name: r.string()?,
496                tooltip: r.string()?,
497                death: r.string()?,
498                value_48: r.i32()?,
499                flag_4c: r.bool()?,
500                flag_4d: r.bool()?,
501                value_54: r.f32()?,
502                value_58: r.f32()?,
503                value_50: r.i32()?,
504                flag_4e: r.bool()?,
505            })
506        })
507    })?;
508    Ok(value)
509}
510
511// +0x28; +0x20
512/// RadiusDescription: radius, display reference.
513///
514/// Original: `FUN_140055e90`.
515pub fn radius_description(data: &[u8]) -> Result<(i32, Option<String>)> {
516    let (_, value) = crs::parse(data, |r: &mut Reader, h| {
517        if h.type_id != type_id("RadiusDescription") {
518            return format_err("expected RadiusDescription");
519        }
520        r.block(|r, _| Ok((r.i32()?, r.external_ref()?)))
521    })?;
522    Ok(value)
523}