Files
rdl/src/wad.rs

374 lines
12 KiB
Rust

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<NiceWadLumpEntry>,
level_indicies: Vec<usize>,
}
pub fn find_txt_file(path: &PathBuf) -> Option<PathBuf> {
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<NiceWadLumpEntry> = 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<String, (HashMap<Enemy, u16>, HashMap<HealthAndArmour, u16>)> {
let mut file = BufReader::new(File::open(ow.path).unwrap());
let mut level_summary: HashMap<String, (HashMap<Enemy, u16>, HashMap<HealthAndArmour, u16>)> =
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<Enemy, u16> = HashMap::new();
let mut health_map: HashMap<HealthAndArmour, u16> = HashMap::new();
for _ in 0..(level_things_wad_lump.size / std::mem::size_of::<WadThingLump>() 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<u16> 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<u16> 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::<WadHeader>()];
const _LDS: [u8; 16] = [0; std::mem::size_of::<WadLumpDirectoryEntry>()];
const _WTL: [u8; 10] = [0; std::mem::size_of::<WadThingLump>()];
}
}