1pub mod parser;
3pub mod script;
4pub mod script_program;
5pub mod script_step;
6
7mod locations;
8mod metadata;
9mod positioning;
10mod shared;
11
12use crate::{Result, error::Error, import::registry::Registry, model3d::Model3DFile};
13pub use locations::actor_location_offsets;
14use log::warn;
15pub use positioning::{Placement, PlacementDiagnostic, PlacementType};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Copy)]
19pub struct RgmSectionHeader {
20 pub name: [u8; 4],
21 pub data_length: u32,
22 pub record_count: Option<u32>,
23}
24
25#[derive(Debug, Clone)]
26pub struct MpsRecord {
27 pub id: u32,
28 pub model_name: [u8; 12],
29 pub pos_x: i32,
30 pub pos_y: i32,
31 pub pos_z: u32,
32 pub rotation_matrix: [i32; 9],
33 pub trailing: [u8; 2],
34}
35
36#[derive(Debug, Clone)]
37pub struct MpobRecord {
38 pub id: u32,
39 pub object_type: u8,
40 pub is_active: u8,
41 pub script_name: [u8; 9],
42 pub model_name: [u8; 9],
43 pub is_static: u8,
44 pub unknown1: i16,
45 pub pos_x: i32,
46 pub pad_x: u8,
47 pub pos_y: i32,
48 pub pad_y: u8,
49 pub pos_z: u32,
50 pub angle_x: u32,
51 pub angle_y: u32,
52 pub angle_z: u32,
53 pub texture_data_raw: i16,
54 pub texture_id: u8,
55 pub image_id: u8,
56 pub intensity: i16,
57 pub radius: i16,
58 pub model_id: i16,
59 pub world_id: i16,
60 pub red: i16,
61 pub green: i16,
62 pub blue: i16,
63}
64
65#[derive(Debug, Clone)]
66pub struct RavcRecord {
67 pub offset_x: i8,
68 pub offset_y: i8,
69 pub offset_z: i8,
70 pub vertex: u16,
71 pub radius: u32,
72}
73
74#[derive(Debug, Clone)]
75pub struct RaexRecord {
76 pub grip0: i16,
77 pub grip1: i16,
78 pub scabbard0: i16,
79 pub scabbard1: i16,
80 pub unknown_08: i16,
81 pub texture_id: i16,
82 pub v_vertex: i16,
83 pub v_size: i16,
84 pub taunt_id: i16,
85 pub unknown_12: i16,
86 pub unknown_14: i16,
87 pub unknown_16: i16,
88 pub range_min: i16,
89 pub range_ideal: i16,
90 pub range_max: i16,
91}
92
93#[derive(Debug, Clone, Copy)]
94pub struct RagrCommand {
95 pub raw: u32,
96 pub opcode: u8,
97}
98
99#[derive(Debug, Clone)]
100pub struct RagrAnimGroup {
101 pub group_index: u16,
102 pub anim_id: u16,
103 pub flag: u8,
104 pub frame_count: u16,
105 pub commands: Vec<RagrCommand>,
106}
107
108#[derive(Debug, Clone)]
109pub struct PositionedModel {
110 pub model: Model3DFile,
111 pub transform: [f32; 16],
112 pub model_name: String,
113 pub source_id: Option<String>,
114 pub render_attributes: Option<u8>,
116 pub flat: Option<FlatSprite>,
118}
119
120#[derive(Debug, Clone, Copy)]
123pub struct FlatSprite {
124 pub texture_id: u16,
125 pub image_id: u8,
126 pub attributes: Option<FlatTextureAttributes>,
127}
128
129#[derive(Debug, Clone)]
130pub struct PositionedLight {
131 pub color: [f32; 3],
132 pub position: [f32; 3],
133 pub range: f32,
134 pub name: String,
135}
136
137#[derive(Debug, Clone)]
139pub struct MpslRecord {
140 pub link: u32,
141 pub world_id: u32,
142 pub position: [i32; 3],
143 pub radius: i16,
144 pub field_16: i16,
145 pub rgb: [i16; 3],
147 pub field_1e: u8,
148 pub active: u8,
149 pub field_20: [u8; 6],
150 pub field_26: [u8; 4],
151}
152
153impl RgmFile {
154 pub fn object_lights(&self, world_id: u32) -> impl Iterator<Item = &MpobRecord> {
159 self.sections
160 .iter()
161 .filter_map(|section| match section {
162 RgmSection::MpobParsed(_, records) => Some(records.as_slice()),
163 _ => None,
164 })
165 .flatten()
166 .filter(move |object| {
167 object.object_type == 6 && i64::from(object.world_id) == i64::from(world_id)
168 })
169 }
170
171 pub fn prelight_lights(&self, world_id: u32) -> Vec<&MpslRecord> {
175 let mut cells = HashMap::new();
176 let mut register = |x: i32, z: i32| {
177 let key = ((z as u32 & 0xfe00) >> 2) + ((x as u32 & 0xffff) >> 9);
178 let next = cells.len();
179 *cells.entry(key).or_insert(next)
180 };
181 for section in &self.sections {
182 if let RgmSection::Mps(_, records) = section {
183 for record in records {
184 register(record.pos_x, record.pos_z as i32);
185 }
186 }
187 }
188 let mut lights: Vec<_> = self
189 .local_lights(world_id)
190 .enumerate()
191 .map(|(index, light)| {
192 (
193 (
194 register(light.position[0], light.position[2]),
195 std::cmp::Reverse(index),
196 ),
197 light,
198 )
199 })
200 .collect();
201 lights.sort_by_key(|(key, _)| *key);
202 lights.into_iter().map(|(_, light)| light).collect()
203 }
204
205 pub fn local_lights(&self, world_id: u32) -> impl Iterator<Item = &MpslRecord> {
207 self.sections
208 .iter()
209 .filter_map(|section| match section {
210 RgmSection::MplParsed(_, records) => Some(records.as_slice()),
211 _ => None,
212 })
213 .flatten()
214 .filter(move |light| light.active == 1 && light.world_id == world_id)
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
221pub struct FlatTextureAttributes {
222 pub archive_id: u16,
224 pub image_id: u8,
226 pub flags: u8,
228 pub alpha_byte: u8,
230}
231
232impl FlatTextureAttributes {
233 #[must_use]
235 pub fn alpha(self) -> u16 {
236 u16::from(self.alpha_byte) + 1
237 }
238}
239
240impl RgmFile {
241 #[must_use]
244 pub fn flat_texture_attributes(&self, texture: u16) -> Option<&FlatTextureAttributes> {
245 self.sections
246 .iter()
247 .filter_map(|section| match section {
248 RgmSection::FlatParsed(_, records) => Some(records.as_slice()),
249 _ => None,
250 })
251 .flatten()
252 .find(|record| {
253 record.archive_id == texture >> 7 && u16::from(record.image_id) == texture & 127
254 })
255 }
256}
257
258#[derive(Debug, Clone)]
259pub struct MprpRecord {
260 pub id: u32,
261 pub unknown0: u8,
262 pub pos_x: i32,
263 pub pad_x: u8,
264 pub pos_y: i32,
265 pub pad_y: u8,
266 pub pos_z: u32,
267 pub angle_y: i32,
268 pub rope_type: i32,
269 pub swing: i32,
270 pub speed: i32,
271 pub length: i16,
272 pub static_model: String,
273 pub rope_model: String,
274 pub unknown1: [i32; 7],
275}
276
277#[derive(Debug, Clone)]
278pub struct RgmFile {
279 pub sections: Vec<RgmSection>,
280}
281
282#[derive(Debug, Clone)]
283pub enum RgmSection {
284 Rahd(RgmSectionHeader, Vec<u8>),
285 Rafs(RgmSectionHeader, Vec<u8>),
286 Rast(RgmSectionHeader, Vec<u8>),
287 Rasb(RgmSectionHeader, Vec<u8>),
288 Rava(RgmSectionHeader, Vec<u8>),
289 Rasc(RgmSectionHeader, Vec<u8>),
290 Rahk(RgmSectionHeader, Vec<u8>),
291 Ralc(RgmSectionHeader, Vec<u8>),
292 Raex(RgmSectionHeader, Vec<u8>),
293 RaexParsed(RgmSectionHeader, Vec<RaexRecord>),
294 Raat(RgmSectionHeader, Vec<u8>),
295 Raan(RgmSectionHeader, Vec<u8>),
296 Ragr(RgmSectionHeader, Vec<u8>),
297 Ranm(RgmSectionHeader, Vec<u8>),
298 Ravc(RgmSectionHeader, Vec<u8>),
299 RavcParsed(RgmSectionHeader, Vec<RavcRecord>),
300 Mpob(RgmSectionHeader, Vec<u8>),
301 MpobParsed(RgmSectionHeader, Vec<MpobRecord>),
302 Mprp(RgmSectionHeader, Vec<u8>),
303 MprpParsed(RgmSectionHeader, Vec<MprpRecord>),
304 Mps(RgmSectionHeader, Vec<MpsRecord>),
305 Mpl(RgmSectionHeader, Vec<u8>),
306 MplParsed(RgmSectionHeader, Vec<MpslRecord>),
307 Mpf(RgmSectionHeader, Vec<u8>),
308 Mpm(RgmSectionHeader, Vec<u8>),
309 Mpsz(RgmSectionHeader, Vec<u8>),
310 Wdnm(RgmSectionHeader, Vec<u8>),
311 Flat(RgmSectionHeader, Vec<u8>),
312 FlatParsed(RgmSectionHeader, Vec<FlatTextureAttributes>),
313 End(RgmSectionHeader),
314}
315
316#[must_use]
320pub fn world_position(position: [f32; 3]) -> [f32; 3] {
321 let origin = positioning::decode_position(0, 0, 0);
322 let local = crate::gltf::model_position(position);
323 std::array::from_fn(|axis| local[axis] + origin[axis])
324}
325
326#[must_use]
327pub fn parse_ragr_actor_groups(ragr_data: &[u8], ragr_offset: usize) -> Vec<RagrAnimGroup> {
328 metadata::parse_ragr_actor_groups_impl(ragr_data, ragr_offset)
329}
330
331#[must_use]
332pub fn parse_rahd_ragr_index(rahd_data: &[u8]) -> HashMap<String, usize> {
333 metadata::parse_rahd_ragr_index_impl(rahd_data)
334}
335
336#[allow(clippy::missing_errors_doc)] pub fn parse_rgm_file(input: &[u8]) -> Result<RgmFile> {
338 match parser::parse_rgm_file(input) {
339 Ok((remaining, rgm_file)) => {
340 if !remaining.is_empty() {
341 warn!("{} bytes remaining unparsed", remaining.len());
342 }
343 Ok(rgm_file)
344 }
345 Err(e) => Err(Error::Parse(format!("Failed to parse RGM file: {e:?}"))),
346 }
347}
348
349#[allow(clippy::missing_errors_doc)] pub fn parse_rgm_with_models(
353 input: &[u8],
354 registry: &Registry,
355) -> Result<(RgmFile, Vec<PositionedModel>, Vec<PositionedLight>)> {
356 parse_rgm_with_models_for_stem(input, registry, None)
357}
358
359#[allow(clippy::missing_errors_doc)]
360pub fn parse_rgm_with_models_for_stem(
363 input: &[u8],
364 registry: &Registry,
365 preferred_rob_stem: Option<&str>,
366) -> Result<(RgmFile, Vec<PositionedModel>, Vec<PositionedLight>)> {
367 let (rgm, models, lights, _) =
368 parse_rgm_with_models_report(input, registry, preferred_rob_stem)?;
369 Ok((rgm, models, lights))
370}
371
372#[allow(clippy::missing_errors_doc, clippy::type_complexity)]
374pub fn parse_rgm_with_models_report(
375 input: &[u8],
376 registry: &Registry,
377 preferred_rob_stem: Option<&str>,
378) -> Result<(
379 RgmFile,
380 Vec<PositionedModel>,
381 Vec<PositionedLight>,
382 Vec<PlacementDiagnostic>,
383)> {
384 let rgm_file = parse_rgm_file(input)?;
385 let mut diagnostics = Vec::new();
386 let (positioned_models, positioned_lights) = positioning::extract_positioned_models(
387 input,
388 &rgm_file,
389 registry,
390 preferred_rob_stem,
391 &mut diagnostics,
392 );
393 Ok((rgm_file, positioned_models, positioned_lights, diagnostics))
394}
395
396#[must_use]
397pub fn export_rgm_metadata_json(
398 rgm: &RgmFile,
399 soup_def: Option<&crate::import::soup_def::SoupDef>,
400) -> serde_json::Value {
401 metadata::export_rgm_metadata_json_impl(rgm, soup_def)
402}
403
404#[allow(clippy::missing_errors_doc)]
405pub fn export_rgm_runtime_metadata_json(
406 input: &[u8],
407 soup_def: Option<&crate::import::soup_def::SoupDef>,
408) -> Result<serde_json::Value> {
409 let rgm_file = parse_rgm_file(input)?;
410 Ok(export_rgm_metadata_json(&rgm_file, soup_def))
411}
412
413#[must_use]
414pub fn disassemble_rgm_scripts(
415 rgm: &RgmFile,
416 soup_def: Option<&crate::import::soup_def::SoupDef>,
417) -> Vec<(String, script::ActorScript)> {
418 let rahd_data = rgm.sections.iter().find_map(|s| match s {
419 RgmSection::Rahd(_, data) => Some(data.as_slice()),
420 _ => None,
421 });
422 let rasc_data = rgm.sections.iter().find_map(|s| match s {
423 RgmSection::Rasc(_, data) => Some(data.as_slice()),
424 _ => None,
425 });
426 let rast_data = rgm.sections.iter().find_map(|s| match s {
427 RgmSection::Rast(_, data) => Some(data.as_slice()),
428 _ => None,
429 });
430 let rasb_data = rgm.sections.iter().find_map(|s| match s {
431 RgmSection::Rasb(_, data) => Some(data.as_slice()),
432 _ => None,
433 });
434 let rava_data = rgm.sections.iter().find_map(|s| match s {
435 RgmSection::Rava(_, data) => Some(data.as_slice()),
436 _ => None,
437 });
438
439 let empty: &[u8] = &[];
440 script::disassemble_actor_scripts(
441 rahd_data.unwrap_or(empty),
442 rasc_data.unwrap_or(empty),
443 rast_data.unwrap_or(empty),
444 rasb_data.unwrap_or(empty),
445 rava_data.unwrap_or(empty),
446 soup_def,
447 )
448}
449
450#[allow(clippy::missing_errors_doc)] pub fn extract_rgm_placements(input: &[u8]) -> Result<(Vec<Placement>, Vec<PositionedLight>)> {
454 let rgm_file = parse_rgm_file(input)?;
455 Ok(positioning::extract_placements(input, &rgm_file))
456}
457
458#[allow(clippy::missing_errors_doc)] pub fn dump_rgm(input: &[u8]) -> Result<String> {
460 shared::dump_rgm_impl(input)
461}
462
463#[cfg(test)]
464mod tests;