Skip to main content

rakata_generics/git/
sound.rs

1//! Sound placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{GffModel, GffStruct};
5
6use crate::shared::ObjectId;
7use crate::uts::UtsSound;
8
9/// The part of a `sound` entry that is what every sound entry carries, whichever form it takes.
10///
11/// Both arms flatten it and the list's element is it, so the labels here
12/// are one declaration reached from both forms rather than two copies.
13#[derive(Debug, Clone, PartialEq, GffModel)]
14pub struct GitSoundCommon {
15    /// Generated type (`GeneratedType`). Values > 255 are truncated and wrapped to an 8-bit byte on save by the engine.
16    #[gff(GeneratedType, unexamined)]
17    pub generated_type: u32,
18    /// Runtime object ID (`ObjectId`). Save-game only.
19    /// Runtime object id (`ObjectId`), which only a saved form carries.
20    ///
21    /// The area-level dispatcher reads it once per element ahead of the
22    /// static-versus-saved branch, so the engine reads it on both forms and
23    /// the liveness below is right. What it does not do is author it: every
24    /// element the engine saves carries one and no element the toolset shipped
25    /// does. Held as the absence so writing a static placement back does not
26    /// invent a runtime id for it.
27    #[gff(ObjectId, stamped = ObjectId::INVALID, optional = ObjectId)]
28    pub object_id: Option<ObjectId>,
29    /// X position (`XPosition`).
30    #[gff(XPosition, unexamined)]
31    pub x_position: f32,
32    /// Y position (`YPosition`).
33    #[gff(YPosition, unexamined)]
34    pub y_position: f32,
35    /// Z position (`ZPosition`).
36    #[gff(ZPosition, unexamined)]
37    pub z_position: f32,
38}
39
40/// A sound instance placed in the area (struct type 6).
41#[derive(Debug, Clone, PartialEq, GffModel)]
42#[gff_manual_element]
43pub struct GitSound {
44    /// The block both forms carry, which the list declares as its element.
45    ///
46    /// No `#[gff]`: the arm contributes its parts to the split, and the
47    /// element is this block, so declaring it here too would make one label
48    /// two declarations. The arm's own codec reads and writes it.
49    pub common: GitSoundCommon,
50    /// Template resref (`TemplateResRef`).
51    #[gff(TemplateResRef, unexamined)]
52    pub template_resref: ResRef,
53}
54
55// =========================================================================
56// The saved form
57// =========================================================================
58
59// Sound emitters as stored inside a save game.
60//
61// A static `.git` sound is a placement referencing a `.uts`. A savegame
62// sound carries the whole emitter inline: its playlist, its timing, and
63// whether it is currently active.
64//
65// A sound has a position but no orientation, which is the one place the
66// saved object types diverge from every other list. The engine writes
67// `XPosition` / `YPosition` / `ZPosition` and nothing else.
68//
69// ## What is not modelled
70//
71// `ActionList`, `VarTable` and `SWVarTable` are live runtime state whose
72// layouts are only partly audited, and they are skipped for the same reason
73// [`SavedCreature`](crate::git::creature::SavedCreature) skips them.
74
75/// A sound emitter as stored inside a save game's module `GIT`.
76#[derive(Debug, Clone, PartialEq, GffModel)]
77#[gff_manual_element]
78pub struct SavedSound {
79    /// The block both forms carry, which the list declares as its element.
80    ///
81    /// No `#[gff]`: the arm contributes its parts to the split, and the
82    /// element is this block, so declaring it here too would make one label
83    /// two declarations. The arm's own codec reads and writes it.
84    pub common: GitSoundCommon,
85    /// Currently emitting (`Active`).
86    #[gff(Active, unexamined)]
87    pub active: bool,
88    /// Accepts scripted commands (`Commandable`), written and never read back.
89    ///
90    /// One of the base-object labels `CSWSObject::LoadObjectState` reads, and
91    /// `LoadSounds` is the only dispatcher that never calls it. So a sound is
92    /// the one owner where the save writer emits this and no load path on
93    /// either branch looks for it.
94    #[gff(
95        Commandable,
96        write_only_dead = "CSWSObject::LoadObjectState is what reads this label and LoadSounds is the only area-level dispatcher that never calls it, so neither branch of a sound's load reaches the read; the save writer emits it on every saved sound regardless",
97        unexamined
98    )]
99    pub commandable: bool,
100    /// Plays without gaps between entries (`Continuous`).
101    #[gff(Continuous, unexamined)]
102    pub continuous: bool,
103    /// Fixed pitch variance (`FixedVariance`).
104    #[gff(FixedVariance, unexamined)]
105    pub fixed_variance: f32,
106    /// Hour mask the emitter is awake for (`Hours`).
107    #[gff(Hours, unexamined)]
108    pub hours: u32,
109    /// Gap between plays, in milliseconds (`Interval`).
110    #[gff(Interval, unexamined)]
111    pub interval: u32,
112    /// Random variance added to the gap (`IntervalVrtn`).
113    #[gff(IntervalVrtn, unexamined)]
114    pub interval_variation: u32,
115    /// Restarts the playlist when it ends (`Looping`).
116    #[gff(Looping, unexamined)]
117    pub looping: bool,
118    /// Distance at which the sound is inaudible (`MaxDistance`).
119    #[gff(MaxDistance, unexamined)]
120    pub max_distance: f32,
121    /// Distance at which attenuation starts (`MinDistance`).
122    #[gff(MinDistance, unexamined)]
123    pub min_distance: f32,
124    /// Random pitch variance (`PitchVariation`).
125    #[gff(PitchVariation, unexamined)]
126    pub pitch_variation: f32,
127    /// Attenuates with distance (`Positional`).
128    #[gff(Positional, unexamined)]
129    pub positional: bool,
130    /// Shuffles the playlist (`Random`).
131    #[gff(Random, unexamined)]
132    pub random: bool,
133    /// Moves the emitter between plays (`RandomPosition`).
134    #[gff(RandomPosition, unexamined)]
135    pub random_position: bool,
136    /// Random reposition range along X (`RandomRangeX`).
137    #[gff(RandomRangeX, unexamined)]
138    pub random_range_x: f32,
139    /// Random reposition range along Y (`RandomRangeY`).
140    #[gff(RandomRangeY, unexamined)]
141    pub random_range_y: f32,
142    /// Playlist entries (`Sounds`). The element shape matches a `.uts`
143    /// entry, so this reuses [`UtsSound`].
144    /// Every element is struct id `0` rather than its index.
145    #[gff(Sounds, unexamined, list = UtsSound, element_id = 0)]
146    pub sounds: Vec<UtsSound>,
147    /// Object tag (`Tag`).
148    #[gff(Tag, unexamined)]
149    pub tag: String,
150    /// Repeat count (`Times`).
151    #[gff(Times, unexamined)]
152    pub times: u8,
153    /// Volume (`Volume`).
154    #[gff(Volume, unexamined)]
155    pub volume: u8,
156    /// Random volume variance (`VolumeVrtn`).
157    #[gff(VolumeVrtn, unexamined)]
158    pub volume_variation: u8,
159}
160
161impl GitSound {
162    /// Reads one element, with the block the list declares as its element.
163    pub fn read_element(structure: &GffStruct) -> Self {
164        Self {
165            common: GitSoundCommon::read_declared(structure),
166            ..Self::read_declared(structure)
167        }
168    }
169
170    /// Writes one element, the common block included.
171    pub fn write_element(&self, structure: &mut GffStruct) {
172        self.write_declared(structure);
173        self.common.write_declared(structure);
174    }
175}
176
177impl SavedSound {
178    /// Reads one element, with the block the list declares as its element.
179    pub fn read_element(structure: &GffStruct) -> Self {
180        Self {
181            common: GitSoundCommon::read_declared(structure),
182            ..Self::read_declared(structure)
183        }
184    }
185
186    /// Writes one element, the common block included.
187    pub fn write_element(&self, structure: &mut GffStruct) {
188        self.write_declared(structure);
189        self.common.write_declared(structure);
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    /// One value written into an element struct carrying the list's own id.
198    macro_rules! written {
199        ($value:expr, $id:expr) => {{
200            let mut element = GffStruct::new($id);
201            $value.write_element(&mut element);
202            element
203        }};
204    }
205    use rakata_core::ResRef;
206
207    fn sample() -> SavedSound {
208        SavedSound {
209            tag: "amb_wind".to_string(),
210            common: GitSoundCommon {
211                x_position: 8.0,
212                object_id: Some(ObjectId::new(0x8000_0004)),
213                ..GitSoundCommon::default()
214            },
215            active: true,
216            looping: true,
217            positional: true,
218            sounds: vec![
219                UtsSound {
220                    sound: ResRef::new("as_wind_01").expect("valid resref"),
221                },
222                UtsSound {
223                    sound: ResRef::new("as_wind_02").expect("valid resref"),
224                },
225            ],
226            interval: 4000,
227            volume: 90,
228            min_distance: 1.0,
229            max_distance: 30.0,
230            ..SavedSound::default()
231        }
232    }
233
234    #[test]
235    fn round_trips_through_a_list_element() {
236        let sound = sample();
237
238        let parsed = SavedSound::read_element(&written!(sound, 0));
239
240        assert_eq!(parsed, sound);
241    }
242
243    #[test]
244    fn carries_a_position_but_no_orientation() {
245        // Sounds are the one saved list with no orientation fields at all.
246        let written = written!(sample(), 0);
247
248        assert!(written.field("XPosition").is_some());
249        assert!(written.field("XOrientation").is_none());
250        assert!(written.field("YOrientation").is_none());
251        assert!(written.field("ZOrientation").is_none());
252    }
253}