use crate::Result;
use crate::read::Bytes;
use crate::structs::Structs;
const FILE: &str = "amat.bin";
const SIZE: usize = 112;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlphaMode {
#[default]
Opaque,
Mask,
Blend,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HeightMode {
#[default]
Parallax,
Displacement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TextureIds {
pub base_color: u64,
pub normal: u64,
pub orm: u64,
pub emissive: u64,
pub height: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Material {
pub base_color: [f32; 4],
pub emissive: [f32; 3],
pub emissive_strength: f32,
pub metallic: f32,
pub roughness: f32,
pub occlusion_strength: f32,
pub normal_scale: f32,
pub height_scale: f32,
pub height_midlevel: f32,
pub alpha_cutoff: f32,
pub alpha_mode: AlphaMode,
pub double_sided: bool,
pub height_mode: HeightMode,
pub textures: TextureIds,
pub structs: Structs,
}
impl Material {
pub fn decode(bytes: &[u8]) -> Result<Self> {
let b = Bytes::new(bytes, FILE);
if bytes.len() < SIZE {
return Err(b.err(format!("size {} is below {SIZE}", bytes.len())));
}
if b.slice(0, 4)? != b"ACMT" {
return Err(b.err("bad magic"));
}
let (record, rest) = bytes.split_at(SIZE);
let b = Bytes::new(record, FILE);
let structs = match b.u32(4)? {
1 if rest.is_empty() => Structs::default(),
1 => return Err(b.err(format!("version 1 is {} bytes, not {SIZE}", bytes.len()))),
2 if rest.is_empty() => Structs::default(),
2 => Structs::decode(rest).map_err(|e| e.in_file(FILE))?,
version => return Err(b.err(format!("unsupported version {version}"))),
};
let flags = b.u32(68)?;
let alpha_mode = match flags & 3 {
0 => AlphaMode::Opaque,
1 => AlphaMode::Mask,
2 => AlphaMode::Blend,
_ => return Err(b.err("bad alpha mode")),
};
Ok(Self {
base_color: b.f32s(8)?,
emissive: b.f32s(24)?,
emissive_strength: b.f32(36)?,
metallic: b.f32(40)?,
roughness: b.f32(44)?,
occlusion_strength: b.f32(48)?,
normal_scale: b.f32(52)?,
height_scale: b.f32(56)?,
height_midlevel: b.f32(60)?,
alpha_cutoff: b.f32(64)?,
alpha_mode,
double_sided: flags & 4 != 0,
height_mode: if flags & 8 != 0 {
HeightMode::Displacement
} else {
HeightMode::Parallax
},
textures: TextureIds {
base_color: b.u64(72)?,
normal: b.u64(80)?,
orm: b.u64(88)?,
emissive: b.u64(96)?,
height: b.u64(104)?,
},
structs,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::Value;
use crate::structs::Scope;
fn record(version: u32) -> Vec<u8> {
let mut out = b"ACMT".to_vec();
out.extend(version.to_le_bytes());
for v in [1.0f32, 0.5, 0.25, 1.0] {
out.extend(v.to_le_bytes());
}
out.resize(68, 0);
out.extend((1u32 | 4).to_le_bytes());
out.extend(7u64.to_le_bytes());
out.resize(SIZE, 0);
out
}
#[test]
fn reads_version_1_and_2() {
for version in [1, 2] {
let m = Material::decode(&record(version)).unwrap();
assert_eq!(m.base_color, [1.0, 0.5, 0.25, 1.0]);
assert_eq!((m.alpha_mode, m.double_sided), (AlphaMode::Mask, true));
assert_eq!(m.textures.base_color, 7);
assert!(m.structs.is_empty());
}
assert!(Material::decode(&record(3)).is_err());
assert!(Material::decode(&record(1)[..SIZE - 1]).is_err());
}
#[test]
fn reads_struct_data_after_the_record() {
let mut bytes = record(2);
bytes.extend(crate::structs::tests::astruct());
let m = Material::decode(&bytes).unwrap();
let meta = m.structs.get(Scope::Asset, 0, "Meta").unwrap();
assert_eq!(meta.get("tier").and_then(Value::as_int), Some(3));
let mut v1 = record(1);
v1.extend(crate::structs::tests::astruct());
assert!(Material::decode(&v1).is_err());
let mut junk = record(2);
junk.extend([0; 40]);
assert!(Material::decode(&junk).is_err());
}
}