Skip to main content

rgpre/import/rgm/
mod.rs

1//! RGM file structures and parsing.
2pub 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    /// Attribute byte of the selected ROB segment; absent for standalone or generated models.
115    pub render_attributes: Option<u8>,
116    /// Original MPSF sprite source; absent for ordinary models.
117    pub flat: Option<FlatSprite>,
118}
119
120/// Original MPSF texture reference and drawing overrides.
121/// Geometry size comes from the referenced BSI, not the generated model mesh.
122#[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/// Authored local light in engine coordinates; see `docs/formats/RGM.md`.
138#[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    /// Signed encoded RGB strengths, including subtractive channels.
146    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    /// Authored runtime object lights in file order when object loading is enabled.
155    /// Kind 6 selects the signed world ID and ignores the activation byte.
156    /// Initial values precede script mutations and active-region selection.
157    /// See `docs/engine/model-lighting.md`.
158    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    /// Map lights in world-entry prelighting order with static loading enabled.
172    /// Cells are allocated by static records first, then admitted lights; each
173    /// cell contributes its lights in reverse insertion order.
174    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    /// Map lights admitted for a WORLD.INI entry when map lighting is enabled.
206    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/// Per-texture flat drawing override from the RGM FLAT section.
219/// See `docs/formats/RGM.md#flat`; duplicate keys retain file order.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
221pub struct FlatTextureAttributes {
222    /// TEXBSI archive ID, in the range 0..512.
223    pub archive_id: u16,
224    /// Image selector within the archive.
225    pub image_id: u8,
226    /// Authored flags ORed with the caller's base drawing flags.
227    pub flags: u8,
228    /// Encoded alpha; the submission input is this byte plus one.
229    pub alpha_byte: u8,
230}
231
232impl FlatTextureAttributes {
233    /// Alpha input in 1..=256, without byte wrapping.
234    #[must_use]
235    pub fn alpha(self) -> u16 {
236        u16::from(self.alpha_byte) + 1
237    }
238}
239
240impl RgmFile {
241    /// First authored override matching a packed archive/image reference.
242    /// Raw malformed FLAT sections remain in `sections` and supply no records.
243    #[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/// Converts world coordinates in engine map units to the scene coordinates used by GLB exports.
317/// Unlike model-local positions, world positions include the map Z-origin translation.
318/// Use this for PVO cell centers; vectors and extents have no origin translation.
319#[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)] // Public wrapper delegates parser errors into crate error type.
337pub 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)] // Public wrapper combines parser/model-loading errors into crate error type.
350/// Load models; authored map lights remain in `RgmFile::local_lights`.
351/// The punctual-light output is empty because local lights require engine shading.
352pub 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)]
360/// Load models with a preferred ROB archive; map lights remain in the parsed file.
361/// The punctual-light output is empty.
362pub 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/// Load the same placements as `parse_rgm_with_models_for_stem`, retaining skipped-model reasons.
373#[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)] // Public wrapper extracts placements without loading models.
451/// Extract placements without loading models. The punctual-light output is empty;
452/// use `parse_rgm_file` and `RgmFile::local_lights` for authored local lights.
453pub 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)] // Public wrapper delegates parse errors into crate error type.
459pub fn dump_rgm(input: &[u8]) -> Result<String> {
460    shared::dump_rgm_impl(input)
461}
462
463#[cfg(test)]
464mod tests;