mod decoder;
mod encoder;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::prelude::*;
use bitflags::bitflags;
use glam::{UVec4, Vec2, Vec3};
use serde::{Deserialize, Serialize};
pub use decoder::*;
pub use encoder::*;
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct M3d {
header: Header,
pub texture_descriptors: Vec<M3dTextureDescriptor>,
pub objects: Vec<Object>,
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub(crate) struct Header {
_magic: u32,
_version: u32,
_crc: u32,
_not_crc: u32,
texture_count: u16,
object_count: u16,
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct M3dTextureDescriptor {
pub path: String,
path_residual_bytes: Option<Vec<u8>>,
pub file_name: String,
file_name_residual_bytes: Option<Vec<u8>>,
}
impl M3dTextureDescriptor {
pub fn is_color_keyed(&self) -> bool {
self.file_name.to_ascii_lowercase().starts_with("_1")
}
}
bitflags! {
#[repr(transparent)]
#[derive(Clone, Copy, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(opaque), reflect(Default, Deserialize, Hash, PartialEq, Serialize))]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct ObjectFlags: u32 {
const NONE = 0;
const UNKNOWN_FLAG_1 = 1 << 0;
const USE_LOCAL_SPACE = 1 << 1;
const UNKNOWN_FLAG_3 = 1 << 2;
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct Object {
pub name: String,
pub name_remainder: Vec<u8>,
pub parent_index: i16,
pub padding: i16,
pub translation: Vec3,
pub flags: ObjectFlags,
pub unknown1: u32,
pub unknown2: u32,
pub faces: Vec<Face>,
pub vertices: Vec<Vertex>,
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct Face {
pub indices: [u16; 3],
pub texture_index: u16,
pub normal: Vec3,
pub unknown1: u32,
pub unknown2: u32,
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(opaque),
reflect(Default, Deserialize, Serialize)
)]
#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
pub struct Vertex {
pub position: Vec3,
pub normal: Vec3,
pub color: UVec4,
pub uv: Vec2,
pub index: u32,
pub unknown1: u32,
}
#[cfg(test)]
mod tests {
use std::{
ffi::{OsStr, OsString},
fs::File,
path::{Path, PathBuf},
};
use pretty_assertions::assert_eq;
use super::*;
fn roundtrip_test(original_bytes: &[u8], m: &M3d) {
let mut encoded_bytes = Vec::new();
Encoder::new(&mut encoded_bytes).encode(m).unwrap();
let original_bytes = original_bytes
.chunks(16)
.map(|chunk| {
chunk
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n");
let encoded_bytes = encoded_bytes
.chunks(16)
.map(|chunk| {
chunk
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n");
assert_eq!(original_bytes, encoded_bytes);
}
#[test]
fn test_decode_b1_01_base() {
let d: PathBuf = [
std::env::var("DARKOMEN_PATH").unwrap().as_str(),
"DARKOMEN",
"GAMEDATA",
"1PBAT",
"B1_01",
"BASE.M3D",
]
.iter()
.collect();
let original_bytes = std::fs::read(d.clone()).unwrap();
let file = File::open(d.clone()).unwrap();
let m3d = Decoder::new(file).decode().unwrap();
assert_eq!(m3d.texture_descriptors.len(), 37);
assert_eq!(m3d.objects.len(), 4);
roundtrip_test(&original_bytes, &m3d);
}
#[test]
fn test_modify_texture_filename() {
let d: PathBuf = [
std::env::var("DARKOMEN_PATH").unwrap().as_str(),
"DARKOMEN",
"GAMEDATA",
"1PBAT",
"B1_01",
"_7WATER.M3X",
]
.iter()
.collect();
let file = File::open(d).unwrap();
let mut m3d = Decoder::new(file).decode().unwrap();
let texture_desc = m3d
.texture_descriptors
.iter_mut()
.find(|desc| desc.file_name == "_2wtrivr.bmp")
.expect("Texture '_2wtrivr.bmp' not found");
texture_desc.file_name = "wtrivr.bmp".to_string();
let mut encoded_bytes = Vec::new();
Encoder::new(&mut encoded_bytes).encode(&m3d).unwrap();
let decoded_m3d = Decoder::new(std::io::Cursor::new(&encoded_bytes))
.decode()
.unwrap();
let modified_texture = decoded_m3d
.texture_descriptors
.iter()
.find(|desc| desc.file_name == "wtrivr.bmp")
.expect("Modified texture 'wtrivr.bmp' not found after re-decoding");
assert_eq!(modified_texture.file_name, "wtrivr.bmp");
assert!(
!decoded_m3d
.texture_descriptors
.iter()
.any(|desc| desc.file_name == "_2wtrivr.bmp"),
"Old texture filename '_2wtrivr.bmp' should not exist after modification"
);
}
#[test]
fn test_decode_all() {
let d: PathBuf = [std::env::var("DARKOMEN_PATH").unwrap().as_str(), "DARKOMEN"]
.iter()
.collect();
let root_output_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "decoded", "m3ds"]
.iter()
.collect();
std::fs::create_dir_all(&root_output_dir).unwrap();
fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&Path)) {
println!("Reading dir {:?}", dir.display());
let mut paths = std::fs::read_dir(dir)
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
.unwrap();
paths.sort();
for path in paths {
if path.is_dir() {
visit_dirs(&path, cb);
} else {
cb(&path);
}
}
}
visit_dirs(&d, &mut |path| {
let Some(ext) = path.extension() else {
return;
};
if !(ext.to_string_lossy().to_uppercase() == "M3D"
|| ext.to_string_lossy().to_uppercase() == "M3X")
{
return;
}
println!("Decoding {:?}", path.file_name().unwrap());
let original_bytes = std::fs::read(path).unwrap();
let file = File::open(path).unwrap();
let m3d = Decoder::new(file).decode().unwrap();
roundtrip_test(&original_bytes, &m3d);
assert_eq!(m3d.header._magic, 908342784);
assert_eq!(m3d.header._version, 1);
assert_eq!(m3d.header._crc, 0);
assert_eq!(m3d.header._not_crc, 4294967295);
let parent_dir = path
.components()
.collect::<Vec<_>>()
.iter()
.rev()
.skip(1) .take_while(|c| c.as_os_str() != "DARKOMEN")
.collect::<Vec<_>>()
.iter()
.rev()
.collect::<PathBuf>();
let output_dir = root_output_dir.join(parent_dir);
std::fs::create_dir_all(&output_dir).unwrap();
let output_path = append_ext("ron", output_dir.join(path.file_name().unwrap()));
let mut buffer = String::new();
ron::ser::to_writer_pretty(&mut buffer, &m3d, Default::default()).unwrap();
std::fs::write(output_path, buffer).unwrap();
});
}
fn append_ext(ext: impl AsRef<OsStr>, path: PathBuf) -> PathBuf {
let mut os_string: OsString = path.into();
os_string.push(".");
os_string.push(ext.as_ref());
os_string.into()
}
}