This commit is contained in:
Robin Appelman 2022-03-06 21:17:15 +01:00
commit f33101119c
10 changed files with 635 additions and 8 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
Cargo.lock
data

View file

@ -2,7 +2,11 @@
name = "vmdl"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
exclude = ["data"]
[dependencies]
arrayvec = "0.7.2"
binrw = "0.8.0"
thiserror = "1.0.30"
static_assertions = "1.1.0"
bitflags = "1.0.4"

10
examples/parse.rs Normal file
View file

@ -0,0 +1,10 @@
fn main() -> Result<(), vmdl::MdlError> {
let mut args = std::env::args();
let _ = args.next();
let data = std::fs::read(args.next().expect("No demo file provided"))?;
let mdl = vmdl::Mdl::read(&data)?;
dbg!(mdl.bones);
Ok(())
}

58
src/data/bone.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::{Quaternion, RadianEuler, Vector};
use binrw::BinRead;
use bitflags::bitflags;
#[derive(Debug, Clone, BinRead)]
pub struct Bone {
pub sz_name_index: i32,
pub parent: i32, // parent bone
pub bone_controller: [i32; 6], // bone controller index, -1 == none
pub pos: Vector,
pub quaternion: Quaternion,
pub rot: RadianEuler,
pub pos_scale: Vector,
pub rot_scale: Vector,
pub pose_to_bone: [[f32; 3]; 4], // 3x4 matrix
pub q_alignment: Quaternion,
pub flags: BoneFlags,
pub proc_type: i32,
pub proc_index: i32, // procedural rule
pub physics_bone: i32, // index into physically simulated bone
pub surface_prop_idx: i32, // index into string table for property name
pub contents: i32, // See BSPFlags.h for the contents flags
#[allow(dead_code)]
reserved: [i32; 8], // remove as appropriate
}
bitflags! {
#[derive(BinRead)]
pub struct BoneFlags: u32 {
const BONE_PHYSICALLY_SIMULATED = 0x00000001;
const BONE_PHYSICS_PROCEDURAL = 0x00000002;
const BONE_ALWAYS_PROCEDURAL = 0x00000004;
const BONE_SCREEN_ALIGN_SPHERE = 0x00000008;
const BONE_SCREEN_ALIGN_CYLINDER = 0x00000010;
const BONE_USED_BY_HITBOX = 0x00000100;
const BONE_USED_BY_ATTACHMENT = 0x00000200;
const BONE_USED_BY_VERTEX_LOD0 = 0x00000400;
const BONE_USED_BY_VERTEX_LOD1 = 0x00000800;
const BONE_USED_BY_VERTEX_LOD2 = 0x00001000;
const BONE_USED_BY_VERTEX_LOD3 = 0x00002000;
const BONE_USED_BY_VERTEX_LOD4 = 0x00004000;
const BONE_USED_BY_VERTEX_LOD5 = 0x00008000;
const BONE_USED_BY_VERTEX_LOD6 = 0x00010000;
const BONE_USED_BY_VERTEX_LOD7 = 0x00020000;
const BONE_USED_BY_BONE_MERGE = 0x00040000;
const BONE_TYPE_MASK = 0x00F00000;
const BONE_FIXED_ALIGNMENT = 0x00100000;
const BONE_HAS_SAVEFRAME_POS = 0x00200000;
const BONE_HAS_SAVEFRAME_ROT = 0x00400000;
}
}

295
src/data/header.rs Normal file
View file

