Skip to main content

darkomen/portrait/heads/
mod.rs

1mod decoder;
2mod encoder;
3
4#[cfg(feature = "bevy_reflect")]
5use bevy_reflect::prelude::*;
6use bitflags::bitflags;
7use glam::{I8Vec3, U8Vec2};
8use serde::{Deserialize, Serialize};
9
10pub use decoder::{DecodeError, Decoder};
11pub use encoder::{EncodeError, Encoder};
12
13bitflags! {
14    #[repr(transparent)]
15    #[derive(Clone, Copy, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
16    #[cfg_attr(feature = "debug", derive(Debug))]
17    #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(opaque), reflect(Default, Deserialize, Hash, PartialEq, Serialize))]
18    #[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
19    pub struct HeadFlags: u8 {
20        const NONE = 0;
21        /// The character has no mouth model, e.g., because they wear a helmet
22        /// (i.e., no "BITS" texture).
23        const NO_MOUTH = 1 << 0;
24        /// Hide accessory slot 0 in meet (non-battle) scenes. Slots are
25        /// 0-indexed.
26        const HIDE_ACCESSORY_0_IN_MEET = 1 << 1;
27        /// Hide accessory slot 1 in meet (non-battle) scenes. Slots are
28        /// 0-indexed.
29        const HIDE_ACCESSORY_1_IN_MEET = 1 << 2;
30        /// Hide head accessory in meet (non-battle) scenes.
31        const HIDE_HEAD_ACCESSORY_IN_MEET = 1 << 3;
32        /// The character does not have an injured variant (i.e., no "HEADI" or
33        /// "BITSI" textures).
34        const NO_INJURED_VARIANT = 1 << 4;
35        /// The character does not have a death variant (i.e., no "DEATH"
36        /// texture).
37        const NO_DEATH_VARIANT = 1 << 5;
38        const UNKNOWN_HEAD_FLAG_6 = 1 << 6;
39        const UNKNOWN_HEAD_FLAG_7 = 1 << 7;
40    }
41}
42
43#[derive(Clone, Default, Deserialize, Serialize)]
44#[cfg_attr(feature = "debug", derive(Debug))]
45#[cfg_attr(
46    feature = "bevy_reflect",
47    derive(Reflect),
48    reflect(Default, Deserialize, Serialize)
49)]
50#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
51pub struct HeadsDatabase {
52    pub entries: Vec<HeadEntry>,
53}
54
55#[derive(Clone, Default, Deserialize, Serialize)]
56#[cfg_attr(feature = "debug", derive(Debug))]
57#[cfg_attr(
58    feature = "bevy_reflect",
59    derive(Reflect),
60    reflect(Default, Deserialize, Serialize)
61)]
62#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
63pub struct HeadEntry {
64    /// 2-character ASCII identifier for the head (e.g., "KZ", "MB", "GS"). Used
65    /// to load textures like `{name}_HEAD.BMP`, `{name}_BODY.BMP`.
66    pub name: String,
67    /// Flags.
68    pub flags: HeadFlags,
69    /// Animation sequences ID for battles. References which .SEQ file to use
70    /// (0-63 maps to `{id}.SEQ`).
71    pub battle_sequences_id: u8,
72    /// Animation sequences ID for meets. References which .SEQ file to use
73    /// (0-63 maps to `{id}.SEQ`).
74    pub meet_sequences_id: u8,
75    pub mouth: Option<Mouth>,
76    pub eyes: Option<Eyes>,
77    /// Body model.
78    body: ModelSlot,
79    /// Head model.
80    head: ModelSlot,
81    /// Animation keyframes ID for battles. References which .KEY file to use
82    /// (0-63 maps to `{id}.KEY`).
83    pub battle_keyframes_id: u8,
84    /// Animation keyframes ID for meets. References which .KEY file to use
85    /// (0-63 maps to `{id}.KEY`).
86    pub meet_keyframes_id: u8,
87    /// Neck model.
88    neck: ModelSlot,
89    /// Equipment/accessory models (e.g., sword, staff, shield). 2 slots
90    /// available.
91    accessories: [ModelSlot; 2],
92    /// Head accessory model (e.g., horns on helmet, laurel wreath on helmet).
93    head_accessory: ModelSlot,
94}
95
96impl HeadEntry {
97    pub fn body(&self) -> Option<ModelSlot> {
98        if self.body.model_id == 0 {
99            None
100        } else {
101            Some(self.body.clone())
102        }
103    }
104
105    pub fn head(&self) -> Option<ModelSlot> {
106        if self.head.model_id == 0 {
107            None
108        } else {
109            Some(self.head.clone())
110        }
111    }
112
113    pub fn head_accessory(&self) -> Option<ModelSlot> {
114        if self.head_accessory.model_id == 0 {
115            None
116        } else {
117            Some(self.head_accessory.clone())
118        }
119    }
120
121    pub fn neck(&self) -> Option<ModelSlot> {
122        if self.neck.model_id == 0 {
123            None
124        } else {
125            Some(self.neck.clone())
126        }
127    }
128
129    pub fn accessories(&self) -> [Option<ModelSlot>; 2] {
130        [
131            if self.accessories[0].model_id == 0 {
132                None
133            } else {
134                Some(self.accessories[0].clone())
135            },
136            if self.accessories[1].model_id == 0 {
137                None
138            } else {
139                Some(self.accessories[1].clone())
140            },
141        ]
142    }
143}
144
145#[derive(Clone, Default, Deserialize, Serialize)]
146#[cfg_attr(feature = "debug", derive(Debug))]
147#[cfg_attr(
148    feature = "bevy_reflect",
149    derive(Reflect),
150    reflect(Default, Deserialize, Serialize)
151)]
152#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
153pub struct ModelSlot {
154    /// Model ID (1-63). 0 means no model in this slot.
155    pub model_id: u8,
156    /// Translation offset [x, y, z] in integer format. Multiply by 0.05 to
157    /// get world coordinates.
158    pub translation: I8Vec3,
159}
160
161#[derive(Clone, Default, Deserialize, Serialize)]
162#[cfg_attr(feature = "debug", derive(Debug))]
163#[cfg_attr(
164    feature = "bevy_reflect",
165    derive(Reflect),
166    reflect(Default, Deserialize, Serialize)
167)]
168#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
169pub struct Mouth {
170    /// The size [width, height] of the mouth. Combine with the position to
171    /// determine the mouth rectangle in the head texture.
172    pub size: U8Vec2,
173    /// The top left position [x, y] to position the mouth in the head texture.
174    pub position: U8Vec2,
175}
176
177#[derive(Clone, Default, Deserialize, Serialize)]
178#[cfg_attr(feature = "debug", derive(Debug))]
179#[cfg_attr(
180    feature = "bevy_reflect",
181    derive(Reflect),
182    reflect(Default, Deserialize, Serialize)
183)]
184#[cfg_attr(all(feature = "bevy_reflect", feature = "debug"), reflect(Debug))]
185pub struct Eyes {
186    /// The size [width, height] of the eyes. Combine with the position to
187    /// determine the eyes rectangle in the head texture.
188    pub size: U8Vec2,
189    /// The top left position [x, y] to position the eyes in the head texture.
190    pub position: U8Vec2,
191}
192
193#[cfg(test)]
194mod tests {
195    use std::{
196        ffi::{OsStr, OsString},
197        fs::File,
198        path::{Path, PathBuf},
199    };
200
201    use pretty_assertions::assert_eq;
202
203    use super::*;
204
205    fn roundtrip_test(original_bytes: &[u8], heads: &HeadsDatabase) {
206        let mut encoded_bytes = Vec::new();
207        Encoder::new(&mut encoded_bytes).encode(heads).unwrap();
208
209        let original_bytes = original_bytes
210            .chunks(16)
211            .map(|chunk| {
212                chunk
213                    .iter()
214                    .map(|b| format!("{b:02X}"))
215                    .collect::<Vec<_>>()
216                    .join(" ")
217            })
218            .collect::<Vec<_>>()
219            .join("\n");
220
221        let encoded_bytes = encoded_bytes
222            .chunks(16)
223            .map(|chunk| {
224                chunk
225                    .iter()
226                    .map(|b| format!("{b:02X}"))
227                    .collect::<Vec<_>>()
228                    .join(" ")
229            })
230            .collect::<Vec<_>>()
231            .join("\n");
232
233        assert_eq!(original_bytes, encoded_bytes);
234    }
235
236    #[test]
237    fn test_decode_heads_db() {
238        let d: PathBuf = [
239            std::env::var("DARKOMEN_PATH").unwrap().as_str(),
240            "DARKOMEN",
241            "GRAPHICS",
242            "PORTRAIT",
243            "SCRIPT",
244            "HEADS.DB",
245        ]
246        .iter()
247        .collect();
248
249        let original_bytes = std::fs::read(d.clone()).unwrap();
250        let file = File::open(d).unwrap();
251        let heads = Decoder::new(file).decode().unwrap();
252
253        assert_eq!(heads.entries.len(), 63);
254        assert_eq!(heads.entries.first().unwrap().name, "MB");
255        assert_eq!(
256            heads.entries.first().unwrap().flags,
257            HeadFlags::HIDE_ACCESSORY_0_IN_MEET | HeadFlags::HIDE_ACCESSORY_1_IN_MEET
258        );
259        assert_eq!(heads.entries.first().unwrap().body.model_id, 2);
260        assert_eq!(heads.entries.first().unwrap().head.model_id, 13);
261
262        roundtrip_test(&original_bytes, &heads);
263    }
264
265    #[test]
266    fn test_encode_too_many_entries() {
267        let heads = HeadsDatabase {
268            entries: vec![HeadEntry::default(); 256],
269        };
270
271        let mut encoded_bytes = Vec::new();
272        let result = Encoder::new(&mut encoded_bytes).encode(&heads);
273
274        assert!(result.is_err());
275        match result {
276            Err(EncodeError::TooManyEntries) => (),
277            _ => panic!("Expected TooManyEntries error"),
278        }
279    }
280
281    #[test]
282    fn test_decode_all() {
283        let d: PathBuf = [
284            std::env::var("DARKOMEN_PATH").unwrap().as_str(),
285            "DARKOMEN",
286            "GRAPHICS",
287            "PORTRAIT",
288        ]
289        .iter()
290        .collect();
291
292        let root_output_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "decoded", "portrait", "heads"]
293            .iter()
294            .collect();
295
296        std::fs::create_dir_all(&root_output_dir).unwrap();
297
298        fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&Path)) {
299            println!("Reading dir {:?}", dir.display());
300
301            let mut paths = std::fs::read_dir(dir)
302                .unwrap()
303                .map(|res| res.map(|e| e.path()))
304                .collect::<Result<Vec<_>, std::io::Error>>()
305                .unwrap();
306
307            paths.sort();
308
309            for path in paths {
310                if path.is_dir() {
311                    visit_dirs(&path, cb);
312                } else {
313                    cb(&path);
314                }
315            }
316        }
317
318        visit_dirs(&d, &mut |path| {
319            let Some(ext) = path.extension() else {
320                return;
321            };
322            if ext.to_string_lossy().to_uppercase() != "DB" {
323                return;
324            }
325            // Just decode HEADS.DB because BACKUP.DB doesn't start with the
326            // head count and HEADSBU.DB seems corrupted so we can't decode them
327            // properly.
328            if path.file_stem().unwrap().to_string_lossy() != "HEADS" {
329                return;
330            }
331
332            println!("Decoding {:?}", path.file_name().unwrap());
333
334            let original_bytes = std::fs::read(path).unwrap();
335
336            let file = File::open(path).unwrap();
337            let heads = Decoder::new(file).decode().unwrap();
338
339            roundtrip_test(&original_bytes, &heads);
340
341            let parent_dir = path
342                .components()
343                .collect::<Vec<_>>()
344                .iter()
345                .rev()
346                .skip(1) // skip the file name
347                .take_while(|c| c.as_os_str() != "DARKOMEN")
348                .collect::<Vec<_>>()
349                .iter()
350                .rev()
351                .collect::<PathBuf>();
352            let output_dir = root_output_dir.join(parent_dir);
353            std::fs::create_dir_all(&output_dir).unwrap();
354
355            // Write the complete database.
356            let output_path = append_ext("ron", output_dir.join(path.file_name().unwrap()));
357            let mut buffer = String::new();
358            ron::ser::to_writer_pretty(&mut buffer, &heads, Default::default()).unwrap();
359            std::fs::write(output_path, buffer).unwrap();
360
361            // Write individual head entries.
362            let db_name = path.file_stem().unwrap().to_string_lossy();
363            let individual_dir = output_dir.join(db_name.as_ref());
364            std::fs::create_dir_all(&individual_dir).unwrap();
365
366            for (index, entry) in heads.entries.iter().enumerate() {
367                let individual_path =
368                    individual_dir.join(format!("{:02}_{}.ron", index, entry.name));
369                let mut buffer = String::new();
370                ron::ser::to_writer_pretty(&mut buffer, entry, Default::default()).unwrap();
371                std::fs::write(individual_path, buffer).unwrap();
372            }
373        });
374    }
375
376    fn append_ext(ext: impl AsRef<OsStr>, path: PathBuf) -> PathBuf {
377        let mut os_string: OsString = path.into();
378        os_string.push(".");
379        os_string.push(ext.as_ref());
380        os_string.into()
381    }
382}