Skip to main content

rakata_generics/git/
sound.rs

1//! Sound placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{
5    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6    GffStruct, GffValue,
7};
8
9use super::object_id_or_invalid;
10use crate::gff_helpers::{
11    get_bool, get_f32, get_resref, get_string, get_u32, get_u8, upsert_field,
12};
13use crate::shared::ObjectId;
14use crate::uts::UtsSound;
15
16/// A sound instance placed in the area (struct type 6).
17#[derive(Debug, Clone, PartialEq, Default)]
18pub struct GitSound {
19    /// Template resref (`TemplateResRef`).
20    pub template_resref: ResRef,
21    /// Generated type (`GeneratedType`). Values > 255 are truncated and wrapped to an 8-bit byte on save by the engine.
22    pub generated_type: u32,
23    /// X position (`XPosition`).
24    pub x_position: f32,
25    /// Y position (`YPosition`).
26    pub y_position: f32,
27    /// Z position (`ZPosition`).
28    pub z_position: f32,
29    /// Runtime object ID (`ObjectId`). Save-game only.
30    pub object_id: ObjectId,
31}
32
33impl GitSound {
34    pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
35        Self {
36            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
37            generated_type: get_u32(s, "GeneratedType").unwrap_or(0),
38            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
39            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
40            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
41            object_id: object_id_or_invalid(s),
42        }
43    }
44
45    pub(crate) fn to_gff_struct(&self) -> GffStruct {
46        let mut s = GffStruct::new(6);
47        upsert_field(
48            &mut s,
49            "TemplateResRef",
50            GffValue::ResRef(self.template_resref),
51        );
52        upsert_field(
53            &mut s,
54            "GeneratedType",
55            GffValue::UInt32(self.generated_type),
56        );
57        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
58        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
59        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
60        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
61        s
62    }
63}
64
65/// GIT `SoundList` entry child schema (struct type 6).
66pub(crate) static SOUND_LIST_CHILDREN: &[FieldSchema] = &[
67    FieldSchema {
68        label: "ObjectId",
69        expected_type: GffType::UInt32,
70        life: FieldLife::Live,
71        required: false,
72        absent: AbsentDefault::Unverified,
73        children: None,
74        constraint: None,
75    },
76    FieldSchema {
77        label: "TemplateResRef",
78        expected_type: GffType::ResRef,
79        life: FieldLife::Live,
80        required: false,
81        absent: AbsentDefault::Unverified,
82        children: None,
83        constraint: None,
84    },
85    FieldSchema {
86        label: "GeneratedType",
87        expected_type: GffType::UInt32,
88        life: FieldLife::Live,
89        required: false,
90        absent: AbsentDefault::Unverified,
91        children: None,
92        constraint: None,
93    },
94    FieldSchema {
95        label: "XPosition",
96        expected_type: GffType::Single,
97        life: FieldLife::Live,
98        required: false,
99        absent: AbsentDefault::Unverified,
100        children: None,
101        constraint: None,
102    },
103    FieldSchema {
104        label: "YPosition",
105        expected_type: GffType::Single,
106        life: FieldLife::Live,
107        required: false,
108        absent: AbsentDefault::Unverified,
109        children: None,
110        constraint: None,
111    },
112    FieldSchema {
113        label: "ZPosition",
114        expected_type: GffType::Single,
115        life: FieldLife::Live,
116        required: false,
117        absent: AbsentDefault::Unverified,
118        children: None,
119        constraint: None,
120    },
121];
122
123// =========================================================================
124// The saved form
125// =========================================================================
126
127// Sound emitters as stored inside a save game.
128//
129// A static `.git` sound is a placement referencing a `.uts`. A savegame
130// sound carries the whole emitter inline: its playlist, its timing, and
131// whether it is currently active.
132//
133// A sound has a position but no orientation, which is the one place the
134// saved object types diverge from every other list. The engine writes
135// `XPosition` / `YPosition` / `ZPosition` and nothing else.
136//
137// ## What is not modelled
138//
139// `ActionList`, `VarTable` and `SWVarTable` are live runtime state whose
140// layouts are only partly audited, and they are skipped for the same reason
141// [`SavedCreature`](crate::saved_creature::SavedCreature) skips them.
142
143/// A sound emitter as stored inside a save game's module `GIT`.
144#[derive(Debug, Clone, PartialEq, Default)]
145pub struct SavedSound {
146    /// Object tag (`Tag`).
147    pub tag: String,
148    /// Runtime object id (`ObjectId`).
149    pub object_id: ObjectId,
150    /// Accepts scripted commands (`Commandable`).
151    pub commandable: bool,
152
153    /// Currently emitting (`Active`).
154    pub active: bool,
155    /// Restarts the playlist when it ends (`Looping`).
156    pub looping: bool,
157    /// Plays without gaps between entries (`Continuous`).
158    pub continuous: bool,
159    /// Shuffles the playlist (`Random`).
160    pub random: bool,
161    /// Moves the emitter between plays (`RandomPosition`).
162    pub random_position: bool,
163    /// Attenuates with distance (`Positional`).
164    pub positional: bool,
165
166    /// Playlist entries (`Sounds`). The element shape matches a `.uts`
167    /// entry, so this reuses [`UtsSound`].
168    pub sounds: Vec<UtsSound>,
169    /// Repeat count (`Times`).
170    pub times: u8,
171    /// Hour mask the emitter is awake for (`Hours`).
172    pub hours: u32,
173    /// Gap between plays, in milliseconds (`Interval`).
174    pub interval: u32,
175    /// Random variance added to the gap (`IntervalVrtn`).
176    pub interval_variation: u32,
177    /// Playback source kind (`GeneratedType`).
178    pub generated_type: u32,
179
180    /// Volume (`Volume`).
181    pub volume: u8,
182    /// Random volume variance (`VolumeVrtn`).
183    pub volume_variation: u8,
184    /// Random pitch variance (`PitchVariation`).
185    pub pitch_variation: f32,
186    /// Fixed pitch variance (`FixedVariance`).
187    pub fixed_variance: f32,
188    /// Distance at which attenuation starts (`MinDistance`).
189    pub min_distance: f32,
190    /// Distance at which the sound is inaudible (`MaxDistance`).
191    pub max_distance: f32,
192    /// Random reposition range along X (`RandomRangeX`).
193    pub random_range_x: f32,
194    /// Random reposition range along Y (`RandomRangeY`).
195    pub random_range_y: f32,
196
197    /// World X position (`XPosition`).
198    pub x_position: f32,
199    /// World Y position (`YPosition`).
200    pub y_position: f32,
201    /// World Z position (`ZPosition`).
202    pub z_position: f32,
203}
204
205impl SavedSound {
206    /// Creates an empty saved sound.
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Reads one saved `SoundList` element.
212    pub fn from_struct(structure: &GffStruct) -> Self {
213        Self {
214            tag: get_string(structure, "Tag").unwrap_or_default(),
215            object_id: object_id_or_invalid(structure),
216            commandable: get_bool(structure, "Commandable").unwrap_or(false),
217
218            active: get_bool(structure, "Active").unwrap_or(false),
219            looping: get_bool(structure, "Looping").unwrap_or(false),
220            continuous: get_bool(structure, "Continuous").unwrap_or(false),
221            random: get_bool(structure, "Random").unwrap_or(false),
222            random_position: get_bool(structure, "RandomPosition").unwrap_or(false),
223            positional: get_bool(structure, "Positional").unwrap_or(false),
224
225            sounds: match structure.field("Sounds") {
226                Some(GffValue::List(entries)) => {
227                    entries.iter().map(UtsSound::from_struct).collect()
228                }
229                _ => Vec::new(),
230            },
231            times: get_u8(structure, "Times").unwrap_or(0),
232            hours: get_u32(structure, "Hours").unwrap_or(0),
233            interval: get_u32(structure, "Interval").unwrap_or(0),
234            interval_variation: get_u32(structure, "IntervalVrtn").unwrap_or(0),
235            generated_type: get_u32(structure, "GeneratedType").unwrap_or(0),
236
237            volume: get_u8(structure, "Volume").unwrap_or(0),
238            volume_variation: get_u8(structure, "VolumeVrtn").unwrap_or(0),
239            pitch_variation: get_f32(structure, "PitchVariation").unwrap_or(0.0),
240            fixed_variance: get_f32(structure, "FixedVariance").unwrap_or(0.0),
241            min_distance: get_f32(structure, "MinDistance").unwrap_or(0.0),
242            max_distance: get_f32(structure, "MaxDistance").unwrap_or(0.0),
243            random_range_x: get_f32(structure, "RandomRangeX").unwrap_or(0.0),
244            random_range_y: get_f32(structure, "RandomRangeY").unwrap_or(0.0),
245
246            x_position: get_f32(structure, "XPosition").unwrap_or(0.0),
247            y_position: get_f32(structure, "YPosition").unwrap_or(0.0),
248            z_position: get_f32(structure, "ZPosition").unwrap_or(0.0),
249        }
250    }
251
252    /// Writes this sound back into a `SoundList` element.
253    pub fn to_struct(&self, index: usize) -> GffStruct {
254        let mut s = GffStruct::new(i32::try_from(index).expect("sound index fits i32"));
255
256        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
257        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
258        upsert_field(
259            &mut s,
260            "Commandable",
261            GffValue::UInt8(self.commandable.into()),
262        );
263
264        upsert_field(&mut s, "Active", GffValue::UInt8(self.active.into()));
265        upsert_field(&mut s, "Looping", GffValue::UInt8(self.looping.into()));
266        upsert_field(
267            &mut s,
268            "Continuous",
269            GffValue::UInt8(self.continuous.into()),
270        );
271        upsert_field(&mut s, "Random", GffValue::UInt8(self.random.into()));
272        upsert_field(
273            &mut s,
274            "RandomPosition",
275            GffValue::UInt8(self.random_position.into()),
276        );
277        upsert_field(
278            &mut s,
279            "Positional",
280            GffValue::UInt8(self.positional.into()),
281        );
282
283        upsert_field(
284            &mut s,
285            "Sounds",
286            GffValue::List(
287                self.sounds
288                    .iter()
289                    .enumerate()
290                    .map(|(index, sound)| sound.to_struct(index))
291                    .collect(),
292            ),
293        );
294        upsert_field(&mut s, "Times", GffValue::UInt8(self.times));
295        upsert_field(&mut s, "Hours", GffValue::UInt32(self.hours));
296        upsert_field(&mut s, "Interval", GffValue::UInt32(self.interval));
297        upsert_field(
298            &mut s,
299            "IntervalVrtn",
300            GffValue::UInt32(self.interval_variation),
301        );
302        upsert_field(
303            &mut s,
304            "GeneratedType",
305            GffValue::UInt32(self.generated_type),
306        );
307
308        upsert_field(&mut s, "Volume", GffValue::UInt8(self.volume));
309        upsert_field(&mut s, "VolumeVrtn", GffValue::UInt8(self.volume_variation));
310        upsert_field(
311            &mut s,
312            "PitchVariation",
313            GffValue::Single(self.pitch_variation),
314        );
315        upsert_field(
316            &mut s,
317            "FixedVariance",
318            GffValue::Single(self.fixed_variance),
319        );
320        upsert_field(&mut s, "MinDistance", GffValue::Single(self.min_distance));
321        upsert_field(&mut s, "MaxDistance", GffValue::Single(self.max_distance));
322        upsert_field(
323            &mut s,
324            "RandomRangeX",
325            GffValue::Single(self.random_range_x),
326        );
327        upsert_field(
328            &mut s,
329            "RandomRangeY",
330            GffValue::Single(self.random_range_y),
331        );
332
333        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
334        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
335        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
336
337        s
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use rakata_core::ResRef;
345
346    fn sample() -> SavedSound {
347        SavedSound {
348            tag: "amb_wind".to_string(),
349            object_id: ObjectId::new(0x8000_0004),
350            active: true,
351            looping: true,
352            positional: true,
353            sounds: vec![
354                UtsSound {
355                    sound: ResRef::new("as_wind_01").expect("valid resref"),
356                },
357                UtsSound {
358                    sound: ResRef::new("as_wind_02").expect("valid resref"),
359                },
360            ],
361            interval: 4000,
362            volume: 90,
363            min_distance: 1.0,
364            max_distance: 30.0,
365            x_position: 8.0,
366            ..SavedSound::default()
367        }
368    }
369
370    #[test]
371    fn round_trips_through_a_list_element() {
372        let sound = sample();
373
374        let parsed = SavedSound::from_struct(&sound.to_struct(0));
375
376        assert_eq!(parsed, sound);
377    }
378
379    #[test]
380    fn carries_a_position_but_no_orientation() {
381        // Sounds are the one saved list with no orientation fields at all.
382        let written = sample().to_struct(0);
383
384        assert!(written.field("XPosition").is_some());
385        assert!(written.field("XOrientation").is_none());
386        assert!(written.field("YOrientation").is_none());
387        assert!(written.field("ZOrientation").is_none());
388    }
389}