Skip to main content

rgpre/import/rob/
mod.rs

1mod index;
2/// Low-level ROB binary parser.
3pub mod parser;
4pub use index::RobIndex;
5
6use crate::{
7    Result,
8    error::Error,
9    model3d::{self, Model3DFile},
10};
11use log::warn;
12
13#[derive(Debug, Clone)]
14/// Parsed ROB file header.
15pub struct RobHeader {
16    pub unknown_04: u32,
17    pub num_segments: u32,
18    pub payload_size: u32,
19}
20
21#[derive(Debug, Clone)]
22/// Axis extents and positive/negative bounds for a ROB segment.
23pub struct SegmentBBox {
24    pub extent_x: u32,
25    pub extent_y: u32,
26    pub extent_z: u32,
27    pub positive_x: u32,
28    pub positive_y: u32,
29    pub positive_z: u32,
30    pub negative_x: u32,
31    pub negative_y: u32,
32    pub negative_z: u32,
33}
34
35#[derive(Debug, Clone)]
36/// Parsed ROB segment record with metadata and optional payload bytes.
37pub struct RobSegment {
38    pub total_size: u32,
39    pub segment_name: [u8; 8],
40    pub segment_type: u16,
41    pub segment_flags: u16,
42    /// Attribute flags byte at offset 0x10. 0x02 = texture preload, 0x40 = special object.
43    pub segment_attribs: u8,
44    /// Bytes at 0x11..0x14: collision flags, reserved zero, and `face_count` mod 256.
45    #[allow(
46        clippy::pub_underscore_fields,
47        reason = "field name mirrors reverse-engineered binary layout"
48    )]
49    pub _face_count_low: [u8; 3],
50    /// Never read at runtime.
51    #[allow(
52        clippy::pub_underscore_fields,
53        reason = "field name mirrors reverse-engineered binary layout"
54    )]
55    pub _unused_14: u32,
56    pub bbox: SegmentBBox,
57    pub data_size: u32,
58    pub data: Vec<u8>,
59}
60
61#[derive(Debug, Clone)]
62/// Parsed ROB archive containing header and segment table.
63pub struct RobFile {
64    pub header: RobHeader,
65    pub segments: Vec<RobSegment>,
66}
67
68impl RobSegment {
69    /// Returns the segment name decoded from its 8-byte field.
70    #[must_use]
71    pub fn name(&self) -> String {
72        String::from_utf8_lossy(&self.segment_name)
73            .trim_matches('\0')
74            .to_string()
75    }
76
77    /// Render mode derived from the high byte of `segment_flags`.
78    /// Value 0 or >0xFD defaults to 0xFF (standard rendering).
79    #[must_use]
80    pub fn render_mode(&self) -> u8 {
81        let hi = u8::try_from(self.segment_flags >> 8).unwrap_or(0xFF);
82        if hi == 0 || hi > 0xFD { 0xFF } else { hi }
83    }
84
85    /// Returns `true` when this segment stores embedded 3D model bytes.
86    #[must_use]
87    pub const fn has_embedded_3d_data(&self) -> bool {
88        self.segment_type == 0 && self.data_size > 0
89    }
90
91    /// Returns `true` when this segment stores embedded special-object bytes.
92    #[must_use]
93    pub const fn has_special_embedded_data(&self) -> bool {
94        self.segment_type == 256 && self.data_size > 0
95    }
96
97    /// Returns `true` when this segment references an external file.
98    #[must_use]
99    pub const fn points_to_external_file(&self) -> bool {
100        self.segment_type == 512
101    }
102
103    /// Parses embedded 3D bytes in this segment into a `Model3DFile`.
104    pub fn parse_embedded_3d_data(&self) -> Result<Model3DFile> {
105        if !self.has_embedded_3d_data() {
106            return Err(Error::Parse(
107                "Segment does not contain embedded 3D data".to_string(),
108            ));
109        }
110        model3d::parse_3d_file(&self.data)
111    }
112}
113
114/// Parses a ROB file from raw bytes.
115pub fn parse_rob_file(input: &[u8]) -> Result<RobFile> {
116    match parser::parse_rob_file(input) {
117        Ok((remaining, rob_file)) => {
118            if !remaining.is_empty() {
119                warn!("{} bytes remaining unparsed", remaining.len());
120            }
121            Ok(rob_file)
122        }
123        Err(e) => Err(Error::Parse(format!("Failed to parse ROB file: {e:?}"))),
124    }
125}
126
127/// Parses a ROB file and extracts all segments with embedded 3D models.
128pub fn parse_rob_with_models(input: &[u8]) -> Result<(RobFile, Vec<Model3DFile>)> {
129    let rob_file = parse_rob_file(input)?;
130    let mut models = Vec::new();
131
132    for segment in &rob_file.segments {
133        if segment.has_embedded_3d_data() {
134            match segment.parse_embedded_3d_data() {
135                Ok(model) => models.push(model),
136                Err(e) => {
137                    warn!(
138                        "Failed to parse embedded 3D data in segment '{}': {}",
139                        segment.name(),
140                        e
141                    );
142                }
143            }
144        }
145    }
146
147    Ok((rob_file, models))
148}