Skip to main content

rakata_generics/git/
store.rs

1//! Store placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{
5    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6    GffLocalizedString, GffStruct, GffValue,
7};
8
9use super::item::SavedItem;
10use super::object_id_or_invalid;
11use crate::gff_helpers::{
12    get_f32, get_i32, get_locstring, get_resref, get_string, get_u8, upsert_field,
13};
14use crate::shared::ObjectId;
15
16/// A store instance placed in the area (struct type 0xb).
17///
18/// Stores use `ResRef` (not `TemplateResRef`) for the template reference.
19#[derive(Debug, Clone, PartialEq, Default)]
20pub struct GitStore {
21    /// Template resref (`ResRef`). Note the non-standard field name.
22    pub resref: ResRef,
23    /// X orientation (`XOrientation`).
24    pub x_orientation: f32,
25    /// Y orientation (`YOrientation`).
26    pub y_orientation: f32,
27    /// Z orientation (`ZOrientation`).
28    pub z_orientation: f32,
29    /// X position (`XPosition`).
30    pub x_position: f32,
31    /// Y position (`YPosition`).
32    pub y_position: f32,
33    /// Z position (`ZPosition`).
34    pub z_position: f32,
35    /// Runtime object ID (`ObjectId`). Save-game only.
36    pub object_id: ObjectId,
37}
38
39impl GitStore {
40    pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
41        Self {
42            resref: get_resref(s, "ResRef").unwrap_or_default(),
43            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
44            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
45            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
46            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
47            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
48            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
49            object_id: object_id_or_invalid(s),
50        }
51    }
52
53    pub(crate) fn to_gff_struct(&self) -> GffStruct {
54        let mut s = GffStruct::new(11);
55        upsert_field(&mut s, "ResRef", GffValue::ResRef(self.resref));
56        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
57        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
58        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
59        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
60        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
61        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
62        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
63        s
64    }
65}
66
67/// GIT `StoreList` entry child schema (struct type 0xb).
68pub(crate) static STORE_LIST_CHILDREN: &[FieldSchema] = &[
69    FieldSchema {
70        label: "ObjectId",
71        expected_type: GffType::UInt32,
72        life: FieldLife::Live,
73        required: false,
74        absent: AbsentDefault::Unverified,
75        children: None,
76        constraint: None,
77    },
78    FieldSchema {
79        label: "ResRef",
80        expected_type: GffType::ResRef,
81        life: FieldLife::Live,
82        required: false,
83        absent: AbsentDefault::Unverified,
84        children: None,
85        constraint: None,
86    },
87    FieldSchema {
88        label: "XOrientation",
89        expected_type: GffType::Single,
90        life: FieldLife::Live,
91        required: false,
92        absent: AbsentDefault::Unverified,
93        children: None,
94        constraint: None,
95    },
96    FieldSchema {
97        label: "YOrientation",
98        expected_type: GffType::Single,
99        life: FieldLife::Live,
100        required: false,
101        absent: AbsentDefault::Unverified,
102        children: None,
103        constraint: None,
104    },
105    FieldSchema {
106        label: "ZOrientation",
107        expected_type: GffType::Single,
108        life: FieldLife::Live,
109        required: false,
110        absent: AbsentDefault::Unverified,
111        children: None,
112        constraint: None,
113    },
114    FieldSchema {
115        label: "XPosition",
116        expected_type: GffType::Single,
117        life: FieldLife::Live,
118        required: false,
119        absent: AbsentDefault::Unverified,
120        children: None,
121        constraint: None,
122    },
123    FieldSchema {
124        label: "YPosition",
125        expected_type: GffType::Single,
126        life: FieldLife::Live,
127        required: false,
128        absent: AbsentDefault::Unverified,
129        children: None,
130        constraint: None,
131    },
132    FieldSchema {
133        label: "ZPosition",
134        expected_type: GffType::Single,
135        life: FieldLife::Live,
136        required: false,
137        absent: AbsentDefault::Unverified,
138        children: None,
139        constraint: None,
140    },
141];
142
143// =========================================================================
144// The saved form
145// =========================================================================
146
147// Merchant stores as stored inside a save game.
148//
149// A static `.git` store is a placement referencing a `.utm`. A savegame
150// store carries the whole object inline, stock included, which is what makes
151// a merchant's current inventory survive a reload after you have bought half
152// of it.
153//
154// Unlike a placeable's container, a store always writes its `ItemList`:
155// every store in the fixture saves has the field, and none of them is empty.
156// Store items also carry an `Infinite` flag that no other list writes; see
157// [`SavedItem`](crate::saved_item::SavedItem).
158//
159// ## What is not modelled
160//
161// `ActionList`, `VarTable` and `SWVarTable` are live runtime state whose
162// layouts are only partly audited, and they are skipped for the same reason
163// [`SavedCreature`](crate::saved_creature::SavedCreature) skips them.
164
165/// A merchant store as stored inside a save game's module `GIT`.
166#[derive(Debug, Clone, PartialEq, Default)]
167pub struct SavedStore {
168    /// Object tag (`Tag`).
169    pub tag: String,
170    /// Runtime object id (`ObjectId`).
171    pub object_id: ObjectId,
172    /// Displayed name (`LocName`).
173    pub loc_name: GffLocalizedString,
174
175    /// Whether the store buys, sells, or both (`BuySellFlag`).
176    pub buy_sell_flag: u8,
177    /// Percentage added when selling to the player (`MarkUp`).
178    pub mark_up: i32,
179    /// Percentage taken when buying from the player (`MarkDown`).
180    pub mark_down: i32,
181    /// Current stock (`ItemList`).
182    pub items: Vec<SavedItem>,
183
184    /// Accepts scripted commands (`Commandable`).
185    pub commandable: bool,
186    /// `OnOpenStore`.
187    pub on_open_store: ResRef,
188
189    /// World X position (`XPosition`).
190    pub x_position: f32,
191    /// World Y position (`YPosition`).
192    pub y_position: f32,
193    /// World Z position (`ZPosition`).
194    pub z_position: f32,
195    /// Facing X component (`XOrientation`).
196    pub x_orientation: f32,
197    /// Facing Y component (`YOrientation`).
198    pub y_orientation: f32,
199    /// Facing Z component (`ZOrientation`).
200    pub z_orientation: f32,
201}
202
203impl SavedStore {
204    /// Creates an empty saved store.
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Reads one saved `StoreList` element.
210    pub fn from_struct(structure: &GffStruct) -> Self {
211        Self {
212            tag: get_string(structure, "Tag").unwrap_or_default(),
213            object_id: object_id_or_invalid(structure),
214            loc_name: get_locstring(structure, "LocName")
215                .cloned()
216                .unwrap_or_default(),
217
218            buy_sell_flag: get_u8(structure, "BuySellFlag").unwrap_or(0),
219            mark_up: get_i32(structure, "MarkUp").unwrap_or(0),
220            mark_down: get_i32(structure, "MarkDown").unwrap_or(0),
221            items: match structure.field("ItemList") {
222                Some(GffValue::List(entries)) => {
223                    entries.iter().map(SavedItem::from_struct).collect()
224                }
225                _ => Vec::new(),
226            },
227
228            commandable: crate::gff_helpers::get_bool(structure, "Commandable").unwrap_or(false),
229            on_open_store: get_resref(structure, "OnOpenStore").unwrap_or_default(),
230
231            x_position: get_f32(structure, "XPosition").unwrap_or(0.0),
232            y_position: get_f32(structure, "YPosition").unwrap_or(0.0),
233            z_position: get_f32(structure, "ZPosition").unwrap_or(0.0),
234            x_orientation: get_f32(structure, "XOrientation").unwrap_or(0.0),
235            y_orientation: get_f32(structure, "YOrientation").unwrap_or(0.0),
236            z_orientation: get_f32(structure, "ZOrientation").unwrap_or(0.0),
237        }
238    }
239
240    /// Writes this store back into a `StoreList` element.
241    pub fn to_struct(&self, index: usize) -> GffStruct {
242        let mut s = GffStruct::new(i32::try_from(index).expect("store index fits i32"));
243
244        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
245        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
246        upsert_field(
247            &mut s,
248            "LocName",
249            GffValue::LocalizedString(self.loc_name.clone()),
250        );
251
252        upsert_field(&mut s, "BuySellFlag", GffValue::UInt8(self.buy_sell_flag));
253        upsert_field(&mut s, "MarkUp", GffValue::Int32(self.mark_up));
254        upsert_field(&mut s, "MarkDown", GffValue::Int32(self.mark_down));
255        upsert_field(
256            &mut s,
257            "ItemList",
258            GffValue::List(
259                self.items
260                    .iter()
261                    .enumerate()
262                    .map(|(index, item)| item.to_struct(index))
263                    .collect(),
264            ),
265        );
266
267        upsert_field(
268            &mut s,
269            "Commandable",
270            GffValue::UInt8(self.commandable.into()),
271        );
272        upsert_field(&mut s, "OnOpenStore", GffValue::ResRef(self.on_open_store));
273
274        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
275        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
276        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
277        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
278        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
279        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
280
281        s
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn round_trips_through_a_list_element() {
291        let store = SavedStore {
292            tag: "dan_merchant".to_string(),
293            object_id: ObjectId::new(0x8000_0009),
294            buy_sell_flag: 3,
295            mark_up: 100,
296            mark_down: 50,
297            items: vec![SavedItem {
298                tag: "g_w_blstrpstl001".to_string(),
299                stack_size: 1,
300                infinite: Some(true),
301                ..SavedItem::default()
302            }],
303            x_position: 3.0,
304            on_open_store: ResRef::new("k_store_open").expect("valid resref"),
305            ..SavedStore::default()
306        };
307
308        let parsed = SavedStore::from_struct(&store.to_struct(0));
309
310        assert_eq!(parsed, store);
311    }
312
313    #[test]
314    fn stock_keeps_its_infinite_flags() {
315        let store = SavedStore {
316            items: vec![SavedItem {
317                infinite: Some(true),
318                ..SavedItem::default()
319            }],
320            ..SavedStore::default()
321        };
322
323        let parsed = SavedStore::from_struct(&store.to_struct(0));
324
325        assert_eq!(parsed.items[0].infinite, Some(true));
326    }
327}