diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-22 12:12:27 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-22 12:13:32 +0300 |
| commit | d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch) | |
| tree | a0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/unitdat | |
| parent | 7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff) | |
| download | fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip | |
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/unitdat')
| -rw-r--r-- | crates/unitdat/Cargo.toml | 10 | ||||
| -rw-r--r-- | crates/unitdat/src/lib.rs | 180 |
2 files changed, 0 insertions, 190 deletions
diff --git a/crates/unitdat/Cargo.toml b/crates/unitdat/Cargo.toml deleted file mode 100644 index 73df4df..0000000 --- a/crates/unitdat/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "unitdat" -version = "0.1.0" -edition = "2021" - -[dependencies] -encoding_rs = "0.8" - -[dev-dependencies] -common = { path = "../common" } diff --git a/crates/unitdat/src/lib.rs b/crates/unitdat/src/lib.rs deleted file mode 100644 index 6414e66..0000000 --- a/crates/unitdat/src/lib.rs +++ /dev/null @@ -1,180 +0,0 @@ -use encoding_rs::WINDOWS_1251; -use std::fmt; -use std::fs; -use std::path::Path; - -const MIN_SIZE: usize = 0x48; -const MAGIC: u32 = 0x0000_F0F1; - -pub type Result<T> = core::result::Result<T, Error>; - -#[derive(Debug)] -pub enum Error { - Io(std::io::Error), - TooSmall { got: usize }, - InvalidMagic { got: u32 }, - MissingArchiveName, - MissingModelKey, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Io(err) => write!(f, "{err}"), - Self::TooSmall { got } => write!(f, "unit .dat is too small: {got} bytes"), - Self::InvalidMagic { got } => write!(f, "invalid .dat magic: 0x{got:08X}"), - Self::MissingArchiveName => write!(f, "unit .dat has empty archive name"), - Self::MissingModelKey => write!(f, "unit .dat has empty model key"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Io(err) => Some(err), - _ => None, - } - } -} - -impl From<std::io::Error> for Error { - fn from(value: std::io::Error) -> Self { - Self::Io(value) - } -} - -#[derive(Clone, Debug)] -pub struct UnitDat { - pub magic: u32, - pub flags: u32, - pub archive_name: String, - pub model_key: String, -} - -pub fn parse_path(path: impl AsRef<Path>) -> Result<UnitDat> { - let bytes = fs::read(path.as_ref())?; - parse_bytes(&bytes) -} - -pub fn parse_bytes(bytes: &[u8]) -> Result<UnitDat> { - if bytes.len() < MIN_SIZE { - return Err(Error::TooSmall { got: bytes.len() }); - } - - let magic = read_u32(bytes, 0).ok_or(Error::TooSmall { got: bytes.len() })?; - if magic != MAGIC { - return Err(Error::InvalidMagic { got: magic }); - } - - let flags = read_u32(bytes, 4).ok_or(Error::TooSmall { got: bytes.len() })?; - let archive_name = decode_c_string_fixed(&bytes[0x08..0x28]); - if archive_name.is_empty() { - return Err(Error::MissingArchiveName); - } - - let model_key = decode_c_string_fixed(&bytes[0x28..0x48]); - if model_key.is_empty() { - return Err(Error::MissingModelKey); - } - - Ok(UnitDat { - magic, - flags, - archive_name, - model_key, - }) -} - -fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> { - let end = offset.checked_add(4)?; - let chunk = bytes.get(offset..end)?; - Some(u32::from_le_bytes(chunk.try_into().ok()?)) -} - -fn decode_c_string_fixed(bytes: &[u8]) -> String { - let used = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - let (decoded, _, _) = WINDOWS_1251.decode(&bytes[..used]); - decoded.trim().to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - use common::collect_files_recursive; - use std::path::{Path, PathBuf}; - - fn game_root() -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("testdata") - .join("Parkan - Iron Strategy"); - root.is_dir().then_some(root) - } - - #[test] - fn parses_known_dat_files() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let samples = [ - root.join("UNITS/UNITS/HERO/tut1_p.dat"), - root.join("UNITS/UNITS/BATTLE/l_targ.dat"), - root.join("UNITS/BUILDS/BRIDGE/m_bridge.dat"), - ]; - - for path in samples { - if !path.is_file() { - eprintln!("skipping missing sample {}", path.display()); - continue; - } - let dat = parse_path(&path) - .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display())); - assert_eq!(dat.magic, MAGIC); - assert!(dat.archive_name.to_ascii_lowercase().ends_with(".rlb")); - assert!(dat.model_key.contains('_')); - } - } - - #[test] - fn parses_retail_dat_corpus() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let units_root = root.join("UNITS"); - let mut files = Vec::new(); - collect_files_recursive(&units_root, &mut files); - files.sort(); - - let mut parsed = 0usize; - for path in files { - if !path - .extension() - .and_then(|ext| ext.to_str()) - .is_some_and(|ext| ext.eq_ignore_ascii_case("dat")) - { - continue; - } - let dat = parse_path(&path) - .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display())); - assert!( - !dat.archive_name.is_empty(), - "{} empty archive", - path.display() - ); - assert!( - !dat.model_key.is_empty(), - "{} empty model key", - path.display() - ); - parsed += 1; - } - - assert!(parsed > 0, "no .dat files parsed"); - } -} |
