1mod index;
2pub 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)]
14pub struct RobHeader {
16 pub unknown_04: u32,
17 pub num_segments: u32,
18 pub payload_size: u32,
19}
20
21#[derive(Debug, Clone)]
22pub 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)]
36pub struct RobSegment {
38 pub total_size: u32,
39 pub segment_name: [u8; 8],
40 pub segment_type: u16,
41 pub segment_flags: u16,
42 pub segment_attribs: u8,
44 #[allow(
46 clippy::pub_underscore_fields,
47 reason = "field name mirrors reverse-engineered binary layout"
48 )]
49 pub _face_count_low: [u8; 3],
50 #[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)]
62pub struct RobFile {
64 pub header: RobHeader,
65 pub segments: Vec<RobSegment>,
66}
67
68impl RobSegment {
69 #[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 #[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 #[must_use]
87 pub const fn has_embedded_3d_data(&self) -> bool {
88 self.segment_type == 0 && self.data_size > 0
89 }
90
91 #[must_use]
93 pub const fn has_special_embedded_data(&self) -> bool {
94 self.segment_type == 256 && self.data_size > 0
95 }
96
97 #[must_use]
99 pub const fn points_to_external_file(&self) -> bool {
100 self.segment_type == 512
101 }
102
103 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
114pub 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
127pub 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}