@ -0,0 +1,295 @@
use crate::{Bone, FixedString, Vector};
use binrw::BinRead;
use std::mem::size_of;
pub const FILETYPE_ID: i32 = i32::from_be_bytes(*b"IDST");
pub const MDL_VERSION: i32 = 48;
#[derive(Debug, Clone, BinRead)]
pub struct StudioHeader {
pub id: i32,
pub version: i32,
pub checksum: [u8; 4], // This has to be the same in the phy and vtx files to load!
pub name: FixedString<64>,
pub data_length: i32,
pub eye_position: Vector, // Position of player viewpoint relative to model origin
pub illumination_position: Vector, // Position (relative to model origin) used to calculate ambient light contribution and cubemap reflections for the entire model.
pub hull_min: Vector, // Corner of model hull box with the least X/Y/Z values
pub hull_max: Vector, // Opposite corner of model hull box
pub view_bb_min: Vector,
pub view_bb_max: Vector,
pub flags: i32, // Binary flags in little-endian order.
// ex (00000001,00000000,00000000,11000000) means flags for position 0, 30, and 31 are set.
// Set model flags section for more information
/*
* After this point, the header contains many references to offsets
* within the MDL file and the number of items at those offsets.
*
* Offsets are from the very beginning of the file.
*
* Note that indexes/counts are not always paired and ordered consistently.
*/
// mstudiobone_t
bone_count: i32, // Number of data sections (of type mstudiobone_t)
bone_offset: i32, // Offset of first data section
// mstudiobonecontroller_t
bone_controller_count: i32,
bone_controller_offset: i32,
// mstudiohitboxset_t
hitbox_count: i32,
hitbox_offset: i32,
// mstudioanimdesc_t
local_animation_count: i32,
local_animation_offset: i32,
// mstudioseqdesc_t
local_seq_count: i32,
local_seq_offset: i32,
pub activity_list_version: i32, // ??
pub events_indexed: i32, // ??
// VMT texture filenames
// mstudiotexture_t
texture_count: i32,
texture_offset: i32,
// This offset points to a series of ints.
// Each int value, in turn, is an offset relative to the start of this header/the-file,
// At which there is a null-terminated string.
texture_dir_count: i32,
texture_dir_offset: i32,
// Each skin-family assigns a texture-id to a skin location
pub skin_reference_count: i32,
pub skin_r_family_count: i32,
pub skin_reference_index: i32,
// mstudiobodyparts_t
body_part_count: i32,
body_part_offset: i32,
// Local attachment points
// mstudioattachment_t
attachment_count: i32,
attachment_offset: i32,
// Node values appear to be single bytes, while their names are null-terminated strings.
local_node_count: i32,
local_node_index: i32,
local_node_name_index: i32,
// mstudioflexdesc_t
flex_desc_count: i32,
flex_desc_index: i32,
// mstudioflexcontroller_t
flex_controller_count: i32,
flex_controller_index: i32,
// mstudioflexrule_t
flex_rules_count: i32,
flex_rules_index: i32,
// IK probably referse to inverse kinematics
// mstudioikchain_t
ik_chain_count: i32,
ik_chain_index: i32,
// Information about any "mouth" on the model for speech animation
// More than one sounds pretty creepy.
// mstudiomouth_t
mouths_count: i32,
mouths_index: i32,
// mstudioposeparamdesc_t
local_pose_param_count: i32,
local_pose_param_index: i32,
/*
* For anyone trying to follow along, as of this writing,
* the next "surfaceprop_index" value is at position 0x0134 (308)
* from the start of the file.
*/
// Surface property value (single null-terminated string)
pub surface_prop_index: i32,
// Unusual: In this one index comes first, then count.
// Key-value data is a series of strings. If you can't find
// what you're interested in, check the associated PHY file as well.
key_value_index: i32,
key_value_count: i32,
// More inverse-kinematics
// mstudioiklock_t
ik_lock_count: i32,
ik_lock_index: i32,
pub mass: f32, // Mass of object (4-bytes)
pub contents: i32, // ??
// Other models can be referenced for re-used sequences and animations
// (See also: The $includemodel QC option.)
// mstudiomodelgroup_t
include_model_count: i32,
include_model_index: i32,
pub virtual_model: i32, // Placeholder for mutable-void*
// Note that the SDK only compiles as 32-bit, so an int and a pointer are the same size (4 bytes)
// mstudioanimblock_t
anim_blocks_name_index: i32,
anim_blocks_count: i32,
anim_blocks_index: i32,
pub anim_block_model: i32, // Placeholder for mutable-void*
// Points to a series of bytes?
pub bone_table_name_index: i32,
pub vertex_base: i32, // Placeholder for void*
pub offset_base: i32, // Placeholder for void*
// Used with $constantdirectionallight from the QC
// Model should have flag #13 set if enabled
pub directional_dot_product: u8,
pub root_lod: u8, // Preferred rather than clamped
// 0 means any allowed, N means Lod 0 -> (N-1)
pub num_allowed_root_lods: u8,
#[allow(dead_code)]
unused0: u8, // ??
#[allow(dead_code)]
unused1: i32, // ??
pub flex_controller_ui_count: i32,
pub flex_controller_ui_index: i32,
pub vert_anim_fixed_point_scale: f32,
pub unused2: i32,
pub studio_hdr2_index: i32,
#[allow(dead_code)]
unused3: i32,
}
impl StudioHeader {
pub fn header2_index(&self) -> Option<usize> {
(self.studio_hdr2_index > 0)
.then(|| self.studio_hdr2_index)
.and_then(|index| usize::try_from(index).ok())
}
pub fn bone_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.bone_offset, self.bone_count, size_of::<Bone>())
}
pub fn bone_controller_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.bone_controller_offset, self.bone_controller_count, 1)
}
pub fn hitbox_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.hitbox_offset, self.hitbox_count, 1)
}
pub fn local_animation_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.local_animation_offset, self.local_animation_count, 1)
}
pub fn local_sequence_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.local_seq_offset, self.local_seq_count, 1)
}
pub fn texture_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.texture_offset, self.texture_count, 1)
}
pub fn texture_dir_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.texture_dir_offset, self.texture_dir_count, 1)
}
pub fn body_part_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.body_part_offset, self.body_part_count, 1)
}
pub fn attachment_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.attachment_offset, self.attachment_count, 1)
}
pub fn local_node_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.local_node_index, self.local_node_count, 1)
}
pub fn local_node_name_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.local_node_name_index, self.local_node_count, 1)
}
pub fn flex_descriptor_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.flex_desc_index, self.flex_desc_count, 1)
}
pub fn flex_controller_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.flex_controller_index, self.flex_controller_count, 1)
}
pub fn flex_rule_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.flex_rules_index, self.flex_rules_count, 1)
}
pub fn ik_chain_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.ik_chain_index, self.ik_chain_count, 1)
}
pub fn mouth_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.mouths_index, self.mouths_count, 1)
}
pub fn local_pose_param_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.local_pose_param_index, self.local_pose_param_count, 1)
}
pub fn key_value_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.key_value_index, self.key_value_count, 1)
}
pub fn ik_lock_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.ik_lock_index, self.ik_lock_count, 1)
}
pub fn include_model_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.include_model_index, self.include_model_count, 1)
}
pub fn animation_block_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.anim_blocks_index, self.anim_blocks_count, 1)
}
pub fn animation_block_name_indexes(&self) -> impl Iterator<Item = usize> {
index_range(self.anim_blocks_name_index, self.anim_blocks_count, 1)
}
pub fn flex_controller_ui_indexes(&self) -> impl Iterator<Item = usize> {
index_range(
self.flex_controller_ui_index,
self.flex_controller_ui_count,
1,
)
}
}
fn index_range(index: i32, count: i32, size: usize) -> impl Iterator<Item = usize> {
(0..count as usize)
.map(move |i| i * size)
.map(move |i| index as usize + i)
}
static_assertions::const_assert_eq!(size_of::<StudioHeader>() - size_of::<FixedString<0>>(), 408);

39
src/data/header2.rs Normal file
View file

@ -0,0 +1,39 @@
use std::ops::Range;
pub struct StudioHHeader2 {
source_bone_transform_count: i32,
source_bone_transform_index: i32,
pub illumination_position_attachment_index: i32,
fl_max_exe_deflection: f32,
pub linear_bone_index: i32,
pub sz_name_index: i32,
bone_flex_driver_count: i32,
bone_flex_driver_index: i32,
#[allow(dead_code)]
reserved: [i32; 56],
}
impl StudioHHeader2 {
pub fn source_bone_transforms(&self) -> Range<i32> {
self.source_bone_transform_index
..(self.source_bone_transform_index + self.source_bone_transform_count)
}
pub fn bone_flex_drivers(&self) -> Range<i32> {
self.bone_flex_driver_index..(self.bone_flex_driver_index + self.bone_flex_driver_count)
}
pub fn max_eye_deflection(&self) -> f32 {
if self.fl_max_exe_deflection == 0.0 {
(30.0f32).cos()
} else {
self.fl_max_exe_deflection
}
}
}

114
src/data/mod.rs Normal file
View file

@ -0,0 +1,114 @@
mod bone;
mod header;
mod header2;
pub use bone::*;
pub use header::*;
pub use header2::*;
use crate::error::StringError;
use arrayvec::ArrayString;
use binrw::{BinRead, BinResult, ReadOptions};
use std::fmt;
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, Copy, BinRead)]
pub struct Vector {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl From<Vector> for [f32; 3] {
fn from(vector: Vector) -> Self {
[vector.x, vector.y, vector.z]
}
}
impl From<[f32; 3]> for Vector {
fn from(vector: [f32; 3]) -> Self {
Vector {
x: vector[0],
y: vector[1],
z: vector[2],
}
}
}
impl From<&Vector> for [f32; 3] {
fn from(vector: &Vector) -> Self {
[vector.x, vector.y, vector.z]
}
}
#[derive(Debug, Clone, BinRead)]
pub struct Quaternion {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
#[derive(Debug, Clone, BinRead)]
pub struct RadianEuler {
pub x: f32,
pub y: f32,
pub z: f32,
}
/// Fixed length, null-terminated string
#[derive(Debug, Clone)]
pub struct FixedString<const LEN: usize>(ArrayString<LEN>);
impl<const N: usize> AsRef<str> for FixedString<N> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<const N: usize> FixedString<N> {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl<const LEN: usize> Display for FixedString<LEN> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<const LEN: usize> BinRead for FixedString<LEN> {
type Args = ();
fn read_options<R: binrw::io::Read + binrw::io::Seek>(
reader: &mut R,
options: &ReadOptions,
args: Self::Args,
) -> BinResult<Self> {
use std::str;
let name_buf = <[u8; LEN]>::read_options(reader, options, args)?;
let zero_pos =
name_buf
.iter()
.position(|c| *c == 0)
.ok_or_else(|| binrw::Error::Custom {
pos: reader.stream_position().unwrap(),
err: Box::new(StringError::NotNullTerminated),
})?;
let name = &name_buf[..zero_pos];
Ok(FixedString(
ArrayString::from(
str::from_utf8(name)
.map_err(StringError::NonUTF8)
.map_err(|e| binrw::Error::Custom {
pos: reader.stream_position().unwrap(),
err: Box::new(e),
})?,
)
.unwrap(),
))
}
}

41
src/error.rs Normal file
View file

@ -0,0 +1,41 @@
use thiserror::Error;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum MdlError {
#[error("io error while reading data: {0}")]
IO(#[from] std::io::Error),
#[error(transparent)]
String(#[from] StringError),
#[error("Malformed field found while parsing: {0:#}")]
MalformedData(binrw::Error),
#[error("referenced data is out of bounds")]
OutOfBounds,
}
impl From<binrw::Error> for MdlError {
fn from(e: binrw::Error) -> Self {
use binrw::Error;
// only a few error types should be generated by our code
match e {
Error::Io(e) => MdlError::IO(e),
Error::Custom { err, .. } => {
if err.is::<StringError>() {
MdlError::String(*err.downcast::<StringError>().unwrap())
} else {
panic!("unexpected custom error")
}
}
e => MdlError::MalformedData(e),
}
}
}
#[derive(Debug, Error)]
pub enum StringError {
#[error(transparent)]
NonUTF8(#[from] std::str::Utf8Error),
#[error("String is not null-terminated")]
NotNullTerminated,
}

32
src/handle/mod.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::Mdl;
use std::ops::Deref;
/// A handle represents a data structure in the mdl file and the mdl file containing it.
///
/// Keeping a reference of the mdl file with the data is required since a lot of data types
/// reference parts from other structures in the mdl file
#[derive(Debug)]
pub struct Handle<'a, T> {
mdl: &'a Mdl,
data: &'a T,
}
impl<T> Clone for Handle<'_, T> {
fn clone(&self) -> Self {
Handle { ..*self }
}
}
impl<'a, T> AsRef<T> for Handle<'a, T> {
fn as_ref(&self) -> &'a T {
self.data
}
}
impl<T> Deref for Handle<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.data
}
}

View file

@ -1,8 +1,41 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
mod data;
mod error;
mod handle;
use binrw::{BinRead, BinReaderExt};
pub use data::*;
pub use error::*;
pub use handle::Handle;
use std::io::Cursor;
#[derive(Debug, Clone)]
pub struct Mdl {
pub header: StudioHeader,
pub bones: Vec<Bone>,
}
impl Mdl {
pub fn read(data: &[u8]) -> Result<Self, MdlError> {
let mut reader = Cursor::new(data);
let header: StudioHeader = reader.read_le()?;
let bones = read_indexes(header.bone_indexes(), data).collect::<Result<_, _>>()?;
Ok(Mdl { header, bones })
}
}
fn read_indexes<'a, I: Iterator<Item = usize> + 'static, T: BinRead>(
indexes: I,
data: &'a [u8],
) -> impl Iterator<Item = Result<T, MdlError>> + 'a
where
T::Args: Default,
{
indexes
.map(|index| data.get(index..).ok_or(MdlError::OutOfBounds))
.map(|data| {
data.and_then(|data| {
let mut cursor = Cursor::new(data);
cursor.read_le().map_err(MdlError::from)
})
})
}