use binrw::{BinRead, BinWrite};
#[derive(Debug, Clone, Copy, Default, BinRead, BinWrite, PartialEq, Eq)]
#[brw(little)]
pub struct MclyFlags {
pub value: u32,
}
impl MclyFlags {
pub fn animation_rotation(&self) -> u8 {
(self.value & 0x007) as u8
}
pub fn animation_speed(&self) -> u8 {
((self.value & 0x038) >> 3) as u8
}
pub fn animation_enabled(&self) -> bool {
self.value & 0x040 != 0
}
pub fn overbright(&self) -> bool {
self.value & 0x080 != 0
}
pub fn use_alpha_map(&self) -> bool {
self.value & 0x100 != 0
}
pub fn alpha_map_compressed(&self) -> bool {
self.value & 0x200 != 0
}
pub fn use_cube_map_reflection(&self) -> bool {
self.value & 0x400 != 0
}
}
#[derive(Debug, Clone, Copy, Default, BinRead, BinWrite, PartialEq, Eq)]
#[brw(little)]
pub struct MclyLayer {
pub texture_id: u32,
pub flags: MclyFlags,
pub offset_in_mcal: u32,
pub effect_id: u32,
}
#[derive(Debug, Clone, Default, BinRead, BinWrite, PartialEq, Eq)]
#[brw(little)]
pub struct MclyChunk {
#[br(parse_with = binrw::helpers::until_eof)]
pub layers: Vec<MclyLayer>,
}
impl MclyChunk {
pub const MAX_LAYERS_CLASSIC: usize = 4;
pub const MAX_LAYERS_MIDNIGHT: usize = 8;
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn is_valid_classic(&self) -> bool {
self.layers.len() <= Self::MAX_LAYERS_CLASSIC
}
pub fn base_layer(&self) -> Option<&MclyLayer> {
self.layers.first()
}
pub fn blend_layers(&self) -> &[MclyLayer] {
if self.layers.len() > 1 {
&self.layers[1..]
} else {
&[]
}
}
pub fn validate_blend_flags(&self) -> bool {
self.blend_layers()
.iter()
.all(|layer| layer.flags.use_alpha_map())
}
}
#[cfg(test)]
mod tests {
use super::*;
use binrw::{BinReaderExt, BinWriterExt, io::Cursor};
#[test]
fn test_mcly_flags_animation_rotation() {
let flags = MclyFlags { value: 0x005 }; assert_eq!(flags.animation_rotation(), 5);
assert_eq!(flags.animation_speed(), 0);
}
#[test]
fn test_mcly_flags_animation_speed() {
let flags = MclyFlags { value: 0x018 }; assert_eq!(flags.animation_rotation(), 0);
assert_eq!(flags.animation_speed(), 3);
}
#[test]
fn test_mcly_flags_animation_enabled() {
let flags = MclyFlags { value: 0x040 }; assert!(flags.animation_enabled());
assert!(!flags.overbright());
assert!(!flags.use_alpha_map());
assert!(!flags.alpha_map_compressed());
assert!(!flags.use_cube_map_reflection());
}
#[test]
fn test_mcly_flags_overbright() {
let flags = MclyFlags { value: 0x080 }; assert!(!flags.animation_enabled());
assert!(flags.overbright());
assert!(!flags.use_alpha_map());
}
#[test]
fn test_mcly_flags_use_alpha_map() {
let flags = MclyFlags { value: 0x100 }; assert!(!flags.animation_enabled());
assert!(!flags.overbright());
assert!(flags.use_alpha_map());
assert!(!flags.alpha_map_compressed());
assert!(!flags.use_cube_map_reflection());
}
#[test]
fn test_mcly_flags_alpha_map_compressed() {
let flags = MclyFlags { value: 0x200 }; assert!(!flags.animation_enabled());
assert!(!flags.use_alpha_map());
assert!(flags.alpha_map_compressed());
assert!(!flags.use_cube_map_reflection());
}
#[test]
fn test_mcly_flags_use_cube_map_reflection() {
let flags = MclyFlags { value: 0x400 }; assert!(!flags.animation_enabled());
assert!(!flags.use_alpha_map());
assert!(!flags.alpha_map_compressed());
assert!(flags.use_cube_map_reflection());
}
#[test]
fn test_mcly_flags_multiple_bits() {
let flags = MclyFlags { value: 0x7C0 }; assert!(flags.animation_enabled());
assert!(flags.overbright());
assert!(flags.use_alpha_map());
assert!(flags.alpha_map_compressed());
assert!(flags.use_cube_map_reflection());
}
#[test]
fn test_mcly_flags_animation_combined() {
let flags = MclyFlags { value: 0x07F }; assert_eq!(flags.animation_rotation(), 7);
assert_eq!(flags.animation_speed(), 7);
assert!(flags.animation_enabled());
assert!(!flags.overbright());
}
#[test]
fn test_mcly_layer_parsing() {
let data: [u8; 16] = [
0x05, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, ];
let mut cursor = Cursor::new(&data[..]);
let layer: MclyLayer = cursor.read_le().expect("Failed to parse MclyLayer");
assert_eq!(layer.texture_id, 5);
assert_eq!(layer.flags.value, 0x100);
assert!(layer.flags.use_alpha_map());
assert_eq!(layer.offset_in_mcal, 2048);
assert_eq!(layer.effect_id, 10);
}
#[test]
fn test_mcly_layer_default() {
let layer = MclyLayer::default();
assert_eq!(layer.texture_id, 0);
assert_eq!(layer.flags.value, 0);
assert_eq!(layer.offset_in_mcal, 0);
assert_eq!(layer.effect_id, 0);
}
#[test]
fn test_mcly_chunk_single_layer() {
let data: [u8; 16] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert_eq!(chunk.layer_count(), 1);
assert!(chunk.is_valid_classic());
assert_eq!(chunk.base_layer().unwrap().texture_id, 1);
assert_eq!(chunk.blend_layers().len(), 0);
}
#[test]
fn test_mcly_chunk_multiple_layers() {
let mut data = Vec::new();
data.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
data.extend_from_slice(&[
0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
data.extend_from_slice(&[
0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert_eq!(chunk.layer_count(), 3);
assert!(chunk.is_valid_classic());
assert_eq!(chunk.base_layer().unwrap().texture_id, 1);
assert_eq!(chunk.blend_layers().len(), 2);
assert_eq!(chunk.blend_layers()[0].texture_id, 2);
assert_eq!(chunk.blend_layers()[1].texture_id, 3);
}
#[test]
fn test_mcly_chunk_max_layers_classic() {
let mut data = Vec::new();
for i in 0..4 {
let flags_bytes = if i == 0 {
[0x00, 0x00, 0x00, 0x00] } else {
[0x00, 0x01, 0x00, 0x00] };
data.extend_from_slice(&[
i as u8, 0x00, 0x00, 0x00, ]);
data.extend_from_slice(&flags_bytes); data.extend_from_slice(&[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
}
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert_eq!(chunk.layer_count(), 4);
assert!(chunk.is_valid_classic());
assert_eq!(chunk.blend_layers().len(), 3);
}
#[test]
fn test_mcly_chunk_exceeds_classic_limit() {
let mut data = Vec::new();
for i in 0..5 {
let flags_bytes = if i == 0 {
[0x00, 0x00, 0x00, 0x00]
} else {
[0x00, 0x01, 0x00, 0x00] };
data.extend_from_slice(&[
i as u8, 0x00, 0x00, 0x00, ]);
data.extend_from_slice(&flags_bytes); data.extend_from_slice(&[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
}
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert_eq!(chunk.layer_count(), 5);
assert!(!chunk.is_valid_classic());
}
#[test]
fn test_mcly_validate_blend_flags_valid() {
let mut data = Vec::new();
data.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]);
data.extend_from_slice(&[
0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]);
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert!(chunk.validate_blend_flags());
}
#[test]
fn test_mcly_validate_blend_flags_invalid() {
let mut data = Vec::new();
data.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]);
data.extend_from_slice(&[
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]);
let mut cursor = Cursor::new(&data[..]);
let chunk: MclyChunk = cursor.read_le().expect("Failed to parse MclyChunk");
assert!(!chunk.validate_blend_flags());
}
#[test]
fn test_mcly_layer_round_trip() {
let original = MclyLayer {
texture_id: 42,
flags: MclyFlags { value: 0x0F },
offset_in_mcal: 1024,
effect_id: 7,
};
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_le(&original)
.expect("Failed to write MclyLayer");
let mut cursor = Cursor::new(&buffer[..]);
let parsed: MclyLayer = cursor.read_le().expect("Failed to read MclyLayer");
assert_eq!(parsed, original);
}
#[test]
fn test_mcly_chunk_round_trip() {
let mut original = MclyChunk { layers: Vec::new() };
original.layers.push(MclyLayer {
texture_id: 1,
flags: MclyFlags { value: 0x00 },
offset_in_mcal: 0,
effect_id: 0,
});
for i in 2..=4 {
original.layers.push(MclyLayer {
texture_id: i,
flags: MclyFlags { value: 0x100 }, offset_in_mcal: (i - 1) * 2048,
effect_id: 0,
});
}
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_le(&original)
.expect("Failed to write MclyChunk");
let mut cursor = Cursor::new(&buffer[..]);
let parsed: MclyChunk = cursor.read_le().expect("Failed to read MclyChunk");
assert_eq!(parsed.layer_count(), original.layer_count());
assert_eq!(parsed, original);
}
#[test]
fn test_mcly_chunk_empty() {
let chunk = MclyChunk::default();
assert_eq!(chunk.layer_count(), 0);
assert!(chunk.is_valid_classic());
assert!(chunk.base_layer().is_none());
assert_eq!(chunk.blend_layers().len(), 0);
assert!(chunk.validate_blend_flags());
}
#[test]
fn test_mcly_flags_default() {
let flags = MclyFlags::default();
assert_eq!(flags.value, 0);
assert!(!flags.animation_enabled());
assert!(!flags.use_alpha_map());
assert!(!flags.alpha_map_compressed());
assert!(!flags.use_cube_map_reflection());
}
}