use crate::Result;
use crate::chunks::ChunkFile;
use crate::data::Value;
use crate::read::Bytes;
use crate::shader::{Shader, State};
use crate::structs::{Attached, Scope, Structs};
const FILE: &str = "acmat.bin";
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CustomMaterial {
pub shader: u64,
pub state: State,
pub params: Vec<u8>,
pub textures: Vec<u64>,
pub structs: Structs,
}
impl CustomMaterial {
pub fn decode(bytes: &[u8]) -> Result<Self> {
let file = ChunkFile::open(bytes, "ACMAT", &[1], FILE)?;
let require = |name| Ok::<_, crate::Error>(Bytes::new(file.require(name, FILE)?, FILE));
let desc = require("DESC")?;
let block_size = desc.u32(16)? as usize;
let texture_count = desc.u32(20)? as usize;
let params = require("PARAMS")?;
if params.bytes.len() != block_size {
return Err(params.err(format!(
"PARAMS is {} bytes, not the block's {block_size}",
params.bytes.len()
)));
}
let ids = require("TEXTURES")?;
if Some(ids.bytes.len()) != texture_count.checked_mul(8) {
return Err(ids.err(format!(
"TEXTURES is {} bytes, not {texture_count} IDs",
ids.bytes.len()
)));
}
let textures = ids.records(8)?.map(|r| r.u64(0)).collect::<Result<_>>()?;
file.require("STRUCTS", FILE)?;
Ok(Self {
shader: desc.u64(0)?,
state: State::read(&desc, 8, 12)?,
params: params.bytes.to_vec(),
textures,
structs: Structs::from_chunks(&file, FILE)?,
})
}
pub fn params_struct(&self) -> Option<&Attached> {
self.structs.of(Scope::Params, 0).next()
}
pub fn param(&self, name: &str) -> Option<&Value> {
self.params_struct()?.get(name)
}
pub fn texture(&self, shader: &Shader, name: &str) -> Option<u64> {
shader
.texture_index(name)
.and_then(|i| self.textures.get(i).copied())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunks::tests::file;
use crate::shader::{Blend, Cull};
fn sample(block: usize, ids: usize, structs: bool) -> Vec<u8> {
let mut desc = 0x5adeu64.to_le_bytes().to_vec();
desc.extend((1u32 | 1 << 3 | 1 << 5 | 1 << 6).to_le_bytes());
desc.extend(0.75f32.to_le_bytes());
desc.extend(16u32.to_le_bytes());
desc.extend(2u32.to_le_bytes());
desc.extend(0u64.to_le_bytes());
let params: Vec<u8> = (0..block as u8).collect();
let textures: Vec<u8> = [7u64, 0]
.iter()
.flat_map(|v| v.to_le_bytes())
.take(ids)
.collect();
let mut chunks = crate::structs::tests::chunks(3);
chunks[0].1[40 + 4..40 + 8].copy_from_slice(&0u32.to_le_bytes());
let mut all: Vec<(&str, &[u8])> = vec![
("DESC", &desc),
("PARAMS", ¶ms),
("TEXTURES", &textures),
];
if structs {
all.extend(chunks.iter().map(|(n, p)| (*n, &p[..])));
}
file("ACMAT", 1, &all)
}
#[test]
fn decodes_values_and_params() {
let m = CustomMaterial::decode(&sample(16, 16, true)).unwrap();
assert_eq!(m.shader, 0x5ade);
assert_eq!((m.state.blend, m.state.cull), (Blend::Mask, Cull::Front));
assert!(m.state.depth_write);
assert_eq!(m.state.alpha_cutoff, 0.75);
assert_eq!(m.params, (0..16).collect::<Vec<u8>>());
assert_eq!(m.textures, [7, 0]);
assert_eq!(m.params_struct().unwrap().type_name, "Step");
assert_eq!(m.param("speed"), Some(&Value::Float(2.5)));
assert!(m.structs.get(Scope::Asset, 0, "Meta").is_some());
let shader = Shader::decode(&crate::shader::tests::sample(&Default::default())).unwrap();
assert_eq!(m.texture(&shader, "color"), Some(7));
assert_eq!(m.texture(&shader, "mask"), Some(0));
assert_eq!(m.texture(&shader, "nope"), None);
}
#[test]
fn empty_struct_chunks_mean_no_params() {
let mut desc = vec![0u8; 32];
desc[8..12].copy_from_slice(&(1u32 << 5 | 1 << 6).to_le_bytes());
let chunks: Vec<(&str, &[u8])> = vec![
("DESC", &desc),
("PARAMS", &[]),
("TEXTURES", &[]),
("STRUCTS", &[]),
("SVALUES", &[]),
("SREFS", &[]),
("SSTRINGS", &[]),
];
let m = CustomMaterial::decode(&file("ACMAT", 1, &chunks)).unwrap();
assert!(m.params.is_empty() && m.textures.is_empty() && m.structs.is_empty());
assert!(m.params_struct().is_none() && m.param("speed").is_none());
}
#[test]
fn rejects_bad_sizes() {
assert!(CustomMaterial::decode(&sample(15, 16, true)).is_err());
assert!(CustomMaterial::decode(&sample(16, 8, true)).is_err());
assert!(CustomMaterial::decode(&sample(16, 16, false)).is_err());
}
}