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}