use bincode; use serde::Deserialize; use std::collections::HashMap; use std::fs::File; use std::fs; use std::io::{BufReader, Seek, SeekFrom}; use std::path::PathBuf; use strsim; /* A great document I've used as a reference is The Unofficial Doom Specs v1.666 https://www.gamers.org/dhs/helpdocs/dmsp1666.html References in comments are probably to this document */ // See Chapter 2 #[derive(Deserialize, Debug)] pub struct WadHeader { pub identifier: [u8; 4], pub num_lumps: i32, pub file_offset_to_start: i32, } // See Chapter 2 #[derive(Deserialize, Debug)] pub struct WadLumpDirectoryEntry { pub offset: i32, pub size: i32, // IN BYTES pub name: [u8; 8], } #[derive(Debug)] pub struct NiceWadLumpEntry { pub offset: i32, pub size: i32, pub name: String, } pub struct LevelSummary { pub name: String, pub num_enemies: u32, pub num_health_pickups: u32, } // Chapter 4-2 #[derive(Deserialize, Debug)] pub struct WadThingLump { pub x: i16, pub y: i16, pub angle: i16, pub thing_type: u16, pub options: u16, } pub struct OpenWad { path: PathBuf, _header: WadHeader, nice_lumps: Vec, level_indicies: Vec, } pub fn find_txt_file(path: &PathBuf) -> Option { if !path.exists() || !path.is_file() { return None } if let (Some(f), Some(p)) = (path.file_name(), path.parent()) { let files_in_folder = fs::read_dir(&p).unwrap(); let mut txt_file = PathBuf::from(f); txt_file.set_extension(".txt"); let txt_file = txt_file.into_os_string().into_string().unwrap().to_lowercase(); for f in files_in_folder { // Jesus this can't be the right way to do things let f1 = f.unwrap().path().file_name().unwrap().to_os_string().into_string().unwrap(); let f2 = f1.clone().to_lowercase(); let result = strsim::normalized_levenshtein(&txt_file, &f2); if result > 0.5 { let mut f3 = PathBuf::from(p); f3.push(f1); return Some(f3); } } } None } pub fn open_wad(path: &PathBuf) -> OpenWad { let mut file = BufReader::new(File::open(path).unwrap()); let header: WadHeader = bincode::deserialize_from(&mut file).unwrap(); assert!(header.identifier == *b"IWAD" || header.identifier == *b"PWAD"); file.seek(SeekFrom::Start(header.file_offset_to_start as u64)) .unwrap(); let mut nice_lumps: Vec = Vec::new(); let mut level_indicies = Vec::new(); for lump_num in 0..header.num_lumps { let lump: WadLumpDirectoryEntry = bincode::deserialize_from(&mut file).unwrap(); let nice_lump = NiceWadLumpEntry { offset: lump.offset, size: lump.size, name: std::str::from_utf8(&lump.name).unwrap().to_string(), }; // I stole this from rust-doom. I looked up idSoftware Linux Doom, but they searched // for the strings "mapXY" or ExMy - I'm not 100% sure, but I bet many PWADs don't // neccesarily use this... I do think that probably PWADs provide different THINGS // for their maps - I don't know how you wouldn't // https://doomwiki.org/wiki/WAD says THINGS always comes after map name if &lump.name == b"THINGS\0\0" { assert!(lump_num > 0); level_indicies.push((lump_num - 1) as usize); } nice_lumps.push(nice_lump); } OpenWad { path: path.clone(), _header: header, nice_lumps, level_indicies, } } pub fn get_enemies_and_health_per_level( ow: OpenWad, ) -> HashMap, HashMap)> { let mut file = BufReader::new(File::open(ow.path).unwrap()); let mut level_summary: HashMap, HashMap)> = HashMap::new(); for level_i in ow.level_indicies { let name = ow.nice_lumps.get(level_i).unwrap().name.clone(); let level_things_wad_lump = ow.nice_lumps.get(level_i + 1).unwrap(); file.seek(SeekFrom::Start(level_things_wad_lump.offset as u64)) .unwrap(); let mut enemy_map: HashMap = HashMap::new(); let mut health_map: HashMap = HashMap::new(); for _ in 0..(level_things_wad_lump.size / std::mem::size_of::() as i32) { let map_thing: WadThingLump = bincode::deserialize_from(&mut file).unwrap(); // Just checking UV if map_thing.options & (1 << 2) > 0 { // This doesn't feel overly rust like - but it works... let health_thing: HealthAndArmour = map_thing.thing_type.into(); let enemy_thing: Enemy = map_thing.thing_type.into(); if health_thing != HealthAndArmour::Unknown { if let Some(n) = health_map.get(&map_thing.thing_type.into()) { health_map.insert(map_thing.thing_type.into(), n + 1); } else { health_map.insert(map_thing.thing_type.into(), 1); } } if enemy_thing != Enemy::Unknown { if let Some(n) = enemy_map.get(&map_thing.thing_type.into()) { enemy_map.insert(map_thing.thing_type.into(), n + 1); } else { enemy_map.insert(map_thing.thing_type.into(), 1); } } } } level_summary.insert(name, (enemy_map, health_map)); } level_summary } #[derive(Eq, Hash, PartialEq, Debug)] pub enum HealthAndArmour { HealthPotion, Stimpack, Medikit, Beserk, Soulsphere, SpiritArmour, GreenArmour, BlueArmour, Megasphere, Invulnerability, Unknown, } #[derive(Eq, Hash, PartialEq, Debug)] pub enum Enemy { FormerHuman, WolfensteinSs, FormerHumanSergeant, HeavyWeaponDude, Imp, Demon, Spectre, LostSoul, Cacodemon, HellKnight, BaronOfHell, Arachnotron, PainElemental, Revenant, Mancubus, ArchVile, SpiderMastermind, CyberDemon, BossBrain, Unknown, } impl Enemy { pub fn difficulty_value(enemy: &Self) -> u16 { match enemy { Self::FormerHuman => 20, Self::WolfensteinSs => 20, Self::FormerHumanSergeant => 60, Self::HeavyWeaponDude => 100, Self::Imp => 40, Self::Arachnotron => 240, Self::ArchVile => 500, Self::BaronOfHell => 250, Self::BossBrain => 1000, Self::Cacodemon => 200, Self::CyberDemon => 500, Self::HellKnight => 150, Self::LostSoul => 30, Self::Mancubus => 280, Self::PainElemental => 470, Self::Revenant => 430, Self::Spectre => 80, Self::SpiderMastermind => 500, Self::Demon => 70, Self::Unknown => 0, } } } impl HealthAndArmour { pub fn health_value(hoa: &Self) -> u16 { match hoa { Self::Beserk => 100, Self::Stimpack => 10, Self::GreenArmour => 100, Self::Invulnerability => 100, Self::Medikit => 25, Self::Megasphere => 300, Self::Soulsphere => 100, Self::SpiritArmour => 1, Self::HealthPotion => 1, Self::BlueArmour => 200, Self::Unknown => 0, } } } impl std::convert::From for Enemy { fn from(ttype: u16) -> Self { match ttype { 3004 => Self::FormerHuman, 84 => Self::WolfensteinSs, 9 => Self::FormerHumanSergeant, 65 => Self::HeavyWeaponDude, 3001 => Self::Imp, 3002 => Self::Demon, 58 => Self::Spectre, 3006 => Self::LostSoul, 3005 => Self::Cacodemon, 69 => Self::HellKnight, 3003 => Self::BaronOfHell, 68 => Self::Arachnotron, 71 => Self::PainElemental, 66 => Self::Revenant, 67 => Self::Mancubus, 64 => Self::ArchVile, 7 => Self::SpiderMastermind, 16 => Self::CyberDemon, 88 => Self::BossBrain, _ => Self::Unknown, } } } impl std::convert::From for HealthAndArmour { fn from(ttype: u16) -> Self { match ttype { 2011 => Self::Stimpack, 2012 => Self::Medikit, 2014 => Self::HealthPotion, 2015 => Self::SpiritArmour, 2018 => Self::GreenArmour, 2019 => Self::BlueArmour, 83 => Self::Soulsphere, 2013 => Self::Megasphere, 2022 => Self::Invulnerability, 2023 => Self::Beserk, _ => Self::Unknown, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_open_wad() { let freedoom_iwad = PathBuf::from("test_files/freedoom1.wad"); assert!( freedoom_iwad.exists(), "WAD test need freedoom1.wad - get it from here https://freedoom.github.io" ); let ow = open_wad(&freedoom_iwad); assert_eq!(&ow._header.identifier, b"IWAD"); assert_eq!(std::str::from_utf8(&ow._header.identifier).unwrap(), "IWAD"); } #[test] fn test_num_levels_correct() { let freedoom_iwad = PathBuf::from("test_files/freedoom1.wad"); let ow = open_wad(&freedoom_iwad); let summary = get_enemies_and_health_per_level(ow); assert_eq!(summary.len(), 9 * 4); } #[test] fn test_enemies_correct() { let freedoom_iwad = PathBuf::from("test_files/freedoom1.wad"); let ow = open_wad(&freedoom_iwad); let summary = get_enemies_and_health_per_level(ow); let (c1m1e, _) = summary.get("E1M1\0\0\0\0").unwrap(); assert_eq!(c1m1e.get(&Enemy::Imp), Some(&14)); } #[test] fn test_health_correct() { let freedoom_iwad = PathBuf::from("test_files/freedoom1.wad"); let ow = open_wad(&freedoom_iwad); let summary = get_enemies_and_health_per_level(ow); let (_, c1m1h) = summary.get("E1M1\0\0\0\0").unwrap(); assert_eq!(c1m1h.get(&HealthAndArmour::BlueArmour), None); // I wonder if this should be a Some(&0) assert_eq!(c1m1h.get(&HealthAndArmour::HealthPotion), Some(&17)); let (_, c4m1h) = summary.get("E4M1\0\0\0\0").unwrap(); assert_eq!(c4m1h.get(&HealthAndArmour::Stimpack), Some(&12)); } #[test] #[ignore] fn test_guess_at_difficulty() { let freedoom_iwad = PathBuf::from("test_files/freedoom1.wad"); let ow = open_wad(&freedoom_iwad); let summary = get_enemies_and_health_per_level(ow); let mut levels = Vec::new(); for (level_name, (enemy_sum, health_sum)) in summary { let mut enemy_total = 0; for (enemy, num) in enemy_sum { enemy_total += Enemy::difficulty_value(&enemy) * num; } let mut health_total = 0; for (hoa, num) in health_sum { health_total += HealthAndArmour::health_value(&hoa) * num; } levels.push((enemy_total / health_total, level_name, enemy_total, health_total)); } levels.sort(); println!("{:#?}", levels); panic!(); } #[test] fn test_txt_file_finding() { let wad_path = PathBuf::from("test_files/10amBreakM.wad"); let txt_file = find_txt_file(&wad_path); assert_eq!(txt_file, Some(PathBuf::from("test_files/10amBreakM.txt"))); } #[test] fn test_struct_size() { // This probably isn't really neccesary... but it might catch a mistake, maybe? // These shouldn't compile if the size is incorrect const _HS: [u8; 12] = [0; std::mem::size_of::()]; const _LDS: [u8; 16] = [0; std::mem::size_of::()]; const _WTL: [u8; 10] = [0; std::mem::size_of::()]; } }