use super::{
FaceData, FaceNormal, FaceVertex, FrameDataEntry, FrameNormalData, FrameType, FrameVertexData,
Model3DFile, Model3DHeader, ModelVersion, TextureData, VertexCoord, VertexNormal,
};
use log::trace;
use nom::{
IResult, Parser,
bytes::complete::take,
number::complete::{le_f32, le_i16, le_i32, le_u8, le_u16, le_u32},
};
pub fn parse_3d_header(input: &[u8]) -> IResult<&[u8], Model3DHeader> {
let (
input,
(
version,
num_vertices,
num_faces,
radius,
num_frames,
offset_frame_data,
total_face_vertices,
offset_section4,
section4_count,
unused_24,
offset_normal_indices,
offset_vertex_normals,
offset_vertex_coords,
offset_face_normals,
total_face_vertices_dup,
offset_face_data,
),
) = (
take(4u8),
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
le_u32,
)
.parse(input)?;
let mut version_bytes = [0_u8; 4];
version_bytes.copy_from_slice(version);
let mut header = Model3DHeader {
version: version_bytes,
num_vertices,
num_faces,
radius,
num_frames,
offset_frame_data,
total_face_vertices,
offset_section4,
section4_count,
_unused_24: unused_24,
offset_normal_indices,
offset_vertex_normals,
offset_vertex_coords,
offset_face_normals,
total_face_vertices_dup,
offset_face_data,
};
let version_str = String::from_utf8_lossy(&header.version);
if version_str.starts_with("v2.6") || version_str.starts_with("v2.7") {
let tmp = header.total_face_vertices;
header.total_face_vertices = 0;
header.offset_normal_indices = tmp;
std::mem::swap(
&mut header.offset_vertex_normals,
&mut header.offset_normal_indices,
);
}
Ok((input, header))
}
fn parse_face_texture_header<'a>(
input: &'a [u8],
version: &ModelVersion,
) -> IResult<&'a [u8], (u32, u8)> {
if matches!(version, ModelVersion::V40 | ModelVersion::V50) {
let (input, tex_hi) = le_u8(input)?;
let (input, raw) = le_u32(input)?;
trace!("v4.0/5.0 texture fields: tex_hi=0x{tex_hi:02x}, raw=0x{raw:08x}");
Ok((input, (raw, tex_hi)))
} else {
let (input, u1) = le_u8(input)?;
let (input, texture_data) = le_u16(input)?;
let raw = u32::from(texture_data);
trace!(
"v2.6/2.7 texture fields: u1=0x{u1:02x}, texture_data=0x{texture_data:04x}, raw=0x{raw:08x}"
);
Ok((input, (raw, u1)))
}
}
fn parse_face_vertices<'a>(
input: &'a [u8],
vertex_count: u8,
version: &ModelVersion,
) -> IResult<&'a [u8], Vec<FaceVertex>> {
let mut face_vertices = Vec::with_capacity(usize::from(vertex_count));
let mut acc_u = 0i16;
let mut acc_v = 0i16;
let mut input = input;
for i in 0..vertex_count {
trace!(
"Parsing face vertex {}, remaining bytes: {:02x?}",
i,
&input[..input.len().min(12)]
);
let (input2, vertex_index_raw) = le_u32(input)?;
let (input2, u) = le_i16(input2)?;
let (input2, v) = le_i16(input2)?;
let vertex_index = if matches!(
version,
ModelVersion::V26 | ModelVersion::V27 | ModelVersion::Unknown
) {
vertex_index_raw / 12
} else {
vertex_index_raw
};
trace!(
"Raw vertex_index: {vertex_index_raw} (0x{vertex_index_raw:08x}), adjusted: {vertex_index}, u: {u}, v: {v}"
);
acc_u = acc_u.wrapping_add(u);
acc_v = acc_v.wrapping_add(v);
face_vertices.push(FaceVertex {
vertex_index,
u: acc_u,
v: acc_v,
});
input = input2;
}
Ok((input, face_vertices))
}
pub fn parse_face_data<'a>(input: &'a [u8], version: &ModelVersion) -> IResult<&'a [u8], FaceData> {
trace!(
"Starting face data parsing, first 16 bytes: {:02x?}",
&input[..input.len().min(16)]
);
let (input, vertex_count) = le_u8(input)?;
trace!("Read vertex_count: {vertex_count}");
let (input, (texture_data_raw, tex_hi_val)) = parse_face_texture_header(input, version)?;
let (input, _unused_04) = le_u32(input)?;
let texture_data = parse_texture_data_value(texture_data_raw, version);
trace!("Parsed texture_data: {texture_data:?}");
let (input, face_vertices) = parse_face_vertices(input, vertex_count, version)?;
Ok((
input,
FaceData {
vertex_count,
tex_hi: tex_hi_val,
texture_data,
face_vertices,
},
))
}
#[must_use]
pub const fn parse_texture_data_value(raw: u32, version: &ModelVersion) -> TextureData {
#[allow(clippy::cast_possible_truncation)]
match version {
ModelVersion::V26 | ModelVersion::V27 | ModelVersion::Unknown => {
let texture_id = (raw >> 7) as u16;
if texture_id < 2 {
let color_index = raw as u8;
TextureData::SolidColor(color_index)
} else {
let image_id = (raw & 0x7F) as u8;
TextureData::Texture {
texture_id,
image_id,
}
}
}
ModelVersion::V40 | ModelVersion::V50 => {
if (raw >> 20) == 0x0FFF {
let color_index = (raw >> 8) as u8;
TextureData::SolidColor(color_index)
} else {
let tmp = (raw >> 8).wrapping_sub(4_000_000);
let one = (tmp / 250) % 40;
let ten = ((tmp - (one * 250)) / 1000) % 100;
let hundred = (tmp - (one * 250) - (ten * 1000)) / 4000;
let texture_id = (one + ten + hundred) as u16;
let one = (raw & 0xFF) % 10;
let ten = ((raw & 0xFF) / 40) * 10;
let image_id = (one + ten) as u8;
TextureData::Texture {
texture_id,
image_id,
}
}
}
}
}
pub fn parse_vertex_coord(input: &[u8]) -> IResult<&[u8], VertexCoord> {
let (input, [x, y, z]) = parse_fixed_vec3(input)?;
Ok((input, VertexCoord { x, y, z }))
}
pub fn parse_face_normal(input: &[u8]) -> IResult<&[u8], FaceNormal> {
let (input, [x, y, z]) = parse_fixed_vec3(input)?;
Ok((input, FaceNormal { x, y, z }))
}
fn parse_fixed_vec3(input: &[u8]) -> IResult<&[u8], [f32; 3]> {
let (input, (x_raw, y_raw, z_raw)) = (le_i32, le_i32, le_i32).parse(input)?;
let scale = 1.0 / 256.0;
#[allow(clippy::cast_precision_loss)] Ok((
input,
[
x_raw as f32 * scale,
y_raw as f32 * scale,
z_raw as f32 * scale,
],
))
}
pub fn parse_vertex_normal(input: &[u8]) -> IResult<&[u8], VertexNormal> {
let (input, (x, y, z)) = (le_f32, le_f32, le_f32).parse(input)?;
Ok((input, VertexNormal { x, y, z }))
}
fn parse_frame_data_section(input: &[u8], header: &Model3DHeader) -> Vec<FrameDataEntry> {
let mut entries = Vec::new();
let Ok(mut off) = usize::try_from(header.offset_frame_data) else {
return entries;
};
for _ in 0..header.num_frames {
if off + 16 > input.len() {
break;
}
let vertex_offset =
u32::from_le_bytes([input[off], input[off + 1], input[off + 2], input[off + 3]]);
let normal_offset = u32::from_le_bytes([
input[off + 4],
input[off + 5],
input[off + 6],
input[off + 7],
]);
let reserved = u32::from_le_bytes([
input[off + 8],
input[off + 9],
input[off + 10],
input[off + 11],
]);
let raw_type = u32::from_le_bytes([
input[off + 12],
input[off + 13],
input[off + 14],
input[off + 15],
]);
let frame_type = match raw_type {
0 => FrameType::Static3D,
2 => FrameType::AnimatedI16,
4 => FrameType::AnimatedI32,
8 => FrameType::Static3DC,
other => FrameType::Unknown(other),
};
entries.push(FrameDataEntry {
vertex_offset,
normal_offset,
reserved,
frame_type,
});
off += 16;
}
trace!(
"Parsed {} frame data entries (num_frames={})",
entries.len(),
header.num_frames
);
entries
}
fn parse_section4_data_section(input: &[u8], header: &Model3DHeader) -> Vec<u8> {
if header.offset_section4 == 0 || header.section4_count == 0 {
return Vec::new();
}
let Ok(start) = usize::try_from(header.offset_section4) else {
return Vec::new();
};
if start >= input.len() {
return Vec::new();
}
input[start..].to_vec()
}
fn parse_face_data_section(
input: &[u8],
header: &Model3DHeader,
version: &ModelVersion,
) -> Vec<FaceData> {
let mut face_data = Vec::new();
let input_len_u32 = u32::try_from(input.len()).unwrap_or(u32::MAX);
if header.offset_face_data < input_len_u32 {
let Ok(offset_face_data) = usize::try_from(header.offset_face_data) else {
return face_data;
};
let mut face_data_input = &input[offset_face_data..];
for face_index in 0..header.num_faces {
if face_data_input.len() < 20 {
trace!(
"Face {}: Not enough data ({} bytes remaining)",
face_index,
face_data_input.len()
);
break;
}
match parse_face_data(face_data_input, version) {
Ok((remaining, face)) => {
trace!(
"Face {}: vertex_count={}, tex_hi={}, texture_data={:?}",
face_index, face.vertex_count, face.tex_hi, face.texture_data,
);
face_data.push(face);
face_data_input = remaining;
}
Err(e) => {
trace!("Face {face_index}: Failed to parse face data: {e:?}");
break;
}
}
}
}
trace!("Successfully parsed {} faces", face_data.len());
face_data
}
fn parse_vertex_coords_section(
input: &[u8],
header: &Model3DHeader,
adjusted_offset_vertex_coords: u32,
) -> Vec<VertexCoord> {
let mut vertex_coords = Vec::new();
let input_len_u32 = u32::try_from(input.len()).unwrap_or(u32::MAX);
if adjusted_offset_vertex_coords < input_len_u32 {
let Ok(offset_vertex_coords) = usize::try_from(adjusted_offset_vertex_coords) else {
return vertex_coords;
};
let mut vertex_input = &input[offset_vertex_coords..];
for vertex_index in 0..header.num_vertices {
match parse_vertex_coord(vertex_input) {
Ok((remaining, vertex)) => {
if vertex_index < 5 {
trace!(
"Vertex {}: ({:.2}, {:.2}, {:.2})",
vertex_index, vertex.x, vertex.y, vertex.z
);
}
vertex_coords.push(vertex);
vertex_input = remaining;
}
Err(e) => {
trace!("Vertex {vertex_index}: Failed to parse vertex: {e:?}");
break;
}
}
}
}
trace!("Successfully parsed {} vertices", vertex_coords.len());
vertex_coords
}
fn parse_face_normals_section(input: &[u8], header: &Model3DHeader) -> Vec<FaceNormal> {
let mut face_normals = Vec::new();
let input_len_u32 = u32::try_from(input.len()).unwrap_or(u32::MAX);
if header.offset_face_normals < input_len_u32 {
let Ok(offset_face_normals) = usize::try_from(header.offset_face_normals) else {
return face_normals;
};
let mut normal_input = &input[offset_face_normals..];
for normal_index in 0..header.num_faces {
match parse_face_normal(normal_input) {
Ok((remaining, normal)) => {
if normal_index < 5 {
trace!(
"Normal {}: ({:.2}, {:.2}, {:.2})",
normal_index, normal.x, normal.y, normal.z
);
}
face_normals.push(normal);
normal_input = remaining;
}
Err(e) => {
trace!("Normal {normal_index}: Failed to parse normal: {e:?}");
break;
}
}
}
}
trace!("Successfully parsed {} face normals", face_normals.len());
face_normals
}
fn parse_normal_indices_section(input: &[u8], header: &Model3DHeader) -> Vec<u32> {
let mut indices = Vec::new();
let input_len_u32 = u32::try_from(input.len()).unwrap_or(u32::MAX);
if header.offset_normal_indices == 0 || header.offset_normal_indices >= input_len_u32 {
return indices;
}
let Ok(offset_normal_indices) = usize::try_from(header.offset_normal_indices) else {
return indices;
};
let mut idx_input = &input[offset_normal_indices..];
for i in 0..header.total_face_vertices {
match le_u32::<_, nom::error::Error<_>>(idx_input) {
Ok((remaining, offset)) => {
if i < 5 {
trace!("Normal index {i}: {offset}");
}
indices.push(offset);
idx_input = remaining;
}
Err(e) => {
trace!("Normal index {i}: Failed to parse: {e:?}");
break;
}
}
}
trace!("Successfully parsed {} normal indices", indices.len());
indices
}
fn parse_vertex_normals_section(
input: &[u8],
header: &Model3DHeader,
offset: u32,
) -> Vec<VertexNormal> {
let mut normals = Vec::new();
let input_len_u32 = u32::try_from(input.len()).unwrap_or(u32::MAX);
if offset == 0 || offset >= input_len_u32 {
return normals;
}
let Ok(normals_offset) = usize::try_from(offset) else {
return normals;
};
let mut norm_input = &input[normals_offset..];
for i in 0..header.num_vertices {
match parse_vertex_normal(norm_input) {
Ok((remaining, normal)) => {
if i < 5 {
trace!(
"Vertex normal {}: ({:.4}, {:.4}, {:.4})",
i, normal.x, normal.y, normal.z
);
}
normals.push(normal);
norm_input = remaining;
}
Err(e) => {
trace!("Vertex normal {i}: Failed to parse: {e:?}");
break;
}
}
}
trace!("Successfully parsed {} vertex normals", normals.len());
normals
}
fn vertex_normals_offset(header: &Model3DHeader) -> u32 {
if header.is_v27_or_earlier() {
0
} else {
header.offset_vertex_normals
}
}
fn vertex_coords_offset(
input: &[u8],
header: &Model3DHeader,
frame_data: &[FrameDataEntry],
) -> u32 {
let is_animated_legacy =
header.is_v27_or_earlier() && header.num_frames > 0 && !frame_data.is_empty();
if !is_animated_legacy {
return header.offset_vertex_coords;
}
let face_data_end = header
.offset_face_data
.saturating_add(frame_data[0].normal_offset);
let primary = face_data_end.saturating_add(frame_data[0].reserved);
if vertices_look_valid(input, primary, header.num_vertices) {
primary
} else {
trace!(
"v2.6/v2.7 vertex data at primary offset {} looks invalid, falling back to face_data_end {}",
primary, face_data_end
);
face_data_end
}
}
fn vertices_look_valid(input: &[u8], offset: u32, num_vertices: u32) -> bool {
const MAX_RAW: i32 = 1_090_519_040;
let check_count = num_vertices.min(8) as usize;
let Ok(mut pos) = usize::try_from(offset) else {
return false;
};
for _ in 0..check_count {
if pos + 12 > input.len() {
return false;
}
let x = i32::from_le_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]]);
let y = i32::from_le_bytes([
input[pos + 4],
input[pos + 5],
input[pos + 6],
input[pos + 7],
]);
let z = i32::from_le_bytes([
input[pos + 8],
input[pos + 9],
input[pos + 10],
input[pos + 11],
]);
if x.abs() > MAX_RAW || y.abs() > MAX_RAW || z.abs() > MAX_RAW {
return false;
}
pos += 12;
}
true
}
fn convert_normal_indices(raw_indices: Vec<u32>, adjusted_offset_normals: u32) -> Vec<u32> {
if adjusted_offset_normals > 0 {
raw_indices
.into_iter()
.map(|offset| {
if offset >= adjusted_offset_normals {
(offset - adjusted_offset_normals) / 12
} else {
u32::MAX
}
})
.collect()
} else {
raw_indices
}
}
fn parse_i32_frame_vertices(input: &[u8], offset: usize, num_verts: usize) -> Vec<VertexCoord> {
const SCALE: f32 = 1.0 / 256.0;
let mut coords = Vec::with_capacity(num_verts);
let mut pos = offset;
for _ in 0..num_verts {
if pos + 12 > input.len() {
break;
}
let x = i32::from_le_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]])
as f32
* SCALE;
let y = i32::from_le_bytes([
input[pos + 4],
input[pos + 5],
input[pos + 6],
input[pos + 7],
]) as f32
* SCALE;
let z = i32::from_le_bytes([
input[pos + 8],
input[pos + 9],
input[pos + 10],
input[pos + 11],
]) as f32
* SCALE;
coords.push(VertexCoord { x, y, z });
pos += 12;
}
coords
}
fn parse_i16_frame_vertices(input: &[u8], offset: usize, num_verts: usize) -> Vec<VertexCoord> {
let mut coords = Vec::with_capacity(num_verts);
let mut pos = offset;
for _ in 0..num_verts {
if pos + 6 > input.len() {
coords.push(VertexCoord {
x: 0.0,
y: 0.0,
z: 0.0,
});
continue;
}
let x = i16::from_le_bytes([input[pos], input[pos + 1]]) as f32;
let y = i16::from_le_bytes([input[pos + 2], input[pos + 3]]) as f32;
let z = i16::from_le_bytes([input[pos + 4], input[pos + 5]]) as f32;
coords.push(VertexCoord { x, y, z });
pos += 6;
}
coords
}
fn parse_packed_frame_normals(input: &[u8], offset: usize, num_faces: usize) -> Vec<FaceNormal> {
let mut face_normals = Vec::with_capacity(num_faces);
let mut pos = offset;
for _ in 0..num_faces {
if pos + 4 > input.len() {
break;
}
let packed =
u32::from_le_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]]);
let mut x = (packed & 0x3FF) as i32;
if x >= 512 {
x -= 1024;
}
let mut y = ((packed >> 10) & 0x3FF) as i32;
if y >= 512 {
y -= 1024;
}
let mut z = ((packed >> 20) & 0x3FF) as i32;
if z >= 512 {
z -= 1024;
}
face_normals.push(FaceNormal {
x: x as f32 / 256.0,
y: y as f32 / 256.0,
z: z as f32 / 256.0,
});
pos += 4;
}
face_normals
}
fn parse_all_frame_data(
input: &[u8],
header: &Model3DHeader,
frame_data: &[FrameDataEntry],
frames_use_i32: bool,
uncompressed_frames: bool,
) -> (Vec<FrameVertexData>, Vec<FrameNormalData>) {
let num_verts = header.num_vertices as usize;
let num_faces = header.num_faces as usize;
let mut fvd: Vec<FrameVertexData> = Vec::with_capacity(frame_data.len());
let mut fnd: Vec<FrameNormalData> = Vec::with_capacity(frame_data.len());
for (frame_index, entry) in frame_data.iter().enumerate() {
let coords = if frames_use_i32 || frame_index == 0 {
parse_i32_frame_vertices(input, entry.vertex_offset as usize, num_verts)
} else {
parse_i16_frame_vertices(input, entry.vertex_offset as usize, num_verts)
};
let face_normals = if uncompressed_frames && entry.normal_offset > 0 {
input
.get(entry.normal_offset as usize..)
.map_or_else(Vec::new, |mut bytes| {
let mut normals = Vec::with_capacity(num_faces);
for _ in 0..num_faces {
let Ok((remaining, normal)) = parse_face_normal(bytes) else {
break;
};
normals.push(normal);
bytes = remaining;
}
normals
})
} else if frame_index > 0 && entry.normal_offset > 0 {
parse_packed_frame_normals(input, entry.normal_offset as usize, num_faces)
} else {
Vec::new()
};
fvd.push(FrameVertexData { coords });
fnd.push(FrameNormalData { face_normals });
}
(fvd, fnd)
}
pub fn parse_3d_file(input: &[u8]) -> IResult<&[u8], Model3DFile> {
trace!(
"First 128 bytes of file: {:02x?}",
&input[..128.min(input.len())]
);
let (_input_after_header, header) = parse_3d_header(input)?;
let version = header.parse_version();
trace!(
"Parsing 3D file - Version: {:?}, Vertices: {}, Faces: {}",
version, header.num_vertices, header.num_faces
);
let frame_data = parse_frame_data_section(input, &header);
let _ = parse_section4_data_section(input, &header);
let face_data = parse_face_data_section(input, &header, &version);
let adjusted_offset_normals = vertex_normals_offset(&header);
let adjusted_offset_vertex_coords = vertex_coords_offset(input, &header, &frame_data);
let vertex_coords = parse_vertex_coords_section(input, &header, adjusted_offset_vertex_coords);
let face_normals = parse_face_normals_section(input, &header);
let raw_normal_indices = parse_normal_indices_section(input, &header);
let vertex_normals = parse_vertex_normals_section(input, &header, adjusted_offset_normals);
let normal_indices = convert_normal_indices(raw_normal_indices, adjusted_offset_normals);
let uncompressed_frames = matches!(
header.parse_version(),
ModelVersion::V40 | ModelVersion::V50
) && frame_data
.first()
.is_some_and(|frame| frame.frame_type == FrameType::Static3D);
let frames_use_i32 = uncompressed_frames
|| !frame_data.is_empty() && matches!(frame_data[0].frame_type, FrameType::AnimatedI32);
let (frame_vertex_data, frame_normal_data) = parse_all_frame_data(
input,
&header,
&frame_data,
frames_use_i32,
uncompressed_frames,
);
let remaining = &[];
Ok((
remaining,
Model3DFile {
header,
version,
frame_data,
face_data,
vertex_coords,
face_normals,
normal_indices,
vertex_normals,
frame_vertex_data,
frame_normal_data,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v40_later_frames_keep_full_fixed_point_positions_and_normals() {
let mut bytes = vec![0u8; 154];
bytes[..4].copy_from_slice(b"v4.0");
let words = [1u32, 1, 1, 2, 74, 0, 0, 0, 0, 0, 0, 106, 118, 0, 64];
for (index, word) in words.into_iter().enumerate() {
bytes[4 + index * 4..8 + index * 4].copy_from_slice(&word.to_le_bytes());
}
for (index, (vertex, normal)) in [(106u32, 118u32), (130, 142)].into_iter().enumerate() {
let offset = 74 + index * 16;
bytes[offset..offset + 4].copy_from_slice(&vertex.to_le_bytes());
bytes[offset + 4..offset + 8].copy_from_slice(&normal.to_le_bytes());
}
for (offset, values) in [
(106, [256i32, 512, -768]),
(118, [0, 0, 256]),
(130, [1024, -640, 128]),
(142, [0, -256, 0]),
] {
for (axis, value) in values.into_iter().enumerate() {
bytes[offset + axis * 4..offset + axis * 4 + 4]
.copy_from_slice(&value.to_le_bytes());
}
}
let model = super::super::parse_3d_file(&bytes).unwrap();
let vertex = model.frame_vertex_data[1].coords[0];
assert_eq!([vertex.x, vertex.y, vertex.z], [4.0, -2.5, 0.5]);
let normal = &model.frame_normal_data[1].face_normals[0];
assert_eq!([normal.x, normal.y, normal.z], [0.0, -1.0, 0.0]);
}
#[test]
fn v40_animated_models_keep_packed_later_normals() {
for encoding in [2u32, 4] {
let normal_offset = if encoding == 2 { 136usize } else { 142 };
let mut bytes = vec![0u8; normal_offset + 4];
bytes[..4].copy_from_slice(b"v4.0");
let words = [1u32, 1, 1, 2, 74, 0, 0, 0, 0, 0, 0, 106, 118, 0, 64];
for (index, word) in words.into_iter().enumerate() {
bytes[4 + index * 4..8 + index * 4].copy_from_slice(&word.to_le_bytes());
}
for (index, (vertex, normal)) in [(106u32, 118u32), (130, normal_offset as u32)]
.into_iter()
.enumerate()
{
let offset = 74 + index * 16;
bytes[offset..offset + 4].copy_from_slice(&vertex.to_le_bytes());
bytes[offset + 4..offset + 8].copy_from_slice(&normal.to_le_bytes());
}
bytes[86..90].copy_from_slice(&encoding.to_le_bytes());
if encoding == 2 {
for (axis, value) in [-7i16, 9, 2].into_iter().enumerate() {
bytes[130 + axis * 2..132 + axis * 2].copy_from_slice(&value.to_le_bytes());
}
} else {
for (axis, value) in [-1792i32, 2304, 512].into_iter().enumerate() {
bytes[130 + axis * 4..134 + axis * 4].copy_from_slice(&value.to_le_bytes());
}
}
bytes[normal_offset..normal_offset + 4].copy_from_slice(&(768u32 << 10).to_le_bytes());
let model = super::super::parse_3d_file(&bytes).unwrap();
let vertex = model.frame_vertex_data[1].coords[0];
assert_eq!([vertex.x, vertex.y, vertex.z], [-7.0, 9.0, 2.0]);
let normals = &model.frame_normal_data[1].face_normals;
assert_eq!(normals.len(), 1);
assert_eq!([normals[0].x, normals[0].y, normals[0].z], [0.0, -1.0, 0.0]);
}
}
fn make_v27_face_bytes(u1: u8, texture_data: u16, vertex_count: u8) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(vertex_count);
buf.push(u1);
buf.extend_from_slice(&texture_data.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
for _ in 0..vertex_count {
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
}
buf
}
#[test]
fn v27_texture_decode_uses_u16_not_combined_with_u1() {
let data = make_v27_face_bytes(0xFF, 0x0180, 3);
let (_, face) = parse_face_data(&data, &ModelVersion::V27).unwrap();
match face.texture_data {
TextureData::Texture {
texture_id,
image_id,
} => {
assert_eq!(texture_id, 3, "texture_id should be 0x0180 >> 7 = 3");
assert_eq!(image_id, 0, "image_id should be 0x0180 & 0x7F = 0");
}
TextureData::SolidColor(_) => panic!("expected Texture, got SolidColor"),
}
}
#[test]
fn v27_u1_byte_does_not_affect_texture_id() {
let data_a = make_v27_face_bytes(0x00, 0x0400, 3);
let data_b = make_v27_face_bytes(0xFF, 0x0400, 3);
let (_, face_a) = parse_face_data(&data_a, &ModelVersion::V27).unwrap();
let (_, face_b) = parse_face_data(&data_b, &ModelVersion::V27).unwrap();
match (&face_a.texture_data, &face_b.texture_data) {
(
TextureData::Texture {
texture_id: tid_a,
image_id: iid_a,
},
TextureData::Texture {
texture_id: tid_b,
image_id: iid_b,
},
) => {
assert_eq!(tid_a, tid_b, "u1 must not contaminate texture_id");
assert_eq!(iid_a, iid_b, "u1 must not contaminate image_id");
}
_ => panic!("expected Texture variants"),
}
}
#[test]
fn v27_solid_color_when_texture_id_below_2() {
let data = make_v27_face_bytes(0x00, 0x0042, 3);
let (_, face) = parse_face_data(&data, &ModelVersion::V27).unwrap();
match face.texture_data {
TextureData::SolidColor(idx) => {
assert_eq!(idx, 0x42, "color_index should be low byte of texture_data");
}
TextureData::Texture { .. } => panic!("expected SolidColor for texture_id < 2"),
}
}
}