From 527dbb76e952d31840a86c6d352ae064cf160b13 Mon Sep 17 00:00:00 2001 From: feored Date: Sun, 23 Jul 2023 23:04:50 +0200 Subject: [PATCH 1/9] Mercenary writing and parsing --- notes.md | 11 + rustfmt.toml | 3 +- src/character/mercenary.rs | 351 ++++++++++++++++++ src/{header/character.rs => character/mod.rs} | 31 +- src/header/mercenary.rs | 316 ---------------- src/header/mod.rs | 111 ------ src/items/mod.rs | 9 + src/lib.rs | 154 +++++++- src/quests.rs | 2 +- src/skills.rs | 27 +- 10 files changed, 548 insertions(+), 467 deletions(-) create mode 100644 src/character/mercenary.rs rename src/{header/character.rs => character/mod.rs} (89%) delete mode 100644 src/header/mercenary.rs delete mode 100644 src/header/mod.rs create mode 100644 src/items/mod.rs diff --git a/notes.md b/notes.md index 5dcb445..8a62989 100644 --- a/notes.md +++ b/notes.md @@ -1,3 +1,14 @@ +### Resources + +A list of resources that have helped with reverse engineering the .d2s format. + +* http://user.xmission.com/~trevin/DiabloIIv1.09_File_Format.shtm +* https://github.com/oaken-source/pyd2s/blob/master/docs/d2s_save_file_format_1.13d.txt +* https://github.com/WalterCouto/D2CE/blob/main/d2s_File_Format.md +* https://github.com/krisives/d2s-format +* https://github.com/nokka/d2s/blob/master/README.md + + ### Character Status Loading a single player file with "Ladder" bit set to 1 in Character Status does nothing (duh). diff --git a/rustfmt.toml b/rustfmt.toml index 5f22f2e..fbea351 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ -edition="2021" \ No newline at end of file +edition="2021" +array_width=100 \ No newline at end of file diff --git a/src/character/mercenary.rs b/src/character/mercenary.rs new file mode 100644 index 0000000..8ec236f --- /dev/null +++ b/src/character/mercenary.rs @@ -0,0 +1,351 @@ +use crate::Difficulty; +use crate::GameError; +use crate::ParseError; + +const SECTION_NAME: &'static str = "Mercenary"; +const SECTION_LENGTH: usize = 14; +const VARIANTS: &'static [Variant; 39] = &[ + (Class::Rogue(Rogue::Fire), Difficulty::Normal), + (Class::Rogue(Rogue::Cold), Difficulty::Normal), + (Class::Rogue(Rogue::Fire), Difficulty::Nightmare), + (Class::Rogue(Rogue::Cold), Difficulty::Nightmare), + (Class::Rogue(Rogue::Fire), Difficulty::Hell), + (Class::Rogue(Rogue::Cold), Difficulty::Hell), + ( + Class::DesertMercenary(DesertMercenary::Prayer), + Difficulty::Normal, + ), + ( + Class::DesertMercenary(DesertMercenary::Defiance), + Difficulty::Normal, + ), + ( + Class::DesertMercenary(DesertMercenary::BlessedAim), + Difficulty::Normal, + ), + ( + Class::DesertMercenary(DesertMercenary::Thorns), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::HolyFreeze), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::Might), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::Prayer), + Difficulty::Hell, + ), + ( + Class::DesertMercenary(DesertMercenary::Defiance), + Difficulty::Hell, + ), + ( + Class::DesertMercenary(DesertMercenary::BlessedAim), + Difficulty::Hell, + ), + (Class::IronWolf(IronWolf::Fire), Difficulty::Normal), + (Class::IronWolf(IronWolf::Cold), Difficulty::Normal), + (Class::IronWolf(IronWolf::Lightning), Difficulty::Normal), + (Class::IronWolf(IronWolf::Fire), Difficulty::Nightmare), + (Class::IronWolf(IronWolf::Cold), Difficulty::Nightmare), + (Class::IronWolf(IronWolf::Lightning), Difficulty::Nightmare), + (Class::IronWolf(IronWolf::Fire), Difficulty::Hell), + (Class::IronWolf(IronWolf::Cold), Difficulty::Hell), + (Class::IronWolf(IronWolf::Lightning), Difficulty::Hell), + (Class::Barbarian(Barbarian::Bash), Difficulty::Normal), + (Class::Barbarian(Barbarian::Bash), Difficulty::Normal), + (Class::Barbarian(Barbarian::Bash), Difficulty::Nightmare), + (Class::Barbarian(Barbarian::Bash), Difficulty::Nightmare), + (Class::Barbarian(Barbarian::Bash), Difficulty::Hell), + (Class::Barbarian(Barbarian::Bash), Difficulty::Hell), + ( + Class::DesertMercenary(DesertMercenary::Prayer), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::Defiance), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::BlessedAim), + Difficulty::Nightmare, + ), + ( + Class::DesertMercenary(DesertMercenary::Thorns), + Difficulty::Hell, + ), + ( + Class::DesertMercenary(DesertMercenary::HolyFreeze), + Difficulty::Hell, + ), + ( + Class::DesertMercenary(DesertMercenary::Might), + Difficulty::Hell, + ), + (Class::Barbarian(Barbarian::Frenzy), Difficulty::Normal), + (Class::Barbarian(Barbarian::Frenzy), Difficulty::Nightmare), + (Class::Barbarian(Barbarian::Frenzy), Difficulty::Hell), +]; + +const ROGUE_NAMES: [&'static str; 41] = [ + "Aliza", "Ampliza", "Annor", "Abhaya", "Elly", "Paige", "Basanti", "Blaise", "Kyoko", + "Klaudia", "Kundri", "Kyle", "Visala", "Elexa", "Floria", "Fiona", "Gwinni", "Gaile", "Hannah", + "Heather", "Iantha", "Diane", "Isolde", "Divo", "Ithera", "Itonya", "Liene", "Maeko", "Mahala", + "Liaza", "Meghan", "Olena", "Oriana", "Ryann", "Rozene", "Raissa", "Sharyn", "Shikha", "Debi", + "Tylena", "Wendy", +]; + +const DESERTMERCENARY_NAMES: [&'static str; 21] = [ + "Hazade", "Alhizeer", "Azrael", "Ahsab", "Chalan", "Haseen", "Razan", "Emilio", "Pratham", + "Fazel", "Jemali", "Kasim", "Gulzar", "Mizan", "Leharas", "Durga", "Neeraj", "Ilzan", + "Zanarhi", "Waheed", "Vikhyat", +]; + +const IRONWOLF_NAMES: [&'static str; 20] = [ + "Jelani", "Barani", "Jabari", "Devak", "Raldin", "Telash", "Ajheed", "Narphet", "Khaleel", + "Phaet", "Geshef", "Vanji", "Haphet", "Thadar", "Yatiraj", "Rhadge", "Yashied", "Jarulf", + "Flux", "Scorch", +]; + +const BARBARIAN_NAMES: [&'static str; 67] = [ + "Varaya", + "Khan", + "Klisk", + "Bors", + "Brom", + "Wiglaf", + "Hrothgar", + "Scyld", + "Healfdane", + "Heorogar", + "Halgaunt", + "Hygelac", + "Egtheow", + "Bohdan", + "Wulfgar", + "Hild", + "Heatholaf", + "Weder", + "Vikhyat", + "Unferth", + "Sigemund", + "Heremod", + "Hengest", + "Folcwald", + "Frisian", + "Hnaef", + "Guthlaf", + "Oslaf", + "Yrmenlaf", + "Garmund", + "Freawaru", + "Eadgils", + "Onela", + "Damien", + "Erfor", + "Weohstan", + "Wulf", + "Bulwye", + "Lief", + "Magnus", + "Klatu", + "Drus", + "Hoku", + "Kord", + "Uther", + "Ip", + "Ulf", + "Tharr", + "Kaelim", + "Ulric", + "Alaric", + "Ethelred", + "Caden", + "Elgifu", + "Tostig", + "Alcuin", + "Emund", + "Sigurd", + "Gorm", + "Hollis", + "Ragnar", + "Torkel", + "Wulfstan", + "Alban", + "Barloc", + "Bill", + "Theodoric", +]; + +pub type Variant = (Class, Difficulty); + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum Class { + Rogue(Rogue), + DesertMercenary(DesertMercenary), + IronWolf(IronWolf), + Barbarian(Barbarian), +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum Rogue { + Fire, + Cold, +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum DesertMercenary { + Prayer, + Defiance, + BlessedAim, + Thorns, + HolyFreeze, + Might, +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum IronWolf { + Fire, + Cold, + Lightning, +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum Barbarian { + Bash, + Frenzy, +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub struct Mercenary { + dead: bool, + id: u32, + name_id: u16, + name: &'static str, + variant: Variant, + experience: u32, +} + +impl Default for Mercenary { + fn default() -> Self { + Self { + dead: false, + id: 0, + name_id: 0, + name: ROGUE_NAMES[0], + variant: VARIANTS[0], + experience: 0, + } + } +} + +fn variant_id(variant: &Variant) -> Result{ + let mut variant_id : u16 = 99; + + for i in 0..VARIANTS.len(){ + if *variant == VARIANTS[i]{ + variant_id = i as u16; + break; + } + } + if (variant_id as usize) > VARIANTS.len(){ + Err(GameError{message: format!("There is no mercenary ID for type {0:?} recruited in {1:?}", variant.0, variant.1)}) + } else { + Ok(variant_id) + } +} + +fn names_list(class : Class) -> &'static [&'static str]{ + match class{ + Class::Rogue(_) => { + &ROGUE_NAMES + }, + Class::DesertMercenary(_) => { + &DESERTMERCENARY_NAMES + }, + Class::IronWolf(_) => { + &IRONWOLF_NAMES + }, + Class::Barbarian(_) => { + &BARBARIAN_NAMES + } + } +} + +pub fn parse_mercenary(data: &[u8; 14]) -> Result { + let mut mercenary: Mercenary = Mercenary::default(); + if data[0..2] != [0x00, 0x00] { + mercenary.dead = true; + } + + mercenary.id = u32::from_le_bytes(<[u8; 4]>::try_from(&data[2..6]).unwrap()); + let variant_id = u16::from_le_bytes(<[u8; 2]>::try_from(&data[8..10]).unwrap()); + mercenary.variant = VARIANTS[variant_id as usize]; + + let name_id = u16::from_le_bytes(<[u8; 2]>::try_from(&data[6..8]).unwrap()); + let names_list = names_list(mercenary.variant.0); + if name_id as usize > names_list.len() { + return Err(ParseError{section: String::from(SECTION_NAME), message: format!("Found invalid name ID {} for mercenary", name_id)}); + } + mercenary.name_id = name_id; + mercenary.name = names_list[name_id as usize]; + + mercenary.experience = u32::from_le_bytes(<[u8; 4]>::try_from(&data[10..14]).unwrap()); + + Ok(mercenary) +} + +pub fn generate_mercenary(mercenary: &Mercenary) -> Result<[u8;14], GameError>{ + let mut bytes : [u8; 14] = [0x00; 14]; + bytes[0..2].clone_from_slice(match mercenary.dead { + true => &[0x01, 0x00], + false => &[0x00, 0x00] + }); + + bytes[2..6].clone_from_slice(&mercenary.id.to_le_bytes()); + bytes[6..8].clone_from_slice(&mercenary.name_id.to_le_bytes()); + let variant_id = match variant_id(&mercenary.variant){ + Ok(id) => id, + Err(e) => return Err(e) + }; + + bytes[8..10].clone_from_slice(&variant_id.to_le_bytes()); + bytes[10..14].clone_from_slice(&mercenary.experience.to_le_bytes()); + + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use bit::BitIndex; + + #[test] + fn parse_mercenary_test(){ + let expected_result = Mercenary{dead: false, id:3461679u32, name_id: 3, name: "Abhaya", variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), experience: 63722u32}; + let bytes = [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; + let mut parsed_result : Mercenary = Mercenary::default(); + match parse_mercenary(&bytes) { + Ok(res) => {parsed_result = res}, + Err(e) => {println!{"Test failed: {e:?}"}} + }; + assert_eq!(parsed_result, expected_result); + } + + #[test] + fn generate_mercenary_test(){ + let expected_result = [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; + let merc = Mercenary{dead: false, id:3461679u32, name_id: 3, name: "Abhaya", variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), experience: 63722u32}; + let mut parsed_result : [u8; 14] = [0x00; 14]; + match generate_mercenary(&merc) { + Ok(res) => {parsed_result = res}, + Err(e) => {println!{"Test failed: {e:?}"}} + }; + assert_eq!(parsed_result, expected_result); + } + +} \ No newline at end of file diff --git a/src/header/character.rs b/src/character/mod.rs similarity index 89% rename from src/header/character.rs rename to src/character/mod.rs index cfc24f1..ea81f1b 100644 --- a/src/header/character.rs +++ b/src/character/mod.rs @@ -1,13 +1,15 @@ use crate::Act; use crate::Difficulty; -const CLASS_AMAZON: u8 = 0; -const CLASS_SORCERESS: u8 = 1; -const CLASS_NECROMANCER: u8 = 2; -const CLASS_PALADIN: u8 = 3; -const CLASS_BARBARIAN: u8 = 4; -const CLASS_DRUID: u8 = 5; -const CLASS_ASSASSIN: u8 = 6; +pub mod mercenary; + +const AMAZON: u8 = 0; +const SORCERESS: u8 = 1; +const NECROMANCER: u8 = 2; +const PALADIN: u8 = 3; +const BARBARIAN: u8 = 4; +const DRUID: u8 = 5; +const ASSASSIN: u8 = 6; const TITLES_CLASSIC_STANDARD_MALE: [&'static str; 4] = ["", "Sir", "Lord", "Baron"]; const TITLES_CLASSIC_STANDARD_FEMALE: [&'static str; 4] = ["", "Dame", "Lady", "Baroness"]; @@ -24,7 +26,7 @@ pub struct Character { name: String, pub status: Status, pub progression: u8, - pub class: CharacterClass, + pub class: Class, level: u8, difficulty: (Difficulty, Act), } @@ -43,7 +45,7 @@ pub enum WeaponSet { } #[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum CharacterClass { +pub enum Class { Amazon, Sorceress, Necromancer, @@ -71,7 +73,7 @@ impl Default for Character { name: String::from(""), status: Status::default(), progression: 0, - class: CharacterClass::Amazon, + class: Class::Amazon, level: 1, difficulty: (Difficulty::Normal, Act::Act1), } @@ -127,13 +129,8 @@ impl Character { impl Character { // Return the appropriate title accounting for difficulties beaten pub fn title(&self) -> String { - let male: bool = [ - CharacterClass::Barbarian, - CharacterClass::Paladin, - CharacterClass::Necromancer, - CharacterClass::Druid, - ] - .contains(&self.class); + let male: bool = [Class::Barbarian, Class::Paladin, Class::Necromancer, Class::Druid] + .contains(&self.class); if !self.status.expansion { let stage: usize = if self.progression < 4 { 0 diff --git a/src/header/mercenary.rs b/src/header/mercenary.rs deleted file mode 100644 index a215b46..0000000 --- a/src/header/mercenary.rs +++ /dev/null @@ -1,316 +0,0 @@ -use crate::Difficulty; - -//TOOD ADD ROGUE NAMES -// const ROGUE_NAMES : &'static [&'static str; 41] = [ -// "Aliza", -// "Ampliza", -// "Annor" -// "Abhaya", -// "Elly", -// "Paige", -// "Basanti", -// "Blaise", -// "" -// ] - -const MERCENARY_VARIANTS: &'static [MercenaryType; 39] = &[ - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Fire), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Cold), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Fire), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Cold), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Fire), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Cold), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Prayer), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Defiance), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::BlessedAim), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Thorns), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::HolyFreeze), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Might), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Prayer), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Defiance), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::BlessedAim), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Fire), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Cold), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Lightning), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Fire), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Cold), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Lightning), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Fire), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Cold), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::IronWolf, - variant: MercenaryVariant::IronWolf(IronWolfVariant::Lightning), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Bash), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Prayer), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Defiance), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::BlessedAim), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Thorns), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::HolyFreeze), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::DesertMercenary, - variant: MercenaryVariant::DesertMercenary(DesertMercenaryVariant::Might), - difficulty: Difficulty::Hell, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Frenzy), - difficulty: Difficulty::Normal, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Frenzy), - difficulty: Difficulty::Nightmare, - }, - MercenaryType { - class: MercenaryClass::Barbarian, - variant: MercenaryVariant::Barbarian(BarbarianVariant::Frenzy), - difficulty: Difficulty::Hell, - }, -]; - -#[derive(PartialEq, Eq)] -enum RogueVariant { - Fire, - Cold, -} - -#[derive(PartialEq, Eq)] -enum DesertMercenaryVariant { - Prayer, - Defiance, - BlessedAim, - Thorns, - HolyFreeze, - Might, -} - -#[derive(PartialEq, Eq)] -enum IronWolfVariant { - Fire, - Cold, - Lightning, -} - -#[derive(PartialEq, Eq)] -enum BarbarianVariant { - Bash, - Frenzy, -} - -#[derive(PartialEq, Eq)] -enum MercenaryVariant { - Rogue(RogueVariant), - DesertMercenary(DesertMercenaryVariant), - IronWolf(IronWolfVariant), - Barbarian(BarbarianVariant), -} - -impl Default for MercenaryType { - fn default() -> Self { - Self { - class: MercenaryClass::Rogue, - variant: MercenaryVariant::Rogue(RogueVariant::Cold), - difficulty: Difficulty::Normal, - } - } -} - -#[derive(PartialEq, Eq)] -pub struct MercenaryType { - class: MercenaryClass, - variant: MercenaryVariant, - difficulty: Difficulty, -} - -impl From for &MercenaryType { - fn from(id: u16) -> Self { - let id_as_usize = usize::from(id); - if id_as_usize > MERCENARY_VARIANTS.len() { - return &MERCENARY_VARIANTS[0]; - } else { - return &MERCENARY_VARIANTS[id_as_usize]; - } - } -} - -impl From<&MercenaryType> for u16 { - fn from(mercenary_type: &MercenaryType) -> Self { - for i in 0..MERCENARY_VARIANTS.len() { - if MERCENARY_VARIANTS[i] == *mercenary_type { - return i as u16; - } - } - return 0; - } -} - -#[derive(PartialEq, Eq)] -pub enum MercenaryClass { - Rogue, - DesertMercenary, - IronWolf, - Barbarian, -} - -pub struct Mercenary { - dead: bool, - id: u32, - name_id: u16, - mercenary_type: MercenaryType, - experience: u32, -} - -impl Default for Mercenary { - fn default() -> Self { - Self { - dead: false, - id: 0, - name_id: 0, - mercenary_type: MercenaryType::default(), - experience: 0, - } - } -} diff --git a/src/header/mod.rs b/src/header/mod.rs deleted file mode 100644 index 5a585aa..0000000 --- a/src/header/mod.rs +++ /dev/null @@ -1,111 +0,0 @@ -pub mod character; -pub mod mercenary; - -const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; - -const VERSION_100: u32 = 71; -const VERSION_107: u32 = 87; -const VERSION_108: u32 = 89; -const VERSION_109: u32 = 92; -const VERSION_110: u32 = 96; - -pub const OFFSET: usize = 0; - -#[derive(Debug)] -pub enum HeaderID { - Signature, - VersionID, - FileSize, - Checksum, - WeaponSet, - CharacterName, - CharacterStatus, - CharacterProgression, -} - -#[derive(Debug)] -pub enum Version { - V100, - V107, - V108, - V109, - V110, -} - -struct FileSection { - offset: usize, - bytes: usize, -} - -fn get_file_bytes_range(id: HeaderID) -> (usize, usize) { - let data: FileSection = get_file_data(id); - (data.offset, data.offset + data.bytes) -} - -fn get_file_data(id: HeaderID) -> FileSection { - match id { - HeaderID::Signature => FileSection { - offset: (0), - bytes: (4), - }, - HeaderID::VersionID => FileSection { - offset: (4), - bytes: (4), - }, - HeaderID::FileSize => FileSection { - offset: (8), - bytes: (4), - }, - HeaderID::Checksum => FileSection { - offset: (12), - bytes: (4), - }, - HeaderID::WeaponSet => FileSection { - offset: (16), - bytes: (4), - }, - HeaderID::CharacterName => FileSection { - offset: (20), - bytes: (16), - }, - HeaderID::CharacterStatus => FileSection { - offset: (36), - bytes: (1), - }, - HeaderID::CharacterProgression => FileSection { - offset: (37), - bytes: (1), - }, - } -} - -//Refactor into ::from -pub fn into_version(version_bytes: &[u8; 4]) -> Result { - let version_number: u32 = u32::from_le_bytes(*version_bytes); - match version_number { - VERSION_100 => Ok(Version::V100), - VERSION_107 => Ok(Version::V107), - VERSION_108 => Ok(Version::V108), - VERSION_109 => Ok(Version::V109), - VERSION_110 => Ok(Version::V110), - _ => Err("version ID does not match any known version of the game."), - } -} - -fn check_valid_signature(bytes: &Vec) -> bool { - let (header_start, header_end) = get_file_bytes_range(HeaderID::Signature); - bytes[header_start..header_end] == SIGNATURE -} - -pub fn calc_checksum(bytes: &Vec) -> i32 { - let mut checksum: i32 = 0; - let (checksum_start, checksum_end) = get_file_bytes_range(HeaderID::Checksum); - for i in 0..bytes.len() { - let mut ch: i32 = bytes[i] as i32; - if i >= checksum_start && i < checksum_end { - ch = 0; - } - checksum = (checksum << 1) + ch + ((checksum < 0) as i32); - } - checksum -} diff --git a/src/items/mod.rs b/src/items/mod.rs new file mode 100644 index 0000000..7a5c45d --- /dev/null +++ b/src/items/mod.rs @@ -0,0 +1,9 @@ +pub fn generate_temp_items() -> Vec { + return vec![ + 0x4D, 0x04, 0x00, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x00, 0x80, 0x06, 0x17, + 0x03, 0x02, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x02, 0x80, 0x06, 0x17, 0x03, + 0x02, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x04, 0x80, 0x06, 0x17, 0x03, 0x02, + 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x06, 0x80, 0x06, 0x17, 0x03, 0x02, 0x4A, + 0x4D, 0x00, 0x00, 0x6A, 0x66, 0x6B, 0x66, 0x00, + ]; +} diff --git a/src/lib.rs b/src/lib.rs index 8dbd8a2..de12b53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #![warn( anonymous_parameters, nonstandard_style, - rust_2018_idioms, single_use_lifetimes, trivial_casts, trivial_numeric_casts, @@ -11,28 +10,103 @@ variant_size_differences )] +use std::fmt; + pub mod attributes; -pub mod header; +pub mod character; +pub mod items; pub mod npcs; pub mod quests; pub mod skills; pub mod waypoints; -use header::character; -use header::mercenary; +const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; + +const VERSION_100: u32 = 71; +const VERSION_107: u32 = 87; +const VERSION_108: u32 = 89; +const VERSION_109: u32 = 92; +const VERSION_110: u32 = 96; +const VERSION_D2R_100: u32 = 97; +const VERSION_D2R_240: u32 = 98; +const VERSION_D2R_250: u32 = 99; + +#[derive(Debug, Clone)] +pub struct ParseError { + section: String, + message: String, +} + +#[derive(Debug, Clone)] +pub struct GameError{ + message: String +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Error parsing section '{}': {}", + self.section, self.message + ) + } +} pub struct Save { - version: header::Version, + version: Version, character: character::Character, - mercenary: mercenary::Mercenary, +} + +#[derive(Debug)] +pub enum HeaderID { + Signature, + VersionID, + FileSize, + Checksum, + WeaponSet, + Status, + Progression, + Class, + Level, + CreatedDate, + LastPlayedDate, + AssignedSkills, + LeftMouse, + RightMouse, + LeftMouseSwitch, + RightMouseSwitch, + MenuAppearance, //Needed? + Difficulty, + MapSeed, + Mercenary, + ResurrectedMenuAppearance, + Name, } #[derive(PartialEq, Eq, Debug)] +pub enum Version { + V100, + V107, + V108, + V109, + V110, + V200R, + V240R, + V250R, +} + +struct FileSection { + offset: usize, + bytes: usize, +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub enum Difficulty { Normal, Nightmare, Hell, } + #[derive(PartialEq, Eq, Debug)] pub enum Act { Act1, @@ -41,3 +115,71 @@ pub enum Act { Act4, Act5, } + +fn get_file_bytes_range(id: HeaderID) -> (usize, usize) { + let data: FileSection = get_file_data(id); + (data.offset, data.offset + data.bytes) +} + +fn get_file_data(id: HeaderID) -> FileSection { + match id { + HeaderID::Signature => FileSection { + offset: 0, + bytes: 4, + }, + HeaderID::VersionID => FileSection { + offset: 4, + bytes: 4, + }, + HeaderID::FileSize => FileSection { + offset: 8, + bytes: 4, + }, + HeaderID::Checksum => FileSection { + offset: 12, + bytes: 4, + }, + HeaderID::WeaponSet => FileSection { + offset: 16, + bytes: 4, + }, + HeaderID::Status => FileSection { + offset: 36, + bytes: 1, + }, + HeaderID::Progression => FileSection { + offset: 37, + bytes: 1, + }, + HeaderID::Class => FileSection { + offset: 40, + bytes: 1, + }, + HeaderID::Level => FileSection { + offset: 43, + bytes: 1, + }, + _ => FileSection { + offset: 0, + bytes: 0 + } + } +} + +fn check_valid_signature(bytes: &Vec) -> bool { + let (header_start, header_end) = get_file_bytes_range(HeaderID::Signature); + bytes[header_start..header_end] == SIGNATURE +} + +pub fn calc_checksum(bytes: &Vec) -> i32 { + let mut checksum: i32 = 0; + let (checksum_start, checksum_end) = get_file_bytes_range(HeaderID::Checksum); + for i in 0..bytes.len() { + let mut ch: i32 = bytes[i] as i32; + if i >= checksum_start && i < checksum_end { + ch = 0; + } + checksum = (checksum << 1) + ch + ((checksum < 0) as i32); + } + checksum +} diff --git a/src/quests.rs b/src/quests.rs index b0dc28e..9696131 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -11,7 +11,7 @@ pub struct QuestFlags { pub jerhyn_introduction: u16, } -pub fn build_section() -> Vec { +pub fn generate_section() -> Vec { let mut section = vec![]; section.extend_from_slice(&HEADER); for _i in 0..(SECTION_LENGTH - HEADER.len()) { diff --git a/src/skills.rs b/src/skills.rs index a658d4f..9b8de58 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -1,4 +1,4 @@ -use super::header::character::CharacterClass; +use crate::character::Class; const SECTION_HEADER: [u8; 2] = [0x69, 0x66]; const SECTION_BYTES: usize = 32; @@ -387,23 +387,20 @@ pub fn check_valid(byte_vector: &Vec) -> bool { } /// Converts the value from 0-30 to the one found in the game's file by adding an offset specific to each class. -fn get_offset(class: CharacterClass) -> usize { +fn get_offset(class: Class) -> usize { match class { - CharacterClass::Amazon => SKILL_OFFSET_AMAZON, - CharacterClass::Assassin => SKILL_OFFSET_ASSASSIN, - CharacterClass::Barbarian => SKILL_OFFSET_BARBARIAN, - CharacterClass::Druid => SKILL_OFFSET_DRUID, - CharacterClass::Necromancer => SKILL_OFFSET_NECROMANCER, - CharacterClass::Paladin => SKILL_OFFSET_PALADIN, - CharacterClass::Sorceress => SKILL_OFFSET_SORCERESS, + Class::Amazon => SKILL_OFFSET_AMAZON, + Class::Assassin => SKILL_OFFSET_ASSASSIN, + Class::Barbarian => SKILL_OFFSET_BARBARIAN, + Class::Druid => SKILL_OFFSET_DRUID, + Class::Necromancer => SKILL_OFFSET_NECROMANCER, + Class::Paladin => SKILL_OFFSET_PALADIN, + Class::Sorceress => SKILL_OFFSET_SORCERESS, } } /// Parse a vector of bytes containg a character's skill tree (starting with header 0x69 0x66) and returns a Skillset on success. -pub fn parse_skills( - byte_vector: &Vec, - class: CharacterClass, -) -> Result { +pub fn parse_skills(byte_vector: &Vec, class: Class) -> Result { let mut skills: Skillset = Skillset::default(); if !check_valid(byte_vector) { return Err( @@ -442,7 +439,7 @@ mod tests { 0x00, 0x00, 0x00, 0x14, ]; - let skills = parse_skills(&byte_vector, CharacterClass::Sorceress).unwrap(); + let skills = parse_skills(&byte_vector, Class::Sorceress).unwrap(); for i in 0..30 { if skills[i].name == "Teleport" { assert!(skills[i].id == 54 && skills[i].level == 1); @@ -462,7 +459,7 @@ mod tests { 0x00, 0x00, 0x00, 0x14, ]; - let skills = parse_skills(&byte_vector, CharacterClass::Sorceress).unwrap(); + let skills = parse_skills(&byte_vector, Class::Sorceress).unwrap(); // println!("{0:?}", skills); for skill in skills { if skill.name == "Ice Blast" { From 803a9e1e3f59b9ee501339e6620787d7a0b2b736 Mon Sep 17 00:00:00 2001 From: feored Date: Mon, 24 Jul 2023 04:47:46 +0200 Subject: [PATCH 2/9] Parse character information in header. --- src/attributes.rs | 2 - src/character/mercenary.rs | 125 +++++++------ src/character/mod.rs | 369 +++++++++++++++++++++++++++++++++---- src/lib.rs | 239 +++++++++++++++++++----- src/skills.rs | 2 +- src/utils.rs | 8 + 6 files changed, 603 insertions(+), 142 deletions(-) create mode 100644 src/utils.rs diff --git a/src/attributes.rs b/src/attributes.rs index ef27ed8..c43c142 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -2,7 +2,6 @@ use bit::BitIndex; use std::cmp; use std::fmt; -const OFFSET: usize = 765; const TRAILER: u32 = 0x1FF; const STAT_HEADER_LENGTH: usize = 9; @@ -355,7 +354,6 @@ pub fn parse_attributes(byte_vector: &Vec) -> Attributes { #[cfg(test)] mod tests { use super::*; - use bit::BitIndex; #[test] fn test_write_and_read_attributes() { diff --git a/src/character/mercenary.rs b/src/character/mercenary.rs index 8ec236f..69b5679 100644 --- a/src/character/mercenary.rs +++ b/src/character/mercenary.rs @@ -1,9 +1,7 @@ use crate::Difficulty; -use crate::GameError; +use crate::GameLogicError; use crate::ParseError; -const SECTION_NAME: &'static str = "Mercenary"; -const SECTION_LENGTH: usize = 14; const VARIANTS: &'static [Variant; 39] = &[ (Class::Rogue(Rogue::Fire), Difficulty::Normal), (Class::Rogue(Rogue::Cold), Difficulty::Normal), @@ -184,7 +182,7 @@ const BARBARIAN_NAMES: [&'static str; 67] = [ pub type Variant = (Class, Difficulty); #[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum Class { +pub enum Class { Rogue(Rogue), DesertMercenary(DesertMercenary), IronWolf(IronWolf), @@ -192,13 +190,13 @@ enum Class { } #[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum Rogue { +pub enum Rogue { Fire, Cold, } #[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum DesertMercenary { +pub enum DesertMercenary { Prayer, Defiance, BlessedAim, @@ -208,14 +206,14 @@ enum DesertMercenary { } #[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum IronWolf { +pub enum IronWolf { Fire, Cold, Lightning, } #[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum Barbarian { +pub enum Barbarian { Bash, Frenzy, } @@ -243,53 +241,52 @@ impl Default for Mercenary { } } -fn variant_id(variant: &Variant) -> Result{ - let mut variant_id : u16 = 99; - - for i in 0..VARIANTS.len(){ - if *variant == VARIANTS[i]{ +fn variant_id(variant: &Variant) -> Result { + let mut variant_id: u16 = 99; + + for i in 0..VARIANTS.len() { + if *variant == VARIANTS[i] { variant_id = i as u16; break; } } - if (variant_id as usize) > VARIANTS.len(){ - Err(GameError{message: format!("There is no mercenary ID for type {0:?} recruited in {1:?}", variant.0, variant.1)}) + if (variant_id as usize) > VARIANTS.len() { + Err(GameLogicError { + message: format!( + "There is no mercenary ID for type {0:?} recruited in {1:?}", + variant.0, variant.1 + ), + }) } else { Ok(variant_id) } } -fn names_list(class : Class) -> &'static [&'static str]{ - match class{ - Class::Rogue(_) => { - &ROGUE_NAMES - }, - Class::DesertMercenary(_) => { - &DESERTMERCENARY_NAMES - }, - Class::IronWolf(_) => { - &IRONWOLF_NAMES - }, - Class::Barbarian(_) => { - &BARBARIAN_NAMES - } +fn names_list(class: Class) -> &'static [&'static str] { + match class { + Class::Rogue(_) => &ROGUE_NAMES, + Class::DesertMercenary(_) => &DESERTMERCENARY_NAMES, + Class::IronWolf(_) => &IRONWOLF_NAMES, + Class::Barbarian(_) => &BARBARIAN_NAMES, } } -pub fn parse_mercenary(data: &[u8; 14]) -> Result { +pub fn parse(data: &[u8; 14]) -> Result { let mut mercenary: Mercenary = Mercenary::default(); if data[0..2] != [0x00, 0x00] { mercenary.dead = true; } mercenary.id = u32::from_le_bytes(<[u8; 4]>::try_from(&data[2..6]).unwrap()); - let variant_id = u16::from_le_bytes(<[u8; 2]>::try_from(&data[8..10]).unwrap()); + let variant_id: u16 = u16::from_le_bytes(<[u8; 2]>::try_from(&data[8..10]).unwrap()); mercenary.variant = VARIANTS[variant_id as usize]; - let name_id = u16::from_le_bytes(<[u8; 2]>::try_from(&data[6..8]).unwrap()); - let names_list = names_list(mercenary.variant.0); + let name_id: u16 = u16::from_le_bytes(<[u8; 2]>::try_from(&data[6..8]).unwrap()); + let names_list: &[&str] = names_list(mercenary.variant.0); if name_id as usize > names_list.len() { - return Err(ParseError{section: String::from(SECTION_NAME), message: format!("Found invalid name ID {} for mercenary", name_id)}); + return Err(ParseError { + message: format!("Found invalid name ID {} for mercenary", name_id), + }); } mercenary.name_id = name_id; mercenary.name = names_list[name_id as usize]; @@ -299,18 +296,18 @@ pub fn parse_mercenary(data: &[u8; 14]) -> Result { Ok(mercenary) } -pub fn generate_mercenary(mercenary: &Mercenary) -> Result<[u8;14], GameError>{ - let mut bytes : [u8; 14] = [0x00; 14]; +pub fn generate_mercenary(mercenary: &Mercenary) -> Result<[u8; 14], GameLogicError> { + let mut bytes: [u8; 14] = [0x00; 14]; bytes[0..2].clone_from_slice(match mercenary.dead { true => &[0x01, 0x00], - false => &[0x00, 0x00] + false => &[0x00, 0x00], }); bytes[2..6].clone_from_slice(&mercenary.id.to_le_bytes()); bytes[6..8].clone_from_slice(&mercenary.name_id.to_le_bytes()); - let variant_id = match variant_id(&mercenary.variant){ + let variant_id = match variant_id(&mercenary.variant) { Ok(id) => id, - Err(e) => return Err(e) + Err(e) => return Err(e), }; bytes[8..10].clone_from_slice(&variant_id.to_le_bytes()); @@ -322,30 +319,48 @@ pub fn generate_mercenary(mercenary: &Mercenary) -> Result<[u8;14], GameError>{ #[cfg(test)] mod tests { use super::*; - use bit::BitIndex; #[test] - fn parse_mercenary_test(){ - let expected_result = Mercenary{dead: false, id:3461679u32, name_id: 3, name: "Abhaya", variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), experience: 63722u32}; - let bytes = [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; - let mut parsed_result : Mercenary = Mercenary::default(); - match parse_mercenary(&bytes) { - Ok(res) => {parsed_result = res}, - Err(e) => {println!{"Test failed: {e:?}"}} + fn parse_test() { + let expected_result = Mercenary { + dead: false, + id: 3461679u32, + name_id: 3, + name: "Abhaya", + variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), + experience: 63722u32, + }; + let bytes = + [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; + let mut parsed_result: Mercenary = Mercenary::default(); + match parse(&bytes) { + Ok(res) => parsed_result = res, + Err(e) => { + println! {"Test failed: {e:?}"} + } }; assert_eq!(parsed_result, expected_result); } #[test] - fn generate_mercenary_test(){ - let expected_result = [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; - let merc = Mercenary{dead: false, id:3461679u32, name_id: 3, name: "Abhaya", variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), experience: 63722u32}; - let mut parsed_result : [u8; 14] = [0x00; 14]; + fn generate_mercenary_test() { + let expected_result = + [0x00, 0x00, 0x2F, 0xD2, 0x34, 0x00, 0x03, 0x00, 0x01, 0x00, 0xEA, 0xF8, 0x00, 0x00]; + let merc = Mercenary { + dead: false, + id: 3461679u32, + name_id: 3, + name: "Abhaya", + variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), + experience: 63722u32, + }; + let mut parsed_result: [u8; 14] = [0x00; 14]; match generate_mercenary(&merc) { - Ok(res) => {parsed_result = res}, - Err(e) => {println!{"Test failed: {e:?}"}} + Ok(res) => parsed_result = res, + Err(e) => { + println! {"Test failed: {e:?}"} + } }; assert_eq!(parsed_result, expected_result); } - -} \ No newline at end of file +} diff --git a/src/character/mod.rs b/src/character/mod.rs index ea81f1b..e5b56ab 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -1,15 +1,19 @@ +use crate::get_offset_from_position; +use crate::get_offset_range_from_position; +use crate::utils::get_sys_time_in_secs; use crate::Act; +use crate::Class; use crate::Difficulty; +use crate::GameLogicError; +use crate::OffsetID; +use crate::ParseError; +use bit::BitIndex; +use mercenary::Mercenary; +use std::str; pub mod mercenary; -const AMAZON: u8 = 0; -const SORCERESS: u8 = 1; -const NECROMANCER: u8 = 2; -const PALADIN: u8 = 3; -const BARBARIAN: u8 = 4; -const DRUID: u8 = 5; -const ASSASSIN: u8 = 6; +const SECTION_OFFSET: usize = 16; const TITLES_CLASSIC_STANDARD_MALE: [&'static str; 4] = ["", "Sir", "Lord", "Baron"]; const TITLES_CLASSIC_STANDARD_FEMALE: [&'static str; 4] = ["", "Dame", "Lady", "Baroness"]; @@ -23,13 +27,26 @@ const TITLES_LOD_HARDCORE_FEMALE: [&'static str; 4] = ["", "Destroyer", "Conquer #[derive(PartialEq, Eq, Debug)] pub struct Character { weapon_set: WeaponSet, - name: String, pub status: Status, pub progression: u8, + title: String, pub class: Class, level: u8, - difficulty: (Difficulty, Act), + pub last_played: u32, + assigned_skills: [u32; 16], + left_mouse_skill: u32, + right_mouse_skill: u32, + left_mouse_switch_skill: u32, + right_mouse_switch_skill: u32, + pub menu_appearance: [u8; 32], + pub difficulty: Difficulty, + pub act: Act, + pub map_seed: u32, + pub mercenary: Mercenary, + pub resurrected_menu_appearence: [u8; 48], + name: String, } + #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub struct Status { ladder: bool, @@ -41,18 +58,170 @@ pub struct Status { #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum WeaponSet { Main, - Secondary, + Switch, } -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum Class { - Amazon, - Sorceress, - Necromancer, - Paladin, - Barbarian, - Druid, - Assassin, +impl Default for Character { + fn default() -> Self { + Self { + weapon_set: WeaponSet::Main, + status: Status::default(), + progression: 0, + title: String::default(), + class: Class::Amazon, + level: 1, + last_played: get_sys_time_in_secs(), + assigned_skills: [0x00; 16], + left_mouse_skill: 0, + right_mouse_skill: 0, + left_mouse_switch_skill: 0, + right_mouse_switch_skill: 0, + menu_appearance: [0x00; 32], + difficulty: Difficulty::Normal, + act: Act::Act1, + map_seed: 0, + mercenary: Mercenary::default(), + resurrected_menu_appearence: [0x00; 48], + name: String::from("default"), + } + } +} + +fn parse_character(bytes: &[u8; 319]) -> Result { + let mut character: Character = Character::default(); + + let active_weapon = get_u32(bytes, OffsetID::WeaponSet); + character.weapon_set = WeaponSet::try_from(active_weapon)?; + character.status = + Status::from(bytes[get_offset_from_position(OffsetID::Status, SECTION_OFFSET)]); + character.progression = bytes[get_offset_from_position(OffsetID::Progression, SECTION_OFFSET)]; + + let class = Class::try_from(bytes[get_offset_from_position(OffsetID::Class, SECTION_OFFSET)])?; + + character.class = match class { + Class::Druid | Class::Assassin => { + if character.status.expansion { + class + } else { + return Err(ParseError { + message: format!( + "Found druid or assassin class ({0:?})set in non expansion character.", + class + ), + }); + } + } + _ => class, + }; + + let level = bytes[get_offset_from_position(OffsetID::Level, SECTION_OFFSET)]; + character.level = match level { + 0u8 | 100u8..=255u8 => { + return Err(ParseError { + message: format!( + "Found character level outside of 1-99 range : {0:?}.", + level + ), + }) + } + _ => level, + }; + + character.last_played = get_u32(bytes, OffsetID::LastPlayedDate); + let assigned_skills = + &bytes[get_offset_range_from_position(OffsetID::AssignedSkills, SECTION_OFFSET)]; + for i in 0..16 { + let start = i * 4; + let assigned_skill = + u32::from_le_bytes(assigned_skills[start..start + 4].try_into().unwrap()); + character.assigned_skills[i] = assigned_skill; + } + + character.left_mouse_skill = get_u32(bytes, OffsetID::LeftMouseSkill); + character.right_mouse_skill = get_u32(bytes, OffsetID::RightMouseSkill); + character.left_mouse_switch_skill = get_u32(bytes, OffsetID::LeftMouseSwitchSkill); + character.right_mouse_switch_skill = get_u32(bytes, OffsetID::RightMouseSwitchSkill); + let last_act = parse_last_act( + &bytes[get_offset_range_from_position(OffsetID::Difficulty, SECTION_OFFSET)] + .try_into() + .unwrap(), + ); + + character.menu_appearance.clone_from_slice( + &bytes[get_offset_range_from_position(OffsetID::MenuAppearance, SECTION_OFFSET)], + ); + + match last_act { + Ok(last_act) => { + character.difficulty = last_act.0; + character.act = last_act.1; + } + Err(e) => return Err(e), + }; + + character.map_seed = get_u32(bytes, OffsetID::MapSeed); + character.mercenary = mercenary::parse( + &bytes[get_offset_range_from_position(OffsetID::Mercenary, SECTION_OFFSET)] + .try_into() + .unwrap(), + )?; + character.resurrected_menu_appearence.clone_from_slice( + &bytes[get_offset_range_from_position(OffsetID::ResurrectedMenuAppearance, SECTION_OFFSET)], + ); + + let utf8name = match str::from_utf8( + &bytes[get_offset_range_from_position(OffsetID::Name, SECTION_OFFSET)], + ) { + Ok(res) => res.trim_matches(char::from(0)), + Err(e) => { + return Err(ParseError { + message: format!("Invalid utf-8 for character name: {0:?}", e), + }); + } + }; + character.name = String::from(utf8name); + + character.title = character.title(); + + Ok(character) +} + +fn parse_last_act(bytes: &[u8; 3]) -> Result<(Difficulty, Act), ParseError> { + let mut last_act = (Difficulty::Normal, Act::Act1); + let mut index = 0; + if bytes[0] != 0x00 { + last_act.0 = Difficulty::Normal; + } else if bytes[1] != 0x00 { + last_act.0 = Difficulty::Nightmare; + index = 1; + } else if bytes[2] != 0x00 { + last_act.0 = Difficulty::Hell; + index = 2; + } else { + return Err(ParseError { + message: String::from("Couldn't read current difficulty, all 0."), + }); + } + + last_act.1 = Act::try_from(bytes[index])?; + + Ok(last_act) +} + +fn generate_last_act(difficulty: Difficulty, act: Act) -> [u8; 3] { + let mut bytes: [u8; 3] = [0x00; 3]; + match difficulty { + Difficulty::Normal => { + bytes[0] = u8::from(act); + } + Difficulty::Nightmare => { + bytes[1] = u8::from(act); + } + Difficulty::Hell => { + bytes[2] = u8::from(act); + } + } + bytes } impl Default for Status { @@ -66,16 +235,51 @@ impl Default for Status { } } -impl Default for Character { - fn default() -> Self { - Self { - weapon_set: WeaponSet::Main, - name: String::from(""), - status: Status::default(), - progression: 0, - class: Class::Amazon, - level: 1, - difficulty: (Difficulty::Normal, Act::Act1), +impl From for Status { + fn from(byte: u8) -> Status { + let mut status = Status::default(); + status.hardcore = byte.bit(2); + status.died = byte.bit(3); + status.expansion = byte.bit(5); + status.ladder = byte.bit(6); + status + } +} + +impl From for u8 { + fn from(status: Status) -> u8 { + let mut result = 0u8; + result.set_bit(2, status.hardcore); + result.set_bit(3, status.died); + result.set_bit(5, status.expansion); + result.set_bit(6, status.ladder); + result + } +} + +impl TryFrom for WeaponSet { + type Error = ParseError; + fn try_from(value: u32) -> Result { + match value { + 0u32 => Ok(WeaponSet::Main), + 1u32 => Ok(WeaponSet::Switch), + _ => { + return Err(ParseError { + message: format!( + "Found {0:?} instead of 0 or 1 in current active weapons.", + value + ), + }); + } + } + } +} + +impl From for u32 { + fn from(weapon_set: WeaponSet) -> u32 { + match weapon_set { + WeaponSet::Main => 0u32, + WeaponSet::Switch => 1u32, } } } @@ -114,16 +318,16 @@ impl Character { } } - pub fn difficulty(&self) -> &(Difficulty, Act) { - &self.difficulty - } - pub fn set_difficulty(&mut self, new_difficulty: (Difficulty, Act)) { - if new_difficulty.1 == Act::Act5 && !self.status.expansion { - return; - } - //TODO: set progression accordingly - self.difficulty = new_difficulty - } + // pub fn difficulty(&self) -> &(Difficulty, Act) { + // &self.difficulty + // } + // pub fn set_difficulty(&mut self, new_difficulty: (Difficulty, Act)) { + // if new_difficulty.1 == Act::Act5 && !self.status.expansion { + // return; + // } + // //TODO: set progression accordingly + // self.difficulty = new_difficulty + // } } impl Character { @@ -166,3 +370,90 @@ impl Character { } } } + +fn get_u32(bytes: &[u8], id: OffsetID) -> u32 { + u32::from_le_bytes( + bytes[get_offset_range_from_position(id, SECTION_OFFSET)] + .try_into() + .unwrap(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_character() -> () { + let bytes: [u8; 319] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, + 0x00, 0x00, 0x00, 0x00, 0xBB, 0x29, 0xBD, 0x64, 0xFF, 0xFF, 0xFF, 0xFF, 0x28, 0x00, + 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, + 0x2B, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x9B, 0x00, + 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x39, 0x03, 0x02, 0x02, 0x02, 0x35, + 0xFF, 0x51, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x80, 0x43, 0x2D, 0x95, 0x53, 0x00, 0x00, 0x00, 0x00, 0x19, 0x50, 0x40, 0x5C, 0x07, + 0x00, 0x23, 0x00, 0xD6, 0x9B, 0x19, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0x62, 0x61, 0x20, 0xFF, 0x07, 0x1C, + 0x01, 0x04, 0x00, 0x00, 0x00, 0x75, 0x69, 0x74, 0x20, 0xFF, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x78, 0x70, 0x6C, 0x20, 0xFF, 0x07, 0xD9, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x75, 0x61, 0x70, 0x20, 0x4D, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, + 0x79, 0x61, 0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let expected_result = Character { + weapon_set: WeaponSet::Main, + status: Status { + expansion: true, + hardcore: false, + ladder: false, + died: false, + }, + progression: 15, + title: String::from("Matriarch"), + class: Class::Sorceress, + level: 92, + last_played: get_sys_time_in_secs(), + assigned_skills: [0x00; 16], + left_mouse_skill: 0, + right_mouse_skill: 0, + left_mouse_switch_skill: 0, + right_mouse_switch_skill: 0, + menu_appearance: [0x00; 32], + difficulty: Difficulty::Hell, + act: Act::Act1, + map_seed: 1402285379, + mercenary: Mercenary::default(), + resurrected_menu_appearence: [0x00; 48], + name: String::from("Nyahallo"), + }; + let parsed_result = match parse_character(&bytes) { + Ok(result) => result, + Err(e) => { + println!("{e:?}"); + assert_eq!(false, true); + return; + } + }; + // println!("{0:?}", parsed_result); + assert_eq!(parsed_result.level, expected_result.level); + assert_eq!(parsed_result.class, expected_result.class); + assert_eq!(parsed_result.weapon_set, expected_result.weapon_set); + assert_eq!(parsed_result.map_seed, expected_result.map_seed); + assert_eq!(parsed_result.name, expected_result.name); + assert_eq!(parsed_result.act, expected_result.act); + assert_eq!(parsed_result.difficulty, expected_result.difficulty); + assert_eq!(parsed_result.progression, expected_result.progression); + assert_eq!(parsed_result.title, expected_result.title); + } +} diff --git a/src/lib.rs b/src/lib.rs index de12b53..1ac5423 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,9 @@ variant_size_differences )] +use bit::BitIndex; use std::fmt; +use std::ops::Range; pub mod attributes; pub mod character; @@ -18,6 +20,7 @@ pub mod items; pub mod npcs; pub mod quests; pub mod skills; +pub mod utils; pub mod waypoints; const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; @@ -33,22 +36,17 @@ const VERSION_D2R_250: u32 = 99; #[derive(Debug, Clone)] pub struct ParseError { - section: String, message: String, } #[derive(Debug, Clone)] -pub struct GameError{ - message: String +pub struct GameLogicError { + message: String, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Error parsing section '{}': {}", - self.section, self.message - ) + write!(f, "Parsing error: {}", self.message) } } @@ -58,7 +56,7 @@ pub struct Save { } #[derive(Debug)] -pub enum HeaderID { +pub enum OffsetID { Signature, VersionID, FileSize, @@ -68,19 +66,22 @@ pub enum HeaderID { Progression, Class, Level, - CreatedDate, LastPlayedDate, AssignedSkills, - LeftMouse, - RightMouse, - LeftMouseSwitch, - RightMouseSwitch, - MenuAppearance, //Needed? + LeftMouseSkill, + RightMouseSkill, + LeftMouseSwitchSkill, + RightMouseSwitchSkill, + MenuAppearance, Difficulty, MapSeed, Mercenary, ResurrectedMenuAppearance, Name, + Quests, + Waypoints, + NPCs, + Attributes, } #[derive(PartialEq, Eq, Debug)] @@ -95,6 +96,7 @@ pub enum Version { V250R, } +#[derive(PartialEq, Eq, Debug)] struct FileSection { offset: usize, bytes: usize, @@ -116,70 +118,217 @@ pub enum Act { Act5, } -fn get_file_bytes_range(id: HeaderID) -> (usize, usize) { +impl TryFrom for Act { + type Error = ParseError; + fn try_from(byte: u8) -> Result { + let mut relevant_bits: u8 = 0; + relevant_bits.set_bit_range(0..3, byte.bit_range(0..3)); + match relevant_bits { + 0x00 => Ok(Act::Act1), + 0x01 => Ok(Act::Act2), + 0x02 => Ok(Act::Act3), + 0x03 => Ok(Act::Act4), + 0x04 => Ok(Act::Act5), + _ => Err(ParseError { + message: format!("Found invalid act: {0:?}.", byte), + }), + } + } +} + +impl From for u8 { + fn from(act: Act) -> u8 { + match act { + Act::Act1 => 0x00, + Act::Act2 => 0x01, + Act::Act3 => 0x02, + Act::Act4 => 0x03, + Act::Act5 => 0x04, + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Class { + Amazon, + Sorceress, + Necromancer, + Paladin, + Barbarian, + Druid, + Assassin, +} + +impl TryFrom for Class { + type Error = ParseError; + fn try_from(byte: u8) -> Result { + match byte { + 0x00 => Ok(Class::Amazon), + 0x01 => Ok(Class::Sorceress), + 0x02 => Ok(Class::Necromancer), + 0x03 => Ok(Class::Paladin), + 0x04 => Ok(Class::Barbarian), + 0x05 => Ok(Class::Druid), + 0x06 => Ok(Class::Assassin), + _ => Err(ParseError { + message: format!("Found invalid class: {0:?}.", byte), + }), + } + } +} + +impl From for u8 { + fn from(class: Class) -> u8 { + match class { + Class::Amazon => 0x00, + Class::Sorceress => 0x01, + Class::Necromancer => 0x02, + Class::Paladin => 0x03, + Class::Barbarian => 0x04, + Class::Druid => 0x05, + Class::Assassin => 0x06, + } + } +} + +pub fn get_offset_from_position(id: OffsetID, start: usize) -> usize { let data: FileSection = get_file_data(id); - (data.offset, data.offset + data.bytes) + if start > data.offset { + panic!("Start is after offset!"); + } + data.offset - start +} + +pub fn get_offset_range_from_position(id: OffsetID, start: usize) -> Range { + let data: FileSection = get_file_data(id); + if start > data.offset { + panic!("Start is after offset!"); + } + (data.offset - start)..(data.offset + data.bytes - start) +} + +pub fn get_offset_range(id: OffsetID) -> Range { + get_offset_range_from_position(id, 0) } -fn get_file_data(id: HeaderID) -> FileSection { +fn get_file_data(id: OffsetID) -> FileSection { match id { - HeaderID::Signature => FileSection { + OffsetID::Signature => FileSection { offset: 0, bytes: 4, }, - HeaderID::VersionID => FileSection { + OffsetID::VersionID => FileSection { offset: 4, bytes: 4, }, - HeaderID::FileSize => FileSection { + OffsetID::FileSize => FileSection { offset: 8, bytes: 4, }, - HeaderID::Checksum => FileSection { + OffsetID::Checksum => FileSection { offset: 12, bytes: 4, }, - HeaderID::WeaponSet => FileSection { + OffsetID::WeaponSet => FileSection { offset: 16, bytes: 4, }, - HeaderID::Status => FileSection { + OffsetID::Status => FileSection { offset: 36, bytes: 1, }, - HeaderID::Progression => FileSection { + OffsetID::Progression => FileSection { offset: 37, bytes: 1, }, - HeaderID::Class => FileSection { + OffsetID::Class => FileSection { offset: 40, bytes: 1, }, - HeaderID::Level => FileSection { + OffsetID::Level => FileSection { offset: 43, bytes: 1, }, - _ => FileSection { - offset: 0, - bytes: 0 - } + OffsetID::LastPlayedDate => FileSection { + offset: 48, + bytes: 4, + }, + OffsetID::AssignedSkills => FileSection { + offset: 56, + bytes: 64, + }, + OffsetID::LeftMouseSkill => FileSection { + offset: 120, + bytes: 4, + }, + OffsetID::RightMouseSkill => FileSection { + offset: 124, + bytes: 4, + }, + OffsetID::LeftMouseSwitchSkill => FileSection { + offset: 128, + bytes: 4, + }, + OffsetID::RightMouseSwitchSkill => FileSection { + offset: 132, + bytes: 4, + }, + OffsetID::MenuAppearance => FileSection { + offset: 136, + bytes: 32, + }, + OffsetID::Difficulty => FileSection { + offset: 168, + bytes: 3, + }, + OffsetID::MapSeed => FileSection { + offset: 171, + bytes: 4, + }, + OffsetID::Mercenary => FileSection { + offset: 177, + bytes: 14, + }, + OffsetID::ResurrectedMenuAppearance => FileSection { + offset: 249, + bytes: 48, + }, + OffsetID::Name => FileSection { + offset: 267, + bytes: 16, + }, + OffsetID::Quests => FileSection { + offset: 335, + bytes: 298, + }, + OffsetID::Waypoints => FileSection { + offset: 633, + bytes: 80, + }, + OffsetID::NPCs => FileSection { + offset: 713, + bytes: 52, + }, + OffsetID::Attributes => FileSection { + offset: 765, + bytes: 0, + }, } } fn check_valid_signature(bytes: &Vec) -> bool { - let (header_start, header_end) = get_file_bytes_range(HeaderID::Signature); - bytes[header_start..header_end] == SIGNATURE + bytes[get_offset_range(OffsetID::Signature)] == SIGNATURE } -pub fn calc_checksum(bytes: &Vec) -> i32 { - let mut checksum: i32 = 0; - let (checksum_start, checksum_end) = get_file_bytes_range(HeaderID::Checksum); - for i in 0..bytes.len() { - let mut ch: i32 = bytes[i] as i32; - if i >= checksum_start && i < checksum_end { - ch = 0; - } - checksum = (checksum << 1) + ch + ((checksum < 0) as i32); - } - checksum -} +// pub fn calc_checksum(bytes: &Vec) -> i32 { +// let mut checksum: i32 = 0; +// let (checksum_start, checksum_end) = get_file_bytes_range(OffsetID::Checksum); +// for i in 0..bytes.len() { +// let mut ch: i32 = bytes[i] as i32; +// if i >= checksum_start && i < checksum_end { +// ch = 0; +// } +// checksum = (checksum << 1) + ch + ((checksum < 0) as i32); +// } +// checksum +// } diff --git a/src/skills.rs b/src/skills.rs index 9b8de58..e86a611 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -1,4 +1,4 @@ -use crate::character::Class; +use crate::Class; const SECTION_HEADER: [u8; 2] = [0x69, 0x66]; const SECTION_BYTES: usize = 32; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b4b4dd4 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,8 @@ +use std::time::SystemTime; + +pub fn get_sys_time_in_secs() -> u32 { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs() as u32, + Err(_) => panic!("SystemTime before UNIX EPOCH!"), + } +} From 6b1ad365591bf59bb37d4e9cac7eacc3224bce7b Mon Sep 17 00:00:00 2001 From: feored Date: Mon, 24 Jul 2023 21:53:20 +0200 Subject: [PATCH 3/9] Implement interfaces for parsing quests and add u16_from, u32_from functions. --- notes.md | 10 +++ src/attributes.rs | 2 +- src/character/mod.rs | 114 ++++++++++++++++++-------- src/lib.rs | 142 ++------------------------------- src/quests.rs | 186 +++++++++++++++++++++++++++++++++++++++---- src/utils.rs | 33 ++++++++ 6 files changed, 303 insertions(+), 184 deletions(-) diff --git a/notes.md b/notes.md index 8a62989..7744f5d 100644 --- a/notes.md +++ b/notes.md @@ -144,3 +144,13 @@ CSvBits# is col 9 | Experience | 13 | 1 | 0 | 32 | | Gold (Inventory) | 14 | 1 | 0 | 25 | | Gold (Stash) | 15 | 1 | 0 | 25 | + + +## Quests + +Ex: Den of Evil +Quest not started: 0x00 0x00 => 0000 0000 0000 0000 +Quest started (Talked to Akara): 0x04 0x00 => 0000 0000 0000 0100 +Cleared Den of Evil (Return to Akara for reward): 0x1C 0x00 => 0000 0000 0001 1100 +Talked to Akara (Completed quest): 0x01 0x30 => 0011 0000 0000 0001 +Used skill point: 0x01 0x10 => 0001 0000 0000 0001 \ No newline at end of file diff --git a/src/attributes.rs b/src/attributes.rs index c43c142..67fbe20 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -90,7 +90,7 @@ impl fmt::Debug for FixedPointStat { /// Representation of a character's attributes. /// -/// Can be serialized into a vector of u8 using Vec::from(). +/// Can be serialized into a vector of u8 using `Vec::from()`. /// Values can contain up to 32 bits (experience). /// Certain values are fixed point and stored with integer and /// fraction separately for precision and easier comparison. diff --git a/src/character/mod.rs b/src/character/mod.rs index e5b56ab..ff14243 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -1,19 +1,24 @@ -use crate::get_offset_from_position; -use crate::get_offset_range_from_position; -use crate::utils::get_sys_time_in_secs; +use std::ops::Range; +use std::str; + +use bit::BitIndex; + use crate::Act; use crate::Class; use crate::Difficulty; use crate::GameLogicError; -use crate::OffsetID; use crate::ParseError; -use bit::BitIndex; + +use crate::utils::get_sys_time_in_secs; +use crate::utils::FileSection; +use crate::utils::u32_from; +use crate::utils::u8_from; + use mercenary::Mercenary; -use std::str; + pub mod mercenary; -const SECTION_OFFSET: usize = 16; const TITLES_CLASSIC_STANDARD_MALE: [&'static str; 4] = ["", "Sir", "Lord", "Baron"]; const TITLES_CLASSIC_STANDARD_FEMALE: [&'static str; 4] = ["", "Dame", "Lady", "Baroness"]; @@ -24,6 +29,51 @@ const TITLES_LOD_STANDARD_FEMALE: [&'static str; 4] = ["", "Slayer", "Champion", const TITLES_LOD_HARDCORE_MALE: [&'static str; 4] = ["", "Destroyer", "Conqueror", "Guardian"]; const TITLES_LOD_HARDCORE_FEMALE: [&'static str; 4] = ["", "Destroyer", "Conqueror", "Guardian"]; +#[derive(PartialEq, Eq, Debug)] +enum Section { + WeaponSet, + Status, + Progression, + Class, + Level, + LastPlayed, + AssignedSkills, + LeftMouseSkill, + RightMouseSkill, + LeftMouseSwitchSkill, + RightMouseSwitchSkill, + MenuAppearance, + Difficulty, + MapSeed, + Mercenary, + ResurrectedMenuAppearance, + Name +} + +impl From
for FileSection { + fn from(section: Section) -> FileSection { + match section { + Section::WeaponSet => FileSection { offset: 0, bytes: 4 }, + Section::Status => FileSection { offset: 20, bytes: 1 }, + Section::Progression => FileSection { offset: 21, bytes: 1 }, + Section::Class => FileSection { offset: 26, bytes: 1 }, + Section::Level => FileSection { offset: 27, bytes: 1 }, + Section::LastPlayed => FileSection { offset: 32, bytes: 4 }, + Section::AssignedSkills => FileSection { offset: 40, bytes: 64 }, + Section::LeftMouseSkill => FileSection { offset: 104, bytes: 4 }, + Section::RightMouseSkill => FileSection { offset: 108, bytes: 4 }, + Section::LeftMouseSwitchSkill => FileSection { offset: 112, bytes: 4 }, + Section::RightMouseSwitchSkill => FileSection { offset: 116, bytes: 4 }, + Section::MenuAppearance => FileSection { offset: 120, bytes: 32 }, + Section::Difficulty => FileSection { offset: 152, bytes: 3 }, + Section::MapSeed => FileSection { offset: 155, bytes: 4 }, + Section::Mercenary=> FileSection { offset: 161, bytes: 14 }, + Section::ResurrectedMenuAppearance => FileSection { offset: 203, bytes: 48 }, + Section::Name => FileSection { offset: 251, bytes: 16 }, + } + } +} + #[derive(PartialEq, Eq, Debug)] pub struct Character { weapon_set: WeaponSet, @@ -90,13 +140,14 @@ impl Default for Character { fn parse_character(bytes: &[u8; 319]) -> Result { let mut character: Character = Character::default(); - let active_weapon = get_u32(bytes, OffsetID::WeaponSet); + let active_weapon = u32_from(&bytes[Range::::from(FileSection::from(Section::WeaponSet))]); character.weapon_set = WeaponSet::try_from(active_weapon)?; - character.status = - Status::from(bytes[get_offset_from_position(OffsetID::Status, SECTION_OFFSET)]); - character.progression = bytes[get_offset_from_position(OffsetID::Progression, SECTION_OFFSET)]; - let class = Class::try_from(bytes[get_offset_from_position(OffsetID::Class, SECTION_OFFSET)])?; + character.status = Status::from(u8_from(&bytes[Range::::from(FileSection::from(Section::Status))])); + + character.progression = u8_from(&bytes[Range::::from(FileSection::from(Section::Progression))]); + + let class = Class::try_from(u8_from(&bytes[Range::::from(FileSection::from(Section::Class))]))?; character.class = match class { Class::Druid | Class::Assassin => { @@ -114,7 +165,7 @@ fn parse_character(bytes: &[u8; 319]) -> Result { _ => class, }; - let level = bytes[get_offset_from_position(OffsetID::Level, SECTION_OFFSET)]; + let level = u8_from(&bytes[Range::::from(FileSection::from(Section::Level))]); character.level = match level { 0u8 | 100u8..=255u8 => { return Err(ParseError { @@ -127,9 +178,8 @@ fn parse_character(bytes: &[u8; 319]) -> Result { _ => level, }; - character.last_played = get_u32(bytes, OffsetID::LastPlayedDate); - let assigned_skills = - &bytes[get_offset_range_from_position(OffsetID::AssignedSkills, SECTION_OFFSET)]; + character.last_played = u32_from(&bytes[Range::::from(FileSection::from(Section::LastPlayed))]); + let assigned_skills = &bytes[Range::::from(FileSection::from(Section::AssignedSkills))]; for i in 0..16 { let start = i * 4; let assigned_skill = @@ -137,18 +187,19 @@ fn parse_character(bytes: &[u8; 319]) -> Result { character.assigned_skills[i] = assigned_skill; } - character.left_mouse_skill = get_u32(bytes, OffsetID::LeftMouseSkill); - character.right_mouse_skill = get_u32(bytes, OffsetID::RightMouseSkill); - character.left_mouse_switch_skill = get_u32(bytes, OffsetID::LeftMouseSwitchSkill); - character.right_mouse_switch_skill = get_u32(bytes, OffsetID::RightMouseSwitchSkill); + character.left_mouse_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))]); + character.right_mouse_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSkill))]); + character.left_mouse_switch_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))]); + character.right_mouse_switch_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))]); + let last_act = parse_last_act( - &bytes[get_offset_range_from_position(OffsetID::Difficulty, SECTION_OFFSET)] + &bytes[Range::::from(FileSection::from(Section::Difficulty))] .try_into() .unwrap(), ); character.menu_appearance.clone_from_slice( - &bytes[get_offset_range_from_position(OffsetID::MenuAppearance, SECTION_OFFSET)], + &bytes[Range::::from(FileSection::from(Section::MenuAppearance))] ); match last_act { @@ -159,18 +210,19 @@ fn parse_character(bytes: &[u8; 319]) -> Result { Err(e) => return Err(e), }; - character.map_seed = get_u32(bytes, OffsetID::MapSeed); + character.map_seed = u32_from(&bytes[Range::::from(FileSection::from(Section::MapSeed))]); character.mercenary = mercenary::parse( - &bytes[get_offset_range_from_position(OffsetID::Mercenary, SECTION_OFFSET)] + &bytes[Range::::from(FileSection::from(Section::Mercenary))] .try_into() .unwrap(), )?; + character.resurrected_menu_appearence.clone_from_slice( - &bytes[get_offset_range_from_position(OffsetID::ResurrectedMenuAppearance, SECTION_OFFSET)], + &bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))] ); let utf8name = match str::from_utf8( - &bytes[get_offset_range_from_position(OffsetID::Name, SECTION_OFFSET)], + &bytes[Range::::from(FileSection::from(Section::Name))] ) { Ok(res) => res.trim_matches(char::from(0)), Err(e) => { @@ -371,20 +423,14 @@ impl Character { } } -fn get_u32(bytes: &[u8], id: OffsetID) -> u32 { - u32::from_le_bytes( - bytes[get_offset_range_from_position(id, SECTION_OFFSET)] - .try_into() - .unwrap(), - ) -} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_parse_character() -> () { + fn test_parse_character() { let bytes: [u8; 319] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, diff --git a/src/lib.rs b/src/lib.rs index 1ac5423..35a9f9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,6 @@ use bit::BitIndex; use std::fmt; -use std::ops::Range; pub mod attributes; pub mod character; @@ -96,21 +95,17 @@ pub enum Version { V250R, } -#[derive(PartialEq, Eq, Debug)] -struct FileSection { - offset: usize, - bytes: usize, -} - -#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Difficulty { + #[default] Normal, Nightmare, Hell, } -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Act { + #[default] Act1, Act2, Act3, @@ -191,134 +186,11 @@ impl From for u8 { } } -pub fn get_offset_from_position(id: OffsetID, start: usize) -> usize { - let data: FileSection = get_file_data(id); - if start > data.offset { - panic!("Start is after offset!"); - } - data.offset - start -} -pub fn get_offset_range_from_position(id: OffsetID, start: usize) -> Range { - let data: FileSection = get_file_data(id); - if start > data.offset { - panic!("Start is after offset!"); - } - (data.offset - start)..(data.offset + data.bytes - start) -} -pub fn get_offset_range(id: OffsetID) -> Range { - get_offset_range_from_position(id, 0) -} - -fn get_file_data(id: OffsetID) -> FileSection { - match id { - OffsetID::Signature => FileSection { - offset: 0, - bytes: 4, - }, - OffsetID::VersionID => FileSection { - offset: 4, - bytes: 4, - }, - OffsetID::FileSize => FileSection { - offset: 8, - bytes: 4, - }, - OffsetID::Checksum => FileSection { - offset: 12, - bytes: 4, - }, - OffsetID::WeaponSet => FileSection { - offset: 16, - bytes: 4, - }, - OffsetID::Status => FileSection { - offset: 36, - bytes: 1, - }, - OffsetID::Progression => FileSection { - offset: 37, - bytes: 1, - }, - OffsetID::Class => FileSection { - offset: 40, - bytes: 1, - }, - OffsetID::Level => FileSection { - offset: 43, - bytes: 1, - }, - OffsetID::LastPlayedDate => FileSection { - offset: 48, - bytes: 4, - }, - OffsetID::AssignedSkills => FileSection { - offset: 56, - bytes: 64, - }, - OffsetID::LeftMouseSkill => FileSection { - offset: 120, - bytes: 4, - }, - OffsetID::RightMouseSkill => FileSection { - offset: 124, - bytes: 4, - }, - OffsetID::LeftMouseSwitchSkill => FileSection { - offset: 128, - bytes: 4, - }, - OffsetID::RightMouseSwitchSkill => FileSection { - offset: 132, - bytes: 4, - }, - OffsetID::MenuAppearance => FileSection { - offset: 136, - bytes: 32, - }, - OffsetID::Difficulty => FileSection { - offset: 168, - bytes: 3, - }, - OffsetID::MapSeed => FileSection { - offset: 171, - bytes: 4, - }, - OffsetID::Mercenary => FileSection { - offset: 177, - bytes: 14, - }, - OffsetID::ResurrectedMenuAppearance => FileSection { - offset: 249, - bytes: 48, - }, - OffsetID::Name => FileSection { - offset: 267, - bytes: 16, - }, - OffsetID::Quests => FileSection { - offset: 335, - bytes: 298, - }, - OffsetID::Waypoints => FileSection { - offset: 633, - bytes: 80, - }, - OffsetID::NPCs => FileSection { - offset: 713, - bytes: 52, - }, - OffsetID::Attributes => FileSection { - offset: 765, - bytes: 0, - }, - } -} - -fn check_valid_signature(bytes: &Vec) -> bool { - bytes[get_offset_range(OffsetID::Signature)] == SIGNATURE -} +// fn check_valid_signature(bytes: &Vec) -> bool { +// bytes[get_offset_range(OffsetID::Signature)] == SIGNATURE +// } // pub fn calc_checksum(bytes: &Vec) -> i32 { // let mut checksum: i32 = 0; diff --git a/src/quests.rs b/src/quests.rs index 9696131..0a7afdc 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -1,21 +1,179 @@ -pub const OFFSET: usize = 335; -const HEADER: [u8; 10] = [0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01]; -// Woo! + unknown -const SECTION_LENGTH: usize = 298; +use std::ops::Range; +use std::str; -pub struct QuestSet {} +use bit::BitIndex; +use crate::Act; +use crate::Class; +use crate::Difficulty; +use crate::GameLogicError; +use crate::ParseError; + +use crate::utils::FileSection; +use crate::utils::u32_from; +use crate::utils::u16_from; +use crate::utils::u8_from; + +const SECTION_HEADER: [u8; 10] = [0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01]; + + +#[derive(PartialEq, Eq, Debug)] +enum Section { + Act1Introduction, + Act1Quests, + Act2Travel, + Act2Introduction, + Act2Quests, + Act3Travel, + Act3Introduction, + Act3Quests, + Act4Travel, + Act4Introduction, + Act4Quests, + Act5Travel, + BaseGameComplete, + Act5Quests, + ResetStats, + DifficultyComplete +} + +impl From
for FileSection { + fn from(section: Section) -> FileSection { + match section { + Section::Act1Introduction => FileSection {offset: 0, bytes: 2}, + Section::Act1Quests => FileSection {offset: 2, bytes: 12}, + Section::Act2Travel => FileSection {offset: 14, bytes: 2}, + Section::Act2Introduction => FileSection {offset: 16, bytes: 2}, + Section::Act2Quests => FileSection {offset: 18, bytes: 12}, + Section::Act3Travel => FileSection {offset: 30, bytes: 2}, + Section::Act3Introduction => FileSection {offset: 32, bytes: 2}, + Section::Act3Quests => FileSection {offset: 34, bytes: 12}, + Section::Act4Travel => FileSection {offset: 46, bytes: 2}, + Section::Act4Introduction => FileSection {offset: 48, bytes: 2}, + Section::Act4Quests => FileSection {offset: 50, bytes: 12}, + Section::Act5Travel => FileSection {offset: 62, bytes: 2}, + Section::BaseGameComplete => FileSection {offset: 64, bytes: 2}, + Section::Act5Quests => FileSection {offset: 70, bytes: 12}, + Section::ResetStats => FileSection {offset: 82, bytes: 1}, + Section::DifficultyComplete => FileSection {offset: 83, bytes: 1} + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Stage { + Completed, + RequirementsMet, + Started, + Closed, + CompletedInGame +} + +#[derive(PartialEq, Eq, Debug, Default, Clone, Copy)] +pub struct Quest { + id: usize, + flags: u16, + act: Act, + difficulty: Difficulty, +} + +#[derive(PartialEq, Eq, Debug, Default)] +pub struct Quests{ + pub normal: QuestSet, + pub nightmare: QuestSet, + pub hell: QuestSet +} + +#[derive(PartialEq, Eq, Debug, Default)] +pub struct QuestSet{ + pub flags: QuestFlags, + pub quests: [Quest; 27] +} + +#[derive(PartialEq, Eq, Debug, Default)] pub struct QuestFlags { - pub warriv_introduction: u16, - pub warriv_travel: u16, - pub jerhyn_introduction: u16, + pub act_1_introduction: u16, + pub act_2_travel: u16, + pub act_2_introduction: u16, + pub act_3_travel: u16, + pub act_3_introduction: u16, + pub act_4_travel: u16, + pub act_4_introduction: u16, + pub act_5_travel: u16, + pub completed_base_game: u16, + pub reset_stats: u8, + pub completed_difficulty : u8 +} + +impl Quest { + fn set_stage(&mut self, stage: Stage, value: bool){ + self.flags.set_bit(usize::from(stage), value); + } + fn finish(&mut self) { + self.set_stage(Stage::Completed, true); + self.set_stage(Stage::Closed, true); + } +} + +impl From for usize { + fn from(stage:Stage) -> usize { + match stage{ + Stage::Completed => 0, + Stage::RequirementsMet => 1, + Stage::Started => 2, + Stage::Closed => 12, + Stage::CompletedInGame => 13 + } + } +} + +fn parse_flags(bytes: &[u8;96]) -> Result{ + let mut flags : QuestFlags = QuestFlags::default(); + flags.act_1_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act1Introduction))]); + flags.act_2_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Travel))]); + flags.act_2_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Introduction))]); + flags.act_3_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Travel))]); + flags.act_3_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Introduction))]); + flags.act_4_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Travel))]); + flags.act_4_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Introduction))]); + flags.act_5_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act5Travel))]); + flags.completed_base_game = u16_from(&bytes[Range::::from(FileSection::from(Section::BaseGameComplete))]); + flags.reset_stats = u8_from(&bytes[Range::::from(FileSection::from(Section::ResetStats))]); + flags.completed_difficulty = u8_from(&bytes[Range::::from(FileSection::from(Section::DifficultyComplete))]); + Ok(flags) +} + +fn parse_quests(bytes: &[u8;96]) -> Result<[Quest; 27], ParseError>{ + let mut quests : [Quest; 27] = [Quest::default();27]; + + + + Ok(quests) } -pub fn generate_section() -> Vec { - let mut section = vec![]; - section.extend_from_slice(&HEADER); - for _i in 0..(SECTION_LENGTH - HEADER.len()) { - section.push(0x00); +pub fn parse(bytes: &[u8;298]) -> Result{ + if bytes[0..10] != SECTION_HEADER{ + return Err(ParseError{message: format!{"Found wrong header for quests: {:02X?}", &bytes[0..10]}}) + } + let mut quests = Quests::default(); + + Ok(quests) +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse(){ + let bytes : [u8;298] = [0x00; 298]; + let result = match parse(&bytes) { + Ok(res) => res, + Err(e) => { + panic!("Failed test_parse in quests: {0:?}", e); + } + }; + } - section } diff --git a/src/utils.rs b/src/utils.rs index b4b4dd4..9bcd720 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use std::time::SystemTime; +use std::ops::Range; pub fn get_sys_time_in_secs() -> u32 { match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { @@ -6,3 +7,35 @@ pub fn get_sys_time_in_secs() -> u32 { Err(_) => panic!("SystemTime before UNIX EPOCH!"), } } + +#[derive(PartialEq, Eq, Debug)] +pub struct FileSection { + pub offset: usize, + pub bytes: usize, +} + +impl From for Range { + fn from(file_section: FileSection) -> Range { + return file_section.offset..(file_section.offset + file_section.bytes); + } +} + +pub fn u32_from(slice: &[u8]) -> u32 { + u32::from_le_bytes( + slice + .try_into() + .unwrap(), + ) +} + +pub fn u16_from(slice: &[u8]) -> u16 { + u16::from_le_bytes( + slice + .try_into() + .unwrap(), + ) +} + +pub fn u8_from(slice: &[u8]) -> u8 { + slice[0] +} From 0c07961697a27d85f136933bb68334abd27a512a Mon Sep 17 00:00:00 2001 From: feored Date: Tue, 25 Jul 2023 03:50:52 +0200 Subject: [PATCH 4/9] Parse and generate quest sections --- src/character/mod.rs | 2 +- src/lib.rs | 18 ++ src/quests.rs | 454 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 407 insertions(+), 67 deletions(-) diff --git a/src/character/mod.rs b/src/character/mod.rs index ff14243..5414fb3 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -56,7 +56,7 @@ impl From
for FileSection { Section::WeaponSet => FileSection { offset: 0, bytes: 4 }, Section::Status => FileSection { offset: 20, bytes: 1 }, Section::Progression => FileSection { offset: 21, bytes: 1 }, - Section::Class => FileSection { offset: 26, bytes: 1 }, + Section::Class => FileSection { offset: 24, bytes: 1 }, Section::Level => FileSection { offset: 27, bytes: 1 }, Section::LastPlayed => FileSection { offset: 32, bytes: 4 }, Section::AssignedSkills => FileSection { offset: 40, bytes: 64 }, diff --git a/src/lib.rs b/src/lib.rs index 35a9f9b..461ff78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,6 +103,12 @@ pub enum Difficulty { Hell, } +impl fmt::Display for Difficulty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + #[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Act { #[default] @@ -113,6 +119,18 @@ pub enum Act { Act5, } +impl fmt::Display for Act { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Act::Act1 => write!(f, "Act I"), + Act::Act2 => write!(f, "Act II"), + Act::Act3 => write!(f, "Act III"), + Act::Act4 => write!(f, "Act IV"), + Act::Act5 => write!(f, "Act V"), + } + } +} + impl TryFrom for Act { type Error = ParseError; fn try_from(byte: u8) -> Result { diff --git a/src/quests.rs b/src/quests.rs index 0a7afdc..bec05a4 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::ops::Range; use std::str; @@ -9,13 +10,45 @@ use crate::Difficulty; use crate::GameLogicError; use crate::ParseError; -use crate::utils::FileSection; -use crate::utils::u32_from; use crate::utils::u16_from; +use crate::utils::u32_from; use crate::utils::u8_from; +use crate::utils::FileSection; const SECTION_HEADER: [u8; 10] = [0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01]; - +const ACT_1_QUESTS: [&'static str; 6] = [ + "Den of Evil", + "Sisters' Burial Ground", + "Search For Cain", + "The Forgotten Tower", + "Tools of the Trade", + "Sisters to the Slaughter", +]; +const ACT_2_QUESTS: [&'static str; 6] = [ + "Radament's Lair", + "The Horadric Staff", + "Tainted Sun", + "Arcane Sanctuary", + "The Summoner", + "The Seven Tombs", +]; +const ACT_3_QUESTS: [&'static str; 6] = [ + "The Golden Bird", + "Blade of the Old Religion", + "Khalim's Will", + "Lam Esen's Tome", + "The Blackened Temple", + "The Guardian", +]; +const ACT_4_QUESTS: [&'static str; 3] = ["Fallen Angel", "Hell's Forge", "Terror's End"]; +const ACT_5_QUESTS: [&'static str; 6] = [ + "Siege on Harrogath", + "Rescue on Mount Arreat", + "Prison of Ice", + "Betrayal of Harrogath", + "Rite of Passage", + "Eve of Destruction", +]; #[derive(PartialEq, Eq, Debug)] enum Section { @@ -34,31 +67,79 @@ enum Section { BaseGameComplete, Act5Quests, ResetStats, - DifficultyComplete + DifficultyComplete, } impl From
for FileSection { fn from(section: Section) -> FileSection { match section { - Section::Act1Introduction => FileSection {offset: 0, bytes: 2}, - Section::Act1Quests => FileSection {offset: 2, bytes: 12}, - Section::Act2Travel => FileSection {offset: 14, bytes: 2}, - Section::Act2Introduction => FileSection {offset: 16, bytes: 2}, - Section::Act2Quests => FileSection {offset: 18, bytes: 12}, - Section::Act3Travel => FileSection {offset: 30, bytes: 2}, - Section::Act3Introduction => FileSection {offset: 32, bytes: 2}, - Section::Act3Quests => FileSection {offset: 34, bytes: 12}, - Section::Act4Travel => FileSection {offset: 46, bytes: 2}, - Section::Act4Introduction => FileSection {offset: 48, bytes: 2}, - Section::Act4Quests => FileSection {offset: 50, bytes: 12}, - Section::Act5Travel => FileSection {offset: 62, bytes: 2}, - Section::BaseGameComplete => FileSection {offset: 64, bytes: 2}, - Section::Act5Quests => FileSection {offset: 70, bytes: 12}, - Section::ResetStats => FileSection {offset: 82, bytes: 1}, - Section::DifficultyComplete => FileSection {offset: 83, bytes: 1} + Section::Act1Introduction => FileSection { + offset: 0, + bytes: 2, + }, + Section::Act1Quests => FileSection { + offset: 2, + bytes: 12, + }, + Section::Act2Travel => FileSection { + offset: 14, + bytes: 2, + }, + Section::Act2Introduction => FileSection { + offset: 16, + bytes: 2, + }, + Section::Act2Quests => FileSection { + offset: 18, + bytes: 12, + }, + Section::Act3Travel => FileSection { + offset: 30, + bytes: 2, + }, + Section::Act3Introduction => FileSection { + offset: 32, + bytes: 2, + }, + Section::Act3Quests => FileSection { + offset: 34, + bytes: 12, + }, + Section::Act4Travel => FileSection { + offset: 46, + bytes: 2, + }, + Section::Act4Introduction => FileSection { + offset: 48, + bytes: 2, + }, + Section::Act4Quests => FileSection { + offset: 50, + bytes: 12, + }, + Section::Act5Travel => FileSection { + offset: 62, + bytes: 2, + }, + Section::BaseGameComplete => FileSection { + offset: 64, + bytes: 2, + }, + Section::Act5Quests => FileSection { + offset: 70, + bytes: 12, + }, + Section::ResetStats => FileSection { + offset: 82, + bytes: 1, + }, + Section::DifficultyComplete => FileSection { + offset: 83, + bytes: 1, + }, } } -} +} #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum Stage { @@ -66,114 +147,355 @@ pub enum Stage { RequirementsMet, Started, Closed, - CompletedInGame + CompletedInGame, } #[derive(PartialEq, Eq, Debug, Default, Clone, Copy)] pub struct Quest { id: usize, + name: &'static str, flags: u16, act: Act, difficulty: Difficulty, } +impl fmt::Display for Quest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Quest {0}:\t{1}\t({2} {3}):\t{4:#018b}\t{4:?}\t {4:X?}", + self.id, self.name, self.act, self.difficulty, self.flags + ) + } +} + #[derive(PartialEq, Eq, Debug, Default)] -pub struct Quests{ - pub normal: QuestSet, - pub nightmare: QuestSet, - pub hell: QuestSet +pub struct Quests { + pub normal: DifficultyQuests, + pub nightmare: DifficultyQuests, + pub hell: DifficultyQuests, +} + +impl fmt::Display for Quests { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Normal: {0}\nNightmare: {1}\nHell: {2}", + self.normal, self.nightmare, self.hell + ) + } } #[derive(PartialEq, Eq, Debug, Default)] -pub struct QuestSet{ +pub struct DifficultyQuests { pub flags: QuestFlags, - pub quests: [Quest; 27] + pub quests: QuestSet, +} + +pub type QuestSet = [Quest; 27]; + +impl fmt::Display for DifficultyQuests { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut final_string = format!("Flags:\n {0}\nQuests:\n", self.flags); + for i in 0..self.quests.len() { + final_string.push_str(&format!("{0}\n", self.quests[i])); + } + write!(f, "{0}", final_string) + } } #[derive(PartialEq, Eq, Debug, Default)] pub struct QuestFlags { - pub act_1_introduction: u16, - pub act_2_travel: u16, - pub act_2_introduction: u16, - pub act_3_travel: u16, - pub act_3_introduction: u16, - pub act_4_travel: u16, - pub act_4_introduction: u16, - pub act_5_travel: u16, - pub completed_base_game: u16, - pub reset_stats: u8, - pub completed_difficulty : u8 + pub act_1_introduction: bool, + pub act_2_travel: bool, + pub act_2_introduction: bool, + pub act_3_travel: bool, + pub act_3_introduction: bool, + pub act_4_travel: bool, + pub act_4_introduction: bool, + pub act_5_travel: bool, + pub completed_base_game: bool, + pub reset_stats: bool, + pub completed_difficulty: bool, +} + +impl fmt::Display for QuestFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Completed difficulty: {0:?}", + self.completed_difficulty + ) + } } impl Quest { - fn set_stage(&mut self, stage: Stage, value: bool){ + fn set_stage(&mut self, stage: Stage, value: bool) { self.flags.set_bit(usize::from(stage), value); } fn finish(&mut self) { self.set_stage(Stage::Completed, true); self.set_stage(Stage::Closed, true); } + fn clear(&mut self){ + self.flags = 0; + } } impl From for usize { - fn from(stage:Stage) -> usize { - match stage{ + fn from(stage: Stage) -> usize { + match stage { Stage::Completed => 0, Stage::RequirementsMet => 1, Stage::Started => 2, Stage::Closed => 12, - Stage::CompletedInGame => 13 + Stage::CompletedInGame => 13, } } } -fn parse_flags(bytes: &[u8;96]) -> Result{ - let mut flags : QuestFlags = QuestFlags::default(); - flags.act_1_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act1Introduction))]); - flags.act_2_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Travel))]); - flags.act_2_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Introduction))]); - flags.act_3_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Travel))]); - flags.act_3_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Introduction))]); - flags.act_4_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Travel))]); - flags.act_4_introduction = u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Introduction))]); - flags.act_5_travel = u16_from(&bytes[Range::::from(FileSection::from(Section::Act5Travel))]); - flags.completed_base_game = u16_from(&bytes[Range::::from(FileSection::from(Section::BaseGameComplete))]); - flags.reset_stats = u8_from(&bytes[Range::::from(FileSection::from(Section::ResetStats))]); - flags.completed_difficulty = u8_from(&bytes[Range::::from(FileSection::from(Section::DifficultyComplete))]); +fn write_flags(bytes: &mut Vec, flags: &QuestFlags) { + bytes[Range::::from(FileSection::from(Section::Act1Introduction))] + .copy_from_slice(&u16::to_le_bytes(flags.act_1_introduction as u16)); + bytes[Range::::from(FileSection::from(Section::Act2Travel))] + .copy_from_slice(&u16::to_le_bytes(flags.act_2_travel as u16)); + bytes[Range::::from(FileSection::from(Section::Act2Introduction))] + .copy_from_slice(&u16::to_le_bytes(flags.act_2_introduction as u16)); + bytes[Range::::from(FileSection::from(Section::Act3Travel))] + .copy_from_slice(&u16::to_le_bytes(flags.act_3_travel as u16)); + bytes[Range::::from(FileSection::from(Section::Act3Introduction))] + .copy_from_slice(&u16::to_le_bytes(flags.act_3_introduction as u16)); + bytes[Range::::from(FileSection::from(Section::Act4Travel))] + .copy_from_slice(&u16::to_le_bytes(flags.act_4_travel as u16)); + bytes[Range::::from(FileSection::from(Section::Act4Introduction))] + .copy_from_slice(&u16::to_le_bytes(flags.act_4_introduction as u16)); + bytes[Range::::from(FileSection::from(Section::Act5Travel))] + .copy_from_slice(&u16::to_le_bytes(flags.act_5_travel as u16)); + bytes[Range::::from(FileSection::from(Section::BaseGameComplete))] + .copy_from_slice(&u16::to_le_bytes(flags.completed_base_game as u16)); + bytes[Range::::from(FileSection::from(Section::ResetStats))] + .copy_from_slice(&u8::to_le_bytes(flags.reset_stats as u8)); + bytes[Range::::from(FileSection::from(Section::DifficultyComplete))] + .copy_from_slice(match flags.completed_difficulty { true => &[0x80], false => &[0x00]}); +} + +fn parse_flags(bytes: &[u8; 96]) -> Result { + let mut flags: QuestFlags = QuestFlags::default(); + flags.act_1_introduction = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act1Introduction))]); + // Any non-zero is considered true + flags.act_2_travel = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Travel))]); + flags.act_2_introduction = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act2Introduction))]); + flags.act_3_travel = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Travel))]); + flags.act_3_introduction = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act3Introduction))]); + flags.act_4_travel = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Travel))]); + flags.act_4_introduction = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act4Introduction))]); + flags.act_5_travel = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::Act5Travel))]); + flags.completed_base_game = + 0 != u16_from(&bytes[Range::::from(FileSection::from(Section::BaseGameComplete))]); + flags.reset_stats = + 0 != u8_from(&bytes[Range::::from(FileSection::from(Section::ResetStats))]); + flags.completed_difficulty = + 0 != u8_from(&bytes[Range::::from(FileSection::from(Section::DifficultyComplete))]); Ok(flags) } -fn parse_quests(bytes: &[u8;96]) -> Result<[Quest; 27], ParseError>{ - let mut quests : [Quest; 27] = [Quest::default();27]; +fn write_quests(byte_vector: &mut Vec, quests: &QuestSet) { + for act in 0..4 { + let mut act_quests: [u8; 12] = [0x00; 12]; + let quests_number = match act { + 0..=2 | 4 => 6, + 3 => 3, + _ => unreachable!() + }; + for i in 0..quests_number { + let quest_index = i + match act { + 0 => 0, + 1 => 6, + 2 => 12, + 3 => 18, + 4 => 21, + _ => unreachable!() + }; + let quest_value = u16::to_le_bytes(quests[quest_index].flags); + // println!{"@@@@@ Quest: {0}", quests[quest_index]}; + // println!("%%%%% Flags: {0:?} Quest value: {quest_value:X?} ", quests[quest_index].flags); + act_quests[i * 2] = quest_value[0]; + act_quests[(i * 2) + 1] = quest_value[1]; + // println!("Putting them at {0}..{1}", (i * 2), (i * 2) + 1); + // println!{"Current quests: {0:X?}", act_quests}; + } + let section = match act { + 0 => Section::Act1Quests, + 1 => Section::Act2Quests, + 2 => Section::Act3Quests, + 3 => Section::Act4Quests, + 4 => Section::Act5Quests, + _ => unreachable!() + }; + // println!{"############# Writing quests: {0:X?}", act_quests}; + byte_vector[Range::::from(FileSection::from(section))].copy_from_slice(&act_quests); + } +} - +fn parse_quests(bytes: &[u8; 96], difficulty: Difficulty) -> Result { + let mut quests: QuestSet = QuestSet::default(); + let act_1_quests = &bytes[Range::::from(FileSection::from(Section::Act1Quests))]; + // println!("{0:X?}", act_1_quests); + for i in 0..6 { + // println!("{0:X?}", &act_1_quests[(i*2)..((i*2)+ 2)]); + quests[i] = Quest { + id: i, + name: ACT_1_QUESTS[i], + act: Act::Act1, + difficulty: difficulty, + flags: u16_from(&act_1_quests[(i*2)..((i*2) + 2)]) + }; + } + let act_2_quests = &bytes[Range::::from(FileSection::from(Section::Act2Quests))]; + for i in 0..6 { + quests[i + 6] = Quest { + id: i + 6, + name: ACT_2_QUESTS[i], + act: Act::Act2, + difficulty: difficulty, + flags: u16_from(&act_2_quests[(i*2)..((i*2) + 2)]) + }; + } + + let act_3_quests = &bytes[Range::::from(FileSection::from(Section::Act3Quests))]; + for i in 0..6 { + quests[i + 12] = Quest { + id: i + 12, + name: ACT_3_QUESTS[i], + act: Act::Act3, + difficulty: difficulty, + flags: u16_from(&act_3_quests[(i*2)..((i*2) + 2)]) + }; + } + + let act_4_quests = &bytes[Range::::from(FileSection::from(Section::Act4Quests))]; + for i in 0..3 { + quests[i + 18] = Quest { + id: i + 18, + name: ACT_4_QUESTS[i], + act: Act::Act4, + difficulty: difficulty, + flags: u16_from(&act_4_quests[(i*2)..((i*2) + 2)]) + }; + } + + let act_5_quests = &bytes[Range::::from(FileSection::from(Section::Act5Quests))]; + for i in 0..6 { + quests[i + 21] = Quest { + id: i + 21, + name: ACT_5_QUESTS[i], + act: Act::Act5, + difficulty: difficulty, + flags: u16_from(&act_5_quests[(i*2)..((i*2) + 2)]) + }; + } + Ok(quests) } -pub fn parse(bytes: &[u8;298]) -> Result{ - if bytes[0..10] != SECTION_HEADER{ - return Err(ParseError{message: format!{"Found wrong header for quests: {:02X?}", &bytes[0..10]}}) +pub fn parse(bytes: &[u8; 298]) -> Result { + if bytes[0..10] != SECTION_HEADER { + return Err(ParseError { + message: format! {"Found wrong header for quests: {:02X?}", &bytes[0..10]}, + }); } let mut quests = Quests::default(); + quests.normal.quests = parse_quests(&bytes[10..106].try_into().unwrap(), Difficulty::Normal)?; + quests.nightmare.quests = + parse_quests(&bytes[106..202].try_into().unwrap(), Difficulty::Nightmare)?; + quests.hell.quests = parse_quests(&bytes[202..298].try_into().unwrap(), Difficulty::Hell)?; + + quests.normal.flags = parse_flags(&bytes[10..106].try_into().unwrap())?; + quests.nightmare.flags = parse_flags(&bytes[106..202].try_into().unwrap())?; + quests.hell.flags = parse_flags(&bytes[202..298].try_into().unwrap())?; + Ok(quests) } +pub fn generate(all_quests: &Quests) -> Vec { + let mut byte_vector = SECTION_HEADER.to_vec(); + byte_vector.resize(298, 0x00); + + let mut normal = Vec::::new(); + normal.resize(96, 0x00); + write_quests(&mut normal, &all_quests.normal.quests); + write_flags(&mut normal, &all_quests.normal.flags); + byte_vector[10..106].copy_from_slice(&normal); + + let mut nightmare = Vec::::new(); + nightmare.resize(96, 0x00); + write_quests(&mut nightmare, &all_quests.nightmare.quests); + write_flags(&mut nightmare, &all_quests.nightmare.flags); + byte_vector[106..202].copy_from_slice(&nightmare); + + let mut hell = Vec::::new(); + hell.resize(96, 0x00); + write_quests(&mut hell, &all_quests.hell.quests); + write_flags(&mut hell, &all_quests.hell.flags); + byte_vector[202..298].copy_from_slice(&hell); + + byte_vector +} #[cfg(test)] mod tests { use super::*; #[test] - fn test_parse(){ - let bytes : [u8;298] = [0x00; 298]; - let result = match parse(&bytes) { + fn test_generate_and_parse() { + let bytes: [u8; 298] = [ + 0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01, 0x01, 0x00, 0x01, 0x10, + 0x1D, 0x10, 0x4E, 0x80, 0x1D, 0x10, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x1D, 0x10, 0x79, 0x1C, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x65, 0x1F, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x80, 0x08, 0x00, + 0x8D, 0x17, 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x0C, 0x00, + 0x4A, 0x80, 0x19, 0x10, 0x00, 0x00, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x10, + 0x79, 0x18, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x25, 0x18, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x80, 0x08, 0x00, 0x89, 0x17, + 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x02, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x1D, 0x10, 0x0C, 0x00, + 0x19, 0x14, 0x15, 0x10, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x90, 0x79, 0x1C, + 0x05, 0x90, 0x81, 0x11, 0x05, 0x10, 0x25, 0x1A, 0x01, 0x00, 0x01, 0x00, 0x05, 0x10, + 0x7D, 0x10, 0x00, 0x00, 0x01, 0x10, 0x09, 0x10, 0x71, 0x10, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x10, 0x01, 0x13, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x90, 0x01, 0x10, 0x89, 0x17, 0x1E, 0x80, + 0x19, 0x13, 0xDD, 0x17, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + let parsed_result = match parse(&bytes) { Ok(res) => res, Err(e) => { - panic!("Failed test_parse in quests: {0:?}", e); + println!("#### FAILED TO PARSE QUESTS"); + panic!("{e:?}") } }; + println!("{0}", parsed_result); + assert_eq!(parsed_result.hell.flags.completed_difficulty, true); + assert_eq!(parsed_result.hell.quests[26].name, "Eve of Destruction"); + + let mut new_bytes : [u8;298] = [0x00; 298]; + new_bytes.copy_from_slice(&generate(&parsed_result)); + assert_eq!(bytes, new_bytes); } } From 693be88fbdef475286c62a2a729d4022aff57f9e Mon Sep 17 00:00:00 2001 From: feored Date: Tue, 25 Jul 2023 20:08:24 +0200 Subject: [PATCH 5/9] Parse and generate waypoints --- notes.md | 8 +- src/quests.rs | 84 ++++----- src/utils.rs | 1 + src/waypoints.rs | 467 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 501 insertions(+), 59 deletions(-) diff --git a/notes.md b/notes.md index 7744f5d..d99b64e 100644 --- a/notes.md +++ b/notes.md @@ -153,4 +153,10 @@ Quest not started: 0x00 0x00 => 0000 0000 0000 Quest started (Talked to Akara): 0x04 0x00 => 0000 0000 0000 0100 Cleared Den of Evil (Return to Akara for reward): 0x1C 0x00 => 0000 0000 0001 1100 Talked to Akara (Completed quest): 0x01 0x30 => 0011 0000 0000 0001 -Used skill point: 0x01 0x10 => 0001 0000 0000 0001 \ No newline at end of file +Used skill point: 0x01 0x10 => 0001 0000 0000 0001 + + +## Waypoints + +A new character will have three waypoints set to true by default: Rogue encampment in normal, nightmare and hell. +Getting to a new act automatically unlocks the town wp. \ No newline at end of file diff --git a/src/quests.rs b/src/quests.rs index bec05a4..dcdb0b3 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -456,46 +456,46 @@ pub fn generate(all_quests: &Quests) -> Vec { mod tests { use super::*; - #[test] - fn test_generate_and_parse() { - let bytes: [u8; 298] = [ - 0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01, 0x01, 0x00, 0x01, 0x10, - 0x1D, 0x10, 0x4E, 0x80, 0x1D, 0x10, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x1D, 0x10, 0x79, 0x1C, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x65, 0x1F, 0x01, 0x00, - 0x01, 0x00, 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, - 0x01, 0x00, 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x80, 0x08, 0x00, - 0x8D, 0x17, 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x0C, 0x00, - 0x4A, 0x80, 0x19, 0x10, 0x00, 0x00, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x10, - 0x79, 0x18, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x25, 0x18, 0x01, 0x00, 0x01, 0x00, - 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, 0x01, 0x00, - 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x80, 0x08, 0x00, 0x89, 0x17, - 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x02, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x1D, 0x10, 0x0C, 0x00, - 0x19, 0x14, 0x15, 0x10, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x90, 0x79, 0x1C, - 0x05, 0x90, 0x81, 0x11, 0x05, 0x10, 0x25, 0x1A, 0x01, 0x00, 0x01, 0x00, 0x05, 0x10, - 0x7D, 0x10, 0x00, 0x00, 0x01, 0x10, 0x09, 0x10, 0x71, 0x10, 0x01, 0x00, 0x01, 0x00, - 0x01, 0x10, 0x01, 0x13, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x90, 0x01, 0x10, 0x89, 0x17, 0x1E, 0x80, - 0x19, 0x13, 0xDD, 0x17, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - let parsed_result = match parse(&bytes) { - Ok(res) => res, - Err(e) => { - println!("#### FAILED TO PARSE QUESTS"); - panic!("{e:?}") - } - }; - println!("{0}", parsed_result); - assert_eq!(parsed_result.hell.flags.completed_difficulty, true); - assert_eq!(parsed_result.hell.quests[26].name, "Eve of Destruction"); - - let mut new_bytes : [u8;298] = [0x00; 298]; - new_bytes.copy_from_slice(&generate(&parsed_result)); - - assert_eq!(bytes, new_bytes); - } + // #[test] + // fn test_generate_and_parse() { + // let bytes: [u8; 298] = [ + // 0x57, 0x6F, 0x6F, 0x21, 0x06, 0x00, 0x00, 0x00, 0x2A, 0x01, 0x01, 0x00, 0x01, 0x10, + // 0x1D, 0x10, 0x4E, 0x80, 0x1D, 0x10, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00, 0x01, 0x00, + // 0x1D, 0x10, 0x79, 0x1C, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x65, 0x1F, 0x01, 0x00, + // 0x01, 0x00, 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, + // 0x01, 0x00, 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x80, 0x08, 0x00, + // 0x8D, 0x17, 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x0C, 0x00, + // 0x4A, 0x80, 0x19, 0x10, 0x00, 0x00, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x10, + // 0x79, 0x18, 0x05, 0x10, 0x81, 0x11, 0x05, 0x10, 0x25, 0x18, 0x01, 0x00, 0x01, 0x00, + // 0x01, 0x10, 0x7D, 0x10, 0xF5, 0x13, 0x01, 0x10, 0x0D, 0x10, 0x61, 0x10, 0x01, 0x00, + // 0x01, 0x00, 0x01, 0x10, 0x01, 0x13, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x80, 0x08, 0x00, 0x89, 0x17, + // 0x0C, 0x00, 0x19, 0x13, 0xCD, 0x15, 0x02, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x1D, 0x10, 0x0C, 0x00, + // 0x19, 0x14, 0x15, 0x10, 0x19, 0x10, 0x01, 0x00, 0x01, 0x00, 0x11, 0x90, 0x79, 0x1C, + // 0x05, 0x90, 0x81, 0x11, 0x05, 0x10, 0x25, 0x1A, 0x01, 0x00, 0x01, 0x00, 0x05, 0x10, + // 0x7D, 0x10, 0x00, 0x00, 0x01, 0x10, 0x09, 0x10, 0x71, 0x10, 0x01, 0x00, 0x01, 0x00, + // 0x01, 0x10, 0x01, 0x13, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x90, 0x01, 0x10, 0x89, 0x17, 0x1E, 0x80, + // 0x19, 0x13, 0xDD, 0x17, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00, 0x00, + // ]; + // let parsed_result = match parse(&bytes) { + // Ok(res) => res, + // Err(e) => { + // println!("#### FAILED TO PARSE QUESTS"); + // panic!("{e:?}") + // } + // }; + // println!("{0}", parsed_result); + // assert_eq!(parsed_result.hell.flags.completed_difficulty, true); + // assert_eq!(parsed_result.hell.quests[26].name, "Eve of Destruction"); + + // let mut new_bytes : [u8;298] = [0x00; 298]; + // new_bytes.copy_from_slice(&generate(&parsed_result)); + + // assert_eq!(bytes, new_bytes); + // } } diff --git a/src/utils.rs b/src/utils.rs index 9bcd720..37fd4b7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,6 +20,7 @@ impl From for Range { } } + pub fn u32_from(slice: &[u8]) -> u32 { u32::from_le_bytes( slice diff --git a/src/waypoints.rs b/src/waypoints.rs index 143a39a..aa3796d 100644 --- a/src/waypoints.rs +++ b/src/waypoints.rs @@ -1,20 +1,455 @@ -pub const OFFSET: usize = 633; -const HEADER: [u8; 8] = [0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00]; +use std::fmt; + +use crate::Act; +use crate::Difficulty; +use crate::ParseError; +use bit::BitIndex; + +const SECTION_HEADER: [u8; 8] = [0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00]; const DIFFICULTY_HEADER: [u8; 2] = [0x02, 0x01]; -const DIFFICULTY_LENGTH: usize = 24; -const TRAILER: u8 = 0x01; - -pub fn build_section() -> Vec { - let mut waypoints_section = vec![]; - waypoints_section.extend_from_slice(&HEADER); - for _i in 0..3 { - waypoints_section.extend_from_slice(&DIFFICULTY_HEADER); - waypoints_section.push(0x01); - let remaining_bytes = DIFFICULTY_LENGTH - DIFFICULTY_HEADER.len() - 1; - for _j in 0..remaining_bytes { - waypoints_section.push(0x00); +const SECTION_TRAILER: u8 = 0x01; + +const NAMES_ACT1: [&'static str; 9] = [ + "Rogue Encampment", + "Cold Plains", + "Stony Field", + "Dark Wood", + "Black Marsh", + "Outer Cloister", + "Jail", + "Inner Cloister", + "Catacombs", +]; + +const NAMES_ACT2: [&'static str; 9] = [ + "Lut Gholein", + "Sewers", + "Dry Hills", + "Halls of the Dead", + "Far Oasis", + "Lost City", + "Palace Cellar", + "Arcane Sanctuary", + "Canyon of the Magi", +]; + +const NAMES_ACT3: [&'static str; 9] = [ + "Kurast Docks", + "Spider Forest", + "Great Marsh", + "Flayer Jungle", + "Lower Kurast", + "Kurast Bazaar", + "Upper Kurast", + "Travincal", + "Durance of Hate", +]; + +const NAMES_ACT4: [&'static str; 3] = + ["Pandemonium Fortress", "City of the Damned", "River of Flames"]; + +const NAMES_ACT5: [&'static str; 9] = [ + "Harrogath", + "Frigid Highlands", + "Arreat Plateau", + "Crystalline Passage", + "Halls of Pain", + "Glacial Trail", + "Frozen Tundra", + "The Ancients' Way", + "Worldstone Keep", +]; + +#[derive(PartialEq, Eq, Debug, Clone, Default)] +pub struct WaypointInfo { + id: Waypoint, + name: &'static str, + act: Act, + acquired: bool, +} + +impl fmt::Display for WaypointInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{0} - {1}: {2}", self.act, self.name, self.acquired) + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Default)] +pub struct Waypoints { + normal: DifficultyWaypoints, + nightmare: DifficultyWaypoints, + hell: DifficultyWaypoints, +} + +impl fmt::Display for Waypoints { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Normal:\n{0}\nNightmare:{1}\nHell:\n {2}", + self.normal, self.nightmare, self.hell + ) + } +} + +// impl Waypoints { +// fn acquire(&mut self, difficulty: Difficulty, id: Waypoint) { +// match Act::from(id) { +// Act::Act1 => self.act1[id as usize].acquired = true, +// } +// } +// } + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct DifficultyWaypoints { + act1: [WaypointInfo; 9], + act2: [WaypointInfo; 9], + act3: [WaypointInfo; 9], + act4: [WaypointInfo; 3], + act5: [WaypointInfo; 9], +} + +impl fmt::Display for DifficultyWaypoints { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let array_to_string = |array: &[WaypointInfo], length: usize| -> String { + let mut final_string = String::default(); + for i in 0..length { + final_string.push_str(&array[i].to_string()); + final_string.push_str("\n"); + } + final_string + }; + let mut final_string = String::default(); + final_string.push_str(&array_to_string(&self.act1, 9)); + final_string.push_str(&array_to_string(&self.act2, 9)); + final_string.push_str(&array_to_string(&self.act3, 9)); + final_string.push_str(&array_to_string(&self.act4, 3)); + final_string.push_str(&array_to_string(&self.act5, 9)); + + write!(f, "{0}", final_string) + } +} + +impl Default for DifficultyWaypoints { + fn default() -> Self { + fn default_waypoints(act: Act) -> [WaypointInfo; 9] { + let mut default_waypoints: [WaypointInfo; 9] = <[WaypointInfo; 9]>::default(); + for i in 0..9 { + default_waypoints[i].act = act; + default_waypoints[i].name = match act { + Act::Act1 => NAMES_ACT1[i], + Act::Act2 => NAMES_ACT2[i], + Act::Act3 => NAMES_ACT3[i], + Act::Act4 => NAMES_ACT4[i], + Act::Act5 => NAMES_ACT5[i], + }; + let absolute_id: usize = i + match act { + Act::Act1 => 0, + Act::Act2 => 9, + Act::Act3 => 18, + Act::Act4 => 27, + Act::Act5 => 30, + }; + default_waypoints[i].id = match Waypoint::try_from(absolute_id) { + Ok(res) => res, + Err(e) => panic!("Error getting default difficulty waypoint: {e}"), + }; + default_waypoints[i].acquired = false + } + if act == Act::Act1 { + default_waypoints[0].acquired = true; + } + default_waypoints + } + Self { + act1: default_waypoints(Act::Act1), + act2: default_waypoints(Act::Act2), + act3: default_waypoints(Act::Act3), + act4: { + let mut default_waypoints: [WaypointInfo; 3] = <[WaypointInfo; 3]>::default(); + for i in 0..3 { + default_waypoints[i].act = Act::Act4; + default_waypoints[i].name = NAMES_ACT4[i]; + default_waypoints[i].id = match Waypoint::try_from(27 + i) { + Ok(res) => res, + Err(e) => panic!("Error getting default difficulty waypoint: {e}"), + }; + default_waypoints[i].acquired = false; + } + default_waypoints + }, + act5: default_waypoints(Act::Act5), + } + } +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)] +pub enum Waypoint { + #[default] + RogueEncampment = 0, + ColdPlains = 1, + StonyField = 2, + DarkWood = 3, + BlackMarsh = 4, + OuterCloister = 5, + Jail = 6, + InnerCloister = 7, + Catacombs = 8, + LutGholein = 9, + Sewers = 10, + DryHills = 11, + HallsOfTheDead = 12, + FarOasis = 13, + LostCity = 14, + PalaceCellar = 15, + ArcaneSanctuary = 16, + CanyonOfTheMagi = 17, + KurastDocks = 18, + SpiderForest = 19, + GreatMarsh = 20, + FlayerJungle = 21, + LowerKurast = 22, + KurastBazaar = 23, + UpperKurast = 24, + Travincal = 25, + DuranceOfHate = 26, + PandemoniumFortress = 27, + CityOfTheDamned = 28, + RiverOfFlames = 29, + Harrogath = 30, + FrigidHighlands = 31, + ArreatPlateau = 32, + CrystallinePassage = 33, + HallsOfPain = 34, + GlacialTrail = 35, + FrozenTundra = 36, + TheAncientsWay = 37, + WorldstoneKeep = 38, +} + +impl From for Act { + fn from(id: Waypoint) -> Act { + match id as usize { + 0..=8 => Act::Act1, + 9..=17 => Act::Act2, + 18..=26 => Act::Act3, + 27..=29 => Act::Act4, + 30..=38 => Act::Act5, + _ => unreachable!(), + } + } +} + +impl TryFrom for Waypoint { + type Error = ParseError; + fn try_from(id: usize) -> Result { + match id { + 0 => Ok(Waypoint::RogueEncampment), + 1 => Ok(Waypoint::ColdPlains), + 2 => Ok(Waypoint::StonyField), + 3 => Ok(Waypoint::DarkWood), + 4 => Ok(Waypoint::BlackMarsh), + 5 => Ok(Waypoint::OuterCloister), + 6 => Ok(Waypoint::Jail), + 7 => Ok(Waypoint::InnerCloister), + 8 => Ok(Waypoint::Catacombs), + 9 => Ok(Waypoint::LutGholein), + 10 => Ok(Waypoint::Sewers), + 11 => Ok(Waypoint::DryHills), + 12 => Ok(Waypoint::HallsOfTheDead), + 13 => Ok(Waypoint::FarOasis), + 14 => Ok(Waypoint::LostCity), + 15 => Ok(Waypoint::PalaceCellar), + 16 => Ok(Waypoint::ArcaneSanctuary), + 17 => Ok(Waypoint::CanyonOfTheMagi), + 18 => Ok(Waypoint::KurastDocks), + 19 => Ok(Waypoint::SpiderForest), + 20 => Ok(Waypoint::GreatMarsh), + 21 => Ok(Waypoint::FlayerJungle), + 22 => Ok(Waypoint::LowerKurast), + 23 => Ok(Waypoint::KurastBazaar), + 24 => Ok(Waypoint::UpperKurast), + 25 => Ok(Waypoint::Travincal), + 26 => Ok(Waypoint::DuranceOfHate), + 27 => Ok(Waypoint::PandemoniumFortress), + 28 => Ok(Waypoint::CityOfTheDamned), + 29 => Ok(Waypoint::RiverOfFlames), + 30 => Ok(Waypoint::Harrogath), + 31 => Ok(Waypoint::FrigidHighlands), + 32 => Ok(Waypoint::ArreatPlateau), + 33 => Ok(Waypoint::CrystallinePassage), + 34 => Ok(Waypoint::HallsOfPain), + 35 => Ok(Waypoint::GlacialTrail), + 36 => Ok(Waypoint::FrozenTundra), + 37 => Ok(Waypoint::TheAncientsWay), + 38 => Ok(Waypoint::WorldstoneKeep), + _ => Err(ParseError { + message: format!("Cannot convert ID > 8 to waypoint: {id:?}"), + }), } } - waypoints_section.push(TRAILER); - waypoints_section +} + +fn parse_waypoints(bytes: &[u8; 24]) -> Result { + let mut waypoints: DifficultyWaypoints = DifficultyWaypoints::default(); + if bytes[0..2] != DIFFICULTY_HEADER { + return Err(ParseError { + message: format!( + "Found wrong waypoint difficulty header: {0:X?}", + &bytes[0..2] + ), + }); + } + for id in 0..39 { + let current_byte = bytes[2 + id / 8]; + let waypoint = Waypoint::try_from(id)?; + match Act::from(waypoint) { + Act::Act1 => { + waypoints.act1[id] = WaypointInfo { + id: waypoint, + name: NAMES_ACT1[id], + act: Act::Act1, + acquired: current_byte.bit(id % 8), + } + } + Act::Act2 => { + waypoints.act2[id - 9] = WaypointInfo { + id: waypoint, + name: NAMES_ACT2[id - 9], + act: Act::Act2, + acquired: current_byte.bit(id % 8), + } + } + Act::Act3 => { + waypoints.act3[id - 18] = WaypointInfo { + id: waypoint, + name: NAMES_ACT3[id - 18], + act: Act::Act3, + acquired: current_byte.bit(id % 8), + } + } + Act::Act4 => { + waypoints.act4[id - 27] = WaypointInfo { + id: waypoint, + name: NAMES_ACT4[id - 27], + act: Act::Act4, + acquired: current_byte.bit(id % 8), + } + } + Act::Act5 => { + waypoints.act5[id - 30] = WaypointInfo { + id: waypoint, + name: NAMES_ACT5[id - 30], + act: Act::Act5, + acquired: current_byte.bit(id % 8), + } + } + _ => unreachable!(), + } + } + Ok(waypoints) +} + +pub fn parse(bytes: &[u8; 81]) -> Result { + let mut waypoints = Waypoints::default(); + if bytes[0..8] != SECTION_HEADER { + return Err(ParseError { + message: format!("Found wrong waypoints header: {0:X?}", &bytes[0..8]), + }); + } + waypoints.normal = match parse_waypoints(&bytes[8..32].try_into().unwrap()) { + Ok(res) => res, + Err(e) => return Err(e), + }; + waypoints.nightmare = parse_waypoints(&bytes[32..56].try_into().unwrap())?; + waypoints.hell = parse_waypoints(&bytes[56..80].try_into().unwrap())?; + Ok(waypoints) +} + +fn generate_difficulty(waypoints: &DifficultyWaypoints) -> [u8; 24] { + let mut bytes: [u8; 24] = [0x00; 24]; + bytes[0..2].copy_from_slice(&DIFFICULTY_HEADER); + fn fill_flags(waypoints: &[WaypointInfo], length: usize) -> u64 { + let mut flags: u64 = 0; + for i in 0..length { + flags.set_bit(i, waypoints[i].acquired); + } + flags + } + let mut flags: u64 = 0; + flags.set_bit_range(0..9, fill_flags(&waypoints.act1, 9).bit_range(0..9)); + flags.set_bit_range(9..18, fill_flags(&waypoints.act2, 9).bit_range(0..9)); + flags.set_bit_range(18..27, fill_flags(&waypoints.act3, 9).bit_range(0..9)); + flags.set_bit_range(27..30, fill_flags(&waypoints.act4, 3).bit_range(0..3)); + flags.set_bit_range(30..39, fill_flags(&waypoints.act5, 9).bit_range(0..9)); + bytes[2..10].copy_from_slice(&u64::to_le_bytes(flags)); + bytes +} + +pub fn generate(waypoints: &Waypoints) -> [u8; 81] { + let mut bytes: [u8; 81] = [0x00; 81]; + bytes[0..8].copy_from_slice(&SECTION_HEADER); + bytes[8..32].copy_from_slice(&generate_difficulty(&waypoints.normal)); + bytes[32..56].copy_from_slice(&generate_difficulty(&waypoints.nightmare)); + bytes[56..80].copy_from_slice(&generate_difficulty(&waypoints.hell)); + bytes[80] = SECTION_TRAILER; + bytes +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_waypoints_test() { + let bytes: [u8; 81] = [ + 0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x02, 0x01, 0xEF, 0xEB, 0xD7, 0xFF, + 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0xEF, 0xE3, 0xBD, 0xFF, 0x51, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x01, 0xEF, 0xEF, 0xEF, 0xFE, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + let parsed_waypoints = match parse(&bytes) { + Ok(res) => res, + Err(e) => panic!("parse_waypoints_test: {0}", e), + }; + //println!("{0}", parsed_waypoints); + assert_eq!( + parsed_waypoints.hell.act1[8], + WaypointInfo { + id: Waypoint::Catacombs, + name: "Catacombs", + act: Act::Act1, + acquired: true + } + ); + assert_eq!( + parsed_waypoints.nightmare.act2[3], + WaypointInfo { + id: Waypoint::HallsOfTheDead, + name: "Halls of the Dead", + act: Act::Act2, + acquired: false + } + ); + } + + #[test] + fn generate_waypoints_test() { + let expected_bytes: [u8; 81] = [ + 0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + + let parsed_waypoints = match parse(&expected_bytes) { + Ok(res) => res, + Err(e) => panic!("parse_waypoints_test: {0}", e), + }; + + assert_eq!(expected_bytes, generate(&Waypoints::default())); + } } From f690db271458ea762f958ae9ce679f9718875b82 Mon Sep 17 00:00:00 2001 From: feored Date: Wed, 26 Jul 2023 02:22:15 +0200 Subject: [PATCH 6/9] Character generation. --- notes.md | 3 + src/character/mercenary.rs | 55 +++++-------- src/character/mod.rs | 161 +++++++++++++++++++++++++++++-------- src/lib.rs | 43 ++++------ src/npcs.rs | 30 ++++--- src/quests.rs | 5 +- src/waypoints.rs | 63 ++++++++++----- 7 files changed, 231 insertions(+), 129 deletions(-) diff --git a/notes.md b/notes.md index d99b64e..520d00d 100644 --- a/notes.md +++ b/notes.md @@ -9,10 +9,13 @@ A list of resources that have helped with reverse engineering the .d2s format. * https://github.com/nokka/d2s/blob/master/README.md +### Class + ### Character Status Loading a single player file with "Ladder" bit set to 1 in Character Status does nothing (duh). + ### Character Menu Appearance 32 bytes starting at offset 136. diff --git a/src/character/mercenary.rs b/src/character/mercenary.rs index 69b5679..48d7a79 100644 --- a/src/character/mercenary.rs +++ b/src/character/mercenary.rs @@ -218,14 +218,16 @@ pub enum Barbarian { Frenzy, } + +/// TODO: Make private, add getters and setters that throw GameLogicError #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct Mercenary { - dead: bool, - id: u32, - name_id: u16, - name: &'static str, - variant: Variant, - experience: u32, + pub dead: bool, + pub id: u32, + pub name_id: u16, + pub name: &'static str, + pub variant: Variant, + pub experience: u32, } impl Default for Mercenary { @@ -241,7 +243,7 @@ impl Default for Mercenary { } } -fn variant_id(variant: &Variant) -> Result { +fn variant_id(variant: &Variant) -> u16{ let mut variant_id: u16 = 99; for i in 0..VARIANTS.len() { @@ -251,15 +253,11 @@ fn variant_id(variant: &Variant) -> Result { } } if (variant_id as usize) > VARIANTS.len() { - Err(GameLogicError { - message: format!( - "There is no mercenary ID for type {0:?} recruited in {1:?}", - variant.0, variant.1 - ), - }) - } else { - Ok(variant_id) + panic!("There is no mercenary ID for type {0:?} recruited in {1:?}", + variant.0, variant.1); + } + variant_id } fn names_list(class: Class) -> &'static [&'static str] { @@ -296,24 +294,21 @@ pub fn parse(data: &[u8; 14]) -> Result { Ok(mercenary) } -pub fn generate_mercenary(mercenary: &Mercenary) -> Result<[u8; 14], GameLogicError> { +pub fn generate(mercenary: &Mercenary) -> [u8; 14] { let mut bytes: [u8; 14] = [0x00; 14]; - bytes[0..2].clone_from_slice(match mercenary.dead { + bytes[0..2].copy_from_slice(match mercenary.dead { true => &[0x01, 0x00], false => &[0x00, 0x00], }); - bytes[2..6].clone_from_slice(&mercenary.id.to_le_bytes()); - bytes[6..8].clone_from_slice(&mercenary.name_id.to_le_bytes()); - let variant_id = match variant_id(&mercenary.variant) { - Ok(id) => id, - Err(e) => return Err(e), - }; + bytes[2..6].copy_from_slice(&mercenary.id.to_le_bytes()); + bytes[6..8].copy_from_slice(&mercenary.name_id.to_le_bytes()); + let variant_id = variant_id(&mercenary.variant); - bytes[8..10].clone_from_slice(&variant_id.to_le_bytes()); - bytes[10..14].clone_from_slice(&mercenary.experience.to_le_bytes()); + bytes[8..10].copy_from_slice(&variant_id.to_le_bytes()); + bytes[10..14].copy_from_slice(&mercenary.experience.to_le_bytes()); - Ok(bytes) + bytes } #[cfg(test)] @@ -354,13 +349,7 @@ mod tests { variant: (Class::Rogue(Rogue::Cold), Difficulty::Normal), experience: 63722u32, }; - let mut parsed_result: [u8; 14] = [0x00; 14]; - match generate_mercenary(&merc) { - Ok(res) => parsed_result = res, - Err(e) => { - println! {"Test failed: {e:?}"} - } - }; + let mut parsed_result: [u8; 14] = generate(&merc); assert_eq!(parsed_result, expected_result); } } diff --git a/src/character/mod.rs b/src/character/mod.rs index 5414fb3..557e218 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -102,7 +102,7 @@ pub struct Status { ladder: bool, expansion: bool, hardcore: bool, - died: bool, + died: bool } #[derive(PartialEq, Eq, Clone, Copy, Debug)] @@ -137,7 +137,7 @@ impl Default for Character { } } -fn parse_character(bytes: &[u8; 319]) -> Result { +fn parse(bytes: &[u8; 319]) -> Result { let mut character: Character = Character::default(); let active_weapon = u32_from(&bytes[Range::::from(FileSection::from(Section::WeaponSet))]); @@ -238,6 +238,45 @@ fn parse_character(bytes: &[u8; 319]) -> Result { Ok(character) } +pub fn generate(character: &Character) -> [u8; 319] { + let mut bytes : [u8;319] = [0x00; 319]; + + bytes[Range::::from(FileSection::from(Section::WeaponSet))].copy_from_slice(&u32::to_le_bytes(u32::from(character.weapon_set))); + bytes[Range::::from(FileSection::from(Section::Status))][0] = u8::from(character.status); + bytes[Range::::from(FileSection::from(Section::Progression))][0] = u8::from(character.progression); + bytes[Range::::from(FileSection::from(Section::Class))][0] = u8::from(character.class); + bytes[Range::::from(FileSection::from(Section::Level))][0] = u8::from(character.level); + bytes[Range::::from(FileSection::from(Section::LastPlayed))].copy_from_slice(&u32::to_le_bytes(u32::from(character.last_played))); + + + let mut assigned_skills : [u8; 64] = [0x00; 64]; + for i in 0..16 { + assigned_skills[(i*4)..((i*4)+4)].copy_from_slice(&u32::to_le_bytes(character.assigned_skills[i])); + } + bytes[Range::::from(FileSection::from(Section::AssignedSkills))].copy_from_slice(&assigned_skills); + bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))].copy_from_slice(&u32::to_le_bytes(character.left_mouse_skill)); + bytes[Range::::from(FileSection::from(Section::RightMouseSkill))].copy_from_slice(&u32::to_le_bytes(character.right_mouse_skill)); + bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))].copy_from_slice(&u32::to_le_bytes(character.left_mouse_switch_skill)); + bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))].copy_from_slice(&u32::to_le_bytes(character.right_mouse_switch_skill)); + bytes[Range::::from(FileSection::from(Section::MenuAppearance))].copy_from_slice(&character.menu_appearance); + bytes[Range::::from(FileSection::from(Section::Difficulty))].copy_from_slice(&generate_last_act(character.difficulty, character.act)); + bytes[Range::::from(FileSection::from(Section::MapSeed))].copy_from_slice(&u32::to_le_bytes(character.map_seed)); + bytes[Range::::from(FileSection::from(Section::Mercenary))].copy_from_slice(&mercenary::generate(&character.mercenary)); + bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))].copy_from_slice(&character.resurrected_menu_appearence); + let mut name : [u8; 16] = [0x00;16]; + let name_as_bytes = character.name.as_bytes(); + name[0..name_as_bytes.len()].clone_from_slice(&name_as_bytes); + bytes[Range::::from(FileSection::from(Section::Name))].copy_from_slice(&name); + + + // Add padding, unknown bytes, etc + bytes[25] = 0x10; + bytes[26] = 0x1E; + bytes[36..40].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); + + bytes +} + fn parse_last_act(bytes: &[u8; 3]) -> Result<(Difficulty, Act), ParseError> { let mut last_act = (Difficulty::Normal, Act::Act1); let mut index = 0; @@ -260,29 +299,31 @@ fn parse_last_act(bytes: &[u8; 3]) -> Result<(Difficulty, Act), ParseError> { Ok(last_act) } + + fn generate_last_act(difficulty: Difficulty, act: Act) -> [u8; 3] { - let mut bytes: [u8; 3] = [0x00; 3]; + let mut active_byte = u8::from(act); + active_byte.set_bit(7, true); match difficulty { Difficulty::Normal => { - bytes[0] = u8::from(act); + [active_byte, 0x00, 0x00] } Difficulty::Nightmare => { - bytes[1] = u8::from(act); + [0x00, active_byte, 0x00] } Difficulty::Hell => { - bytes[2] = u8::from(act); + [0x00, 0x00, active_byte] } } - bytes } impl Default for Status { fn default() -> Self { Self { - expansion: (true), - hardcore: (false), - ladder: (false), - died: (false), + expansion: true, + hardcore: false, + ladder: false, + died: false, } } } @@ -305,6 +346,7 @@ impl From for u8 { result.set_bit(3, status.died); result.set_bit(5, status.expansion); result.set_bit(6, status.ladder); + println!("Converted status: {0:#010b}", result); result } } @@ -430,7 +472,7 @@ mod tests { use super::*; #[test] - fn test_parse_character() { + fn test_parse() { let bytes: [u8; 319] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, @@ -457,33 +499,33 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - let expected_result = Character { + let expected_result = Character{ weapon_set: WeaponSet::Main, status: Status { expansion: true, hardcore: false, ladder: false, - died: false, + died: true, }, progression: 15, title: String::from("Matriarch"), class: Class::Sorceress, level: 92, - last_played: get_sys_time_in_secs(), - assigned_skills: [0x00; 16], - left_mouse_skill: 0, - right_mouse_skill: 0, + last_played: 1690118587, + assigned_skills: [40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535], + left_mouse_skill: 55, + right_mouse_skill: 54, left_mouse_switch_skill: 0, - right_mouse_switch_skill: 0, - menu_appearance: [0x00; 32], + right_mouse_switch_skill: 54, + menu_appearance: [57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], difficulty: Difficulty::Hell, act: Act::Act1, map_seed: 1402285379, - mercenary: Mercenary::default(), - resurrected_menu_appearence: [0x00; 48], + mercenary: Mercenary { dead: false, id: 1547718681, name_id: 7, name: "Emilio", variant: (mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), Difficulty::Hell), experience: 102341590 }, + resurrected_menu_appearence: [111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0, 0, 0, 0], name: String::from("Nyahallo"), }; - let parsed_result = match parse_character(&bytes) { + let parsed_result = match parse(&bytes) { Ok(result) => result, Err(e) => { println!("{e:?}"); @@ -491,15 +533,68 @@ mod tests { return; } }; - // println!("{0:?}", parsed_result); - assert_eq!(parsed_result.level, expected_result.level); - assert_eq!(parsed_result.class, expected_result.class); - assert_eq!(parsed_result.weapon_set, expected_result.weapon_set); - assert_eq!(parsed_result.map_seed, expected_result.map_seed); - assert_eq!(parsed_result.name, expected_result.name); - assert_eq!(parsed_result.act, expected_result.act); - assert_eq!(parsed_result.difficulty, expected_result.difficulty); - assert_eq!(parsed_result.progression, expected_result.progression); - assert_eq!(parsed_result.title, expected_result.title); + //println!("{0:?}", parsed_result); + assert_eq!(parsed_result, expected_result); + } + + #[test] + fn test_generate(){ + let expected_result: [u8; 319] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, + 0x00, 0x00, 0x00, 0x00, 0xBB, 0x29, 0xBD, 0x64, 0xFF, 0xFF, 0xFF, 0xFF, 0x28, 0x00, + 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, + 0x2B, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x9B, 0x00, + 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x39, 0x03, 0x02, 0x02, 0x02, 0x35, + 0xFF, 0x51, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x80, 0x43, 0x2D, 0x95, 0x53, 0x00, 0x00, 0x00, 0x00, 0x19, 0x50, 0x40, 0x5C, 0x07, + 0x00, 0x23, 0x00, 0xD6, 0x9B, 0x19, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0x62, 0x61, 0x20, 0xFF, 0x07, 0x1C, + 0x01, 0x04, 0x00, 0x00, 0x00, 0x75, 0x69, 0x74, 0x20, 0xFF, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x78, 0x70, 0x6C, 0x20, 0xFF, 0x07, 0xD9, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x75, 0x61, 0x70, 0x20, 0x4D, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, + 0x79, 0x61, 0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let character = Character{ + weapon_set: WeaponSet::Main, + status: Status { + expansion: true, + hardcore: false, + ladder: false, + died: true, + }, + progression: 15, + title: String::from("Matriarch"), + class: Class::Sorceress, + level: 92, + last_played: 1690118587, + assigned_skills: [40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535], + left_mouse_skill: 55, + right_mouse_skill: 54, + left_mouse_switch_skill: 0, + right_mouse_switch_skill: 54, + menu_appearance: [57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + difficulty: Difficulty::Hell, + act: Act::Act1, + map_seed: 1402285379, + mercenary: Mercenary { dead: false, id: 1547718681, name_id: 7, name: "Emilio", variant: (mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), Difficulty::Hell), experience: 102341590 }, + resurrected_menu_appearence: [111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0, 0, 0, 0], + name: String::from("Nyahallo"), + }; + let generated_result = generate(&character); + + + assert_eq!(expected_result, generated_result); + } } diff --git a/src/lib.rs b/src/lib.rs index 461ff78..d6e92de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,21 @@ const VERSION_D2R_100: u32 = 97; const VERSION_D2R_240: u32 = 98; const VERSION_D2R_250: u32 = 99; +enum FileStructure{ + Signature, + Version, + Checksum, + Character, + Quests, + Waypoints, + Npcs, + Attributes, + Skills, + Items +} + + + #[derive(Debug, Clone)] pub struct ParseError { message: String, @@ -54,34 +69,6 @@ pub struct Save { character: character::Character, } -#[derive(Debug)] -pub enum OffsetID { - Signature, - VersionID, - FileSize, - Checksum, - WeaponSet, - Status, - Progression, - Class, - Level, - LastPlayedDate, - AssignedSkills, - LeftMouseSkill, - RightMouseSkill, - LeftMouseSwitchSkill, - RightMouseSwitchSkill, - MenuAppearance, - Difficulty, - MapSeed, - Mercenary, - ResurrectedMenuAppearance, - Name, - Quests, - Waypoints, - NPCs, - Attributes, -} #[derive(PartialEq, Eq, Debug)] pub enum Version { diff --git a/src/npcs.rs b/src/npcs.rs index 7eddcf6..53e8174 100644 --- a/src/npcs.rs +++ b/src/npcs.rs @@ -1,12 +1,20 @@ -pub const OFFSET: usize = 714; -const HEADER: [u8; 3] = [0x77, 0x34, 0x00]; -const NPCS_LENGTH: usize = 40; - -pub fn build_section() -> Vec { - let mut section = vec![]; - section.extend_from_slice(&HEADER); - for _i in 0..NPCS_LENGTH { - section.push(0x00); - } - section +const SECTION_HEADER: [u8; 3] = [0x77, 0x34, 0x00]; + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub struct Placeholder { + data: [u8;52] +} + +pub fn parse(bytes: &[u8;52]) -> Placeholder{ + let mut placeholder: Placeholder = Placeholder {data: [0x00; 52]}; + placeholder.data.copy_from_slice(bytes); + + placeholder } + +pub fn generate(placeholder:Placeholder) -> [u8;52]{ + let mut bytes: [u8;52] = [0x00;52]; + bytes.copy_from_slice(&placeholder.data[0..52]); + + bytes +} \ No newline at end of file diff --git a/src/quests.rs b/src/quests.rs index dcdb0b3..c051368 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -5,13 +5,10 @@ use std::str; use bit::BitIndex; use crate::Act; -use crate::Class; use crate::Difficulty; -use crate::GameLogicError; use crate::ParseError; use crate::utils::u16_from; -use crate::utils::u32_from; use crate::utils::u8_from; use crate::utils::FileSection; @@ -489,7 +486,7 @@ mod tests { // panic!("{e:?}") // } // }; - // println!("{0}", parsed_result); + // //println!("{0}", parsed_result); // assert_eq!(parsed_result.hell.flags.completed_difficulty, true); // assert_eq!(parsed_result.hell.quests[26].name, "Eve of Destruction"); diff --git a/src/waypoints.rs b/src/waypoints.rs index aa3796d..432fcbe 100644 --- a/src/waypoints.rs +++ b/src/waypoints.rs @@ -1,9 +1,38 @@ use std::fmt; +use std::ops::Range; +use bit::BitIndex; + + +use crate::utils::FileSection; use crate::Act; -use crate::Difficulty; use crate::ParseError; -use bit::BitIndex; + +enum Section{ + Header, + Normal, + Nightmare, + Hell, + Trailer, + DifficultyHeader, + DifficultyWaypointsValue +} + + +impl From
for FileSection { + fn from(section: Section) -> FileSection { + match section { + Section::Header => FileSection { offset: 0, bytes: 8 }, + Section::Normal => FileSection { offset: 8, bytes: 24}, + Section::Nightmare => FileSection { offset: 32, bytes: 24}, + Section::Hell => FileSection { offset: 56, bytes: 24}, + Section::Trailer => FileSection { offset: 80, bytes: 1}, + Section::DifficultyHeader => FileSection {offset: 0, bytes: 2}, + Section::DifficultyWaypointsValue => FileSection {offset:2, bytes: 8} + } + } +} + const SECTION_HEADER: [u8; 8] = [0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00]; const DIFFICULTY_HEADER: [u8; 2] = [0x02, 0x01]; @@ -291,7 +320,7 @@ impl TryFrom for Waypoint { fn parse_waypoints(bytes: &[u8; 24]) -> Result { let mut waypoints: DifficultyWaypoints = DifficultyWaypoints::default(); - if bytes[0..2] != DIFFICULTY_HEADER { + if bytes[Range::::from(FileSection::from(Section::DifficultyHeader))] != DIFFICULTY_HEADER { return Err(ParseError { message: format!( "Found wrong waypoint difficulty header: {0:X?}", @@ -343,7 +372,6 @@ fn parse_waypoints(bytes: &[u8; 24]) -> Result acquired: current_byte.bit(id % 8), } } - _ => unreachable!(), } } Ok(waypoints) @@ -351,17 +379,17 @@ fn parse_waypoints(bytes: &[u8; 24]) -> Result pub fn parse(bytes: &[u8; 81]) -> Result { let mut waypoints = Waypoints::default(); - if bytes[0..8] != SECTION_HEADER { + if bytes[Range::::from(FileSection::from(Section::Header))] != SECTION_HEADER { return Err(ParseError { - message: format!("Found wrong waypoints header: {0:X?}", &bytes[0..8]), + message: format!("Found wrong waypoints header: {0:X?}", &bytes[Range::::from(FileSection::from(Section::Header))]), }); } - waypoints.normal = match parse_waypoints(&bytes[8..32].try_into().unwrap()) { + waypoints.normal = match parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Normal))].try_into().unwrap()) { Ok(res) => res, Err(e) => return Err(e), }; - waypoints.nightmare = parse_waypoints(&bytes[32..56].try_into().unwrap())?; - waypoints.hell = parse_waypoints(&bytes[56..80].try_into().unwrap())?; + waypoints.nightmare = parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Nightmare))].try_into().unwrap())?; + waypoints.hell = parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Hell))].try_into().unwrap())?; Ok(waypoints) } @@ -381,17 +409,17 @@ fn generate_difficulty(waypoints: &DifficultyWaypoints) -> [u8; 24] { flags.set_bit_range(18..27, fill_flags(&waypoints.act3, 9).bit_range(0..9)); flags.set_bit_range(27..30, fill_flags(&waypoints.act4, 3).bit_range(0..3)); flags.set_bit_range(30..39, fill_flags(&waypoints.act5, 9).bit_range(0..9)); - bytes[2..10].copy_from_slice(&u64::to_le_bytes(flags)); + bytes[Range::::from(FileSection::from(Section::DifficultyWaypointsValue))].copy_from_slice(&u64::to_le_bytes(flags)); bytes } pub fn generate(waypoints: &Waypoints) -> [u8; 81] { let mut bytes: [u8; 81] = [0x00; 81]; - bytes[0..8].copy_from_slice(&SECTION_HEADER); - bytes[8..32].copy_from_slice(&generate_difficulty(&waypoints.normal)); - bytes[32..56].copy_from_slice(&generate_difficulty(&waypoints.nightmare)); - bytes[56..80].copy_from_slice(&generate_difficulty(&waypoints.hell)); - bytes[80] = SECTION_TRAILER; + bytes[Range::::from(FileSection::from(Section::Header))].copy_from_slice(&SECTION_HEADER); + bytes[Range::::from(FileSection::from(Section::Normal))].copy_from_slice(&generate_difficulty(&waypoints.normal)); + bytes[Range::::from(FileSection::from(Section::Nightmare))].copy_from_slice(&generate_difficulty(&waypoints.nightmare)); + bytes[Range::::from(FileSection::from(Section::Hell))].copy_from_slice(&generate_difficulty(&waypoints.hell)); + bytes[Range::::from(FileSection::from(Section::Trailer))][0] = SECTION_TRAILER; bytes } @@ -445,11 +473,6 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ]; - let parsed_waypoints = match parse(&expected_bytes) { - Ok(res) => res, - Err(e) => panic!("parse_waypoints_test: {0}", e), - }; - assert_eq!(expected_bytes, generate(&Waypoints::default())); } } From 12025a9cad491298d16b0142a93b47be852757d3 Mon Sep 17 00:00:00 2001 From: feored Date: Wed, 26 Jul 2023 04:38:59 +0200 Subject: [PATCH 7/9] Prototype of full save file parsing. --- src/attributes.rs | 54 +++++++++++++-------- src/character/mod.rs | 2 +- src/items/mod.rs | 25 ++++++---- src/lib.rs | 109 ++++++++++++++++++++++++++++++++++++++----- src/npcs.rs | 20 +++++--- src/skills.rs | 37 +++++++-------- src/utils.rs | 6 +++ 7 files changed, 185 insertions(+), 68 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 67fbe20..4c4c725 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -2,7 +2,12 @@ use bit::BitIndex; use std::cmp; use std::fmt; -const TRAILER: u32 = 0x1FF; +use crate::ParseError; +use crate::utils::BytePosition; + +const SECTION_TRAILER: u32 = 0x1FF; + +const SECTION_HEADER : [u8;2] = [0x67, 0x66]; const STAT_HEADER_LENGTH: usize = 9; const STAT_NUMBER: usize = 16; @@ -114,12 +119,6 @@ pub struct Attributes { gold_stash: u32, } -/// Keep track of current byte and bit index in the attributes byte vector. -#[derive(Default)] -pub struct BytePosition { - pub current_byte: usize, - pub current_bit: usize, -} /// Write bits_count number of bits (LSB ordering) from bits_source into a vector of bytes. pub fn write_u8( @@ -196,9 +195,12 @@ pub fn write_u32( /// Get a byte-aligned vector of bytes representing a character's attribute. impl From for Vec { + // TODO: MAKE THSI INTO ITS OWN FUNCTION fn from(attributes: Attributes) -> Vec { let mut result: Vec = Vec::::new(); let mut byte_position: BytePosition = BytePosition::default(); + result.append(&mut SECTION_HEADER.to_vec()); + byte_position.current_byte=2; for header in 0..STAT_NUMBER { let stat = &STAT_KEY[header]; let header_as_u32 = header as u32; @@ -302,15 +304,20 @@ fn parse_bits(byte_vector: &Vec, byte_position: &mut BytePosition, bits_to_r /// Attributes are stored in a pair format (header:value). Not all attributes are required to be /// present. Headers are always 9 bits, and the STAT_KEY array contains the relevant Stat enum /// for every header parsed. Values span different number of bits stored in STAT_BITLENGTH. -pub fn parse_attributes_with_position( +pub fn parse_with_position( byte_vector: &Vec, byte_position: &mut BytePosition, -) -> Attributes { - let mut stats = Attributes::default(); +) -> Result { + if byte_vector[0..2] != SECTION_HEADER{ + return Err(ParseError{message: format!("Found wrong header for attributes, expected {0:X?} but found {1:X?}", SECTION_HEADER, &byte_vector[0..2])}) + } + byte_position.current_byte = 2; + let mut stats = Attributes::default(); + // println!("Parsed\n{0:?}", byte_vector); for _i in 0..STAT_NUMBER { let header = parse_bits(&byte_vector, byte_position, STAT_HEADER_LENGTH); - if header == TRAILER { + if header == SECTION_TRAILER { break; } @@ -340,15 +347,15 @@ pub fn parse_attributes_with_position( Stat::GoldStash => stats.gold_stash = value, } } - stats + Ok(stats) } /// Parse vector of bytes containing attributes data and return an Attributes struct. /// /// Calls parse_attributes_with_position and discards the byte_position information. -pub fn parse_attributes(byte_vector: &Vec) -> Attributes { +pub fn parse(byte_vector: &Vec) -> Result { let mut byte_position = BytePosition::default(); - parse_attributes_with_position(byte_vector, &mut byte_position) + parse_with_position(byte_vector, &mut byte_position) } #[cfg(test)] @@ -394,7 +401,10 @@ mod tests { gold_stash: 45964, }; let result: Vec = Vec::::from(expected_attributes); - let parsed_attributes = parse_attributes(&result); + let parsed_attributes = match parse(&result){ + Ok(res) => res, + Err(e) => panic!("Failed test_write_and_read_attributes: {e}") + }; assert_eq!(parsed_attributes, expected_attributes); } @@ -442,7 +452,7 @@ mod tests { fn test_parse_attributes_1() { // Level 1 newly-created barbarian let bytes: Vec = vec![ - 0x00, 0x3C, 0x08, 0xA0, 0x80, 0x00, 0x0A, 0x06, 0x64, 0x60, 0x00, 0xE0, 0x06, 0x1C, + 0x67, 0x66, 0x00, 0x3C, 0x08, 0xA0, 0x80, 0x00, 0x0A, 0x06, 0x64, 0x60, 0x00, 0xE0, 0x06, 0x1C, 0x00, 0xB8, 0x01, 0x08, 0x00, 0x14, 0x40, 0x02, 0x00, 0x05, 0xA0, 0x00, 0x80, 0x0B, 0x2C, 0x00, 0xE0, 0x02, 0x0C, 0x02, 0xFF, 0x01, ]; @@ -484,7 +494,10 @@ mod tests { gold_stash: 0, }; - let parsed_stats = parse_attributes(&bytes); + let parsed_stats = match parse(&bytes){ + Ok(res) => res, + Err(e) => panic!("Failed test_parse_attributes_1: {e}") + }; //println!("Parsed stats:"); //println!("{parsed_stats:?}"); @@ -496,7 +509,7 @@ mod tests { fn test_parse_attributes_2() { // Level 92 sorceress let bytes: Vec = vec![ - 0x00, 0x38, 0x09, 0x30, 0x82, 0x80, 0x11, 0x06, 0x10, 0x65, 0x00, 0x80, 0x9D, 0x1C, + 0x67, 0x66, 0x00, 0x38, 0x09, 0x30, 0x82, 0x80, 0x11, 0x06, 0x10, 0x65, 0x00, 0x80, 0x9D, 0x1C, 0x00, 0x98, 0x19, 0x08, 0x98, 0x2A, 0x45, 0x02, 0x80, 0x6C, 0xA0, 0x00, 0xA0, 0x44, 0x2C, 0x00, 0xF8, 0x0E, 0x0C, 0xB8, 0x0D, 0xDE, 0xA3, 0xD1, 0xF2, 0x1E, 0x30, 0xCE, 0x02, 0xF8, 0x0F, @@ -539,7 +552,10 @@ mod tests { gold_stash: 45964, }; - let parsed_stats = parse_attributes(&bytes); + let parsed_stats = match parse(&bytes){ + Ok(res) => res, + Err(e) => panic!("Failed test_parse_attributes_2: {e}") + }; // println!("Expected stats:"); // println!("{expected_stats:?}"); diff --git a/src/character/mod.rs b/src/character/mod.rs index 557e218..381b0d6 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -137,7 +137,7 @@ impl Default for Character { } } -fn parse(bytes: &[u8; 319]) -> Result { +pub fn parse(bytes: &[u8; 319]) -> Result { let mut character: Character = Character::default(); let active_weapon = u32_from(&bytes[Range::::from(FileSection::from(Section::WeaponSet))]); diff --git a/src/items/mod.rs b/src/items/mod.rs index 7a5c45d..51b7065 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -1,9 +1,18 @@ -pub fn generate_temp_items() -> Vec { - return vec![ - 0x4D, 0x04, 0x00, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x00, 0x80, 0x06, 0x17, - 0x03, 0x02, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x02, 0x80, 0x06, 0x17, 0x03, - 0x02, 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x04, 0x80, 0x06, 0x17, 0x03, 0x02, - 0x4A, 0x4D, 0x10, 0x00, 0xA2, 0x00, 0x65, 0x08, 0x06, 0x80, 0x06, 0x17, 0x03, 0x02, 0x4A, - 0x4D, 0x00, 0x00, 0x6A, 0x66, 0x6B, 0x66, 0x00, - ]; +#[derive(PartialEq, Eq, Debug, Default)] +pub struct Placeholder { + data: Vec } + +pub fn parse(byte_vector: &mut Vec) -> Placeholder{ + let mut placeholder: Placeholder = Placeholder {data: Vec::::new()}; + placeholder.data.append(byte_vector); + + placeholder +} + +pub fn generate(placeholder: &mut Placeholder) -> Vec{ + let mut byte_vector: Vec = Vec::::new(); + byte_vector.append(&mut placeholder.data); + + byte_vector +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d6e92de..e8c94e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,17 @@ variant_size_differences )] +use std::ops::Range; use bit::BitIndex; use std::fmt; +use utils::FileSection; +use utils::BytePosition; + +use character::Character as Character; +use quests::Quests as Quests; +use waypoints::Waypoints as Waypoints; +use attributes::Attributes as Attributes; +use skills::SkillSet as SkillSet; pub mod attributes; pub mod character; @@ -24,6 +33,8 @@ pub mod waypoints; const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; +const ATTRIBUTES_OFFSET : usize = 765; + const VERSION_100: u32 = 71; const VERSION_107: u32 = 87; const VERSION_108: u32 = 89; @@ -33,20 +44,75 @@ const VERSION_D2R_100: u32 = 97; const VERSION_D2R_240: u32 = 98; const VERSION_D2R_250: u32 = 99; -enum FileStructure{ +#[derive(PartialEq, Eq, Debug)] +enum Section{ Signature, Version, + FileSize, Checksum, Character, Quests, Waypoints, - Npcs, - Attributes, - Skills, - Items + Npcs + // Attributes has no fixed length, and therefore the Skills and Item sections that come after have no fixed offset +} + +impl From
for FileSection { + fn from(section: Section) -> FileSection { + match section { + Section::Signature => FileSection{offset:0, bytes:4}, + Section::Version => FileSection{offset:4, bytes:4}, + Section::FileSize => FileSection{offset:8, bytes:4}, + Section::Checksum => FileSection{offset:12, bytes:4}, + Section::Character => FileSection{offset:16, bytes:319}, + Section::Quests => FileSection {offset:335, bytes:298}, + Section::Waypoints => FileSection {offset: 633, bytes: 81}, + Section::Npcs => FileSection{offset:714, bytes: 51}, + } + } +} + +#[derive(PartialEq, Eq, Debug, Default)] +pub struct Save { + version: Version, + character: Character, + quests: Quests, + waypoints: Waypoints, + npcs: npcs::Placeholder, + attributes: Attributes, + skills: SkillSet, + items: items::Placeholder } +pub fn parse(byte_vector: &Vec) -> Result { + let mut save : Save = Save::default(); + + if byte_vector.len() < (765 + 32 + 16) { + // inferior to size of header + skills + minimum attributes, can't be valid + return Err(ParseError{message:format!("File is smaller than 765 bytes, the fixed size of the header. Length: {0:?}", byte_vector.len())}) + } + + if byte_vector[Range::::from(FileSection::from(Section::Signature))] != SIGNATURE { + return Err(ParseError{message:format!("File signature should be {:0X?} but is {1:X?}", SIGNATURE, &byte_vector[Range::::from(FileSection::from(Section::Signature))])}) + } + + + save.character = character::parse(&byte_vector[Range::::from(FileSection::from(Section::Character))].try_into().unwrap())?; + save.quests = quests::parse(&byte_vector[Range::::from(FileSection::from(Section::Quests))].try_into().unwrap())?; + save.waypoints = waypoints::parse(&byte_vector[Range::::from(FileSection::from(Section::Waypoints))].try_into().unwrap())?; + save.npcs = npcs::parse(&byte_vector[Range::::from(FileSection::from(Section::Npcs))].try_into().unwrap()); + + let mut byte_position : BytePosition = BytePosition::default(); + save.attributes = attributes::parse_with_position(&byte_vector[ATTRIBUTES_OFFSET..byte_vector.len()].try_into().unwrap(), &mut byte_position)?; + let skills_offset = ATTRIBUTES_OFFSET + byte_position.current_byte + 1; + save.skills = skills::parse(&byte_vector[skills_offset..(skills_offset+32)].try_into().unwrap(), save.character.class)?; + let items_offset = skills_offset + 32; + // TODO make byte_vector not mut + save.items = items::parse(&mut byte_vector[items_offset..byte_vector.len()].try_into().unwrap()); + Ok(save) +} + #[derive(Debug, Clone)] pub struct ParseError { @@ -64,13 +130,7 @@ impl fmt::Display for ParseError { } } -pub struct Save { - version: Version, - character: character::Character, -} - - -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Default)] pub enum Version { V100, V107, @@ -79,6 +139,7 @@ pub enum Version { V110, V200R, V240R, + #[default] V250R, } @@ -209,3 +270,27 @@ impl From for u8 { // } // checksum // } + + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + + #[test] + fn test_parse_save() { + let path: &Path = Path::new("C:/Users/feord/Saved Games/Diablo II Resurrected/Nyahallo.d2s"); + let save_file: Vec = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(e) => panic!("File invalid: {e:?}"), + }; + + let save = match parse(&save_file){ + Ok(res) => res, + Err(e) => panic!("PARSE TEST FAILED WITH ERROR: {e}") + }; + + println!("TEST SUCCESSFUL: {0:?}", save); + } +} \ No newline at end of file diff --git a/src/npcs.rs b/src/npcs.rs index 53e8174..88f48cc 100644 --- a/src/npcs.rs +++ b/src/npcs.rs @@ -1,20 +1,26 @@ const SECTION_HEADER: [u8; 3] = [0x77, 0x34, 0x00]; -#[derive(PartialEq, Eq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug)] pub struct Placeholder { - data: [u8;52] + data: [u8;51] } -pub fn parse(bytes: &[u8;52]) -> Placeholder{ - let mut placeholder: Placeholder = Placeholder {data: [0x00; 52]}; +impl Default for Placeholder { + fn default() -> Self { + Placeholder {data: [0x00;51]} + } +} + +pub fn parse(bytes: &[u8;51]) -> Placeholder{ + let mut placeholder: Placeholder = Placeholder {data: [0x00; 51]}; placeholder.data.copy_from_slice(bytes); placeholder } -pub fn generate(placeholder:Placeholder) -> [u8;52]{ - let mut bytes: [u8;52] = [0x00;52]; - bytes.copy_from_slice(&placeholder.data[0..52]); +pub fn generate(placeholder:Placeholder) -> [u8;51]{ + let mut bytes: [u8;51] = [0x00;51]; + bytes.copy_from_slice(&placeholder.data[0..51]); bytes } \ No newline at end of file diff --git a/src/skills.rs b/src/skills.rs index e86a611..2813217 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -1,4 +1,5 @@ use crate::Class; +use crate::ParseError; const SECTION_HEADER: [u8; 2] = [0x69, 0x66]; const SECTION_BYTES: usize = 32; @@ -380,11 +381,7 @@ pub struct Skill { } /// Holds entire skill tree of a character. -pub type Skillset = [Skill; 30]; - -pub fn check_valid(byte_vector: &Vec) -> bool { - return byte_vector.len() == SECTION_BYTES && byte_vector[0..2] == SECTION_HEADER; -} +pub type SkillSet = [Skill; 30]; /// Converts the value from 0-30 to the one found in the game's file by adding an offset specific to each class. fn get_offset(class: Class) -> usize { @@ -399,13 +396,11 @@ fn get_offset(class: Class) -> usize { } } -/// Parse a vector of bytes containg a character's skill tree (starting with header 0x69 0x66) and returns a Skillset on success. -pub fn parse_skills(byte_vector: &Vec, class: Class) -> Result { - let mut skills: Skillset = Skillset::default(); - if !check_valid(byte_vector) { - return Err( - "Byte vector is invalid, either not 32 bytes or does not contain the right header.", - ); +/// Parse a vector of bytes containg a character's skill tree (starting with header 0x69 0x66) and returns a SkillSet on success. +pub fn parse(byte_vector: &[u8;32], class: Class) -> Result { + let mut skills: SkillSet = SkillSet::default(); + if byte_vector[0..2] != SECTION_HEADER { + return Err(ParseError{message: format!("Found wrong header for skills section: expected {0:?}, found {1:?}", SECTION_HEADER, &byte_vector[0..2])}) } let offset = get_offset(class); for i in 0..30 { @@ -418,8 +413,8 @@ pub fn parse_skills(byte_vector: &Vec, class: Class) -> Result Vec { +/// Generates a byte vector from a given SkillSet +pub fn generate(skills: &SkillSet) -> Vec { let mut byte_vector: Vec = SECTION_HEADER.to_vec(); for i in 0..30 { byte_vector.push(skills[i].level); @@ -432,34 +427,34 @@ mod tests { use super::*; #[test] - fn test_parse_and_generate_skills() { - let byte_vector = vec![ + fn test_parse_and_generate() { + let byte_vector = [ 0x69, 0x66, 0x00, 0x01, 0x00, 0x14, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, ]; - let skills = parse_skills(&byte_vector, Class::Sorceress).unwrap(); + let skills = parse(&byte_vector, Class::Sorceress).unwrap(); for i in 0..30 { if skills[i].name == "Teleport" { assert!(skills[i].id == 54 && skills[i].level == 1); } } - let result = generate_skills(&skills); + let result = generate(&skills); assert_eq!(result, byte_vector); } #[test] - fn test_parse_skills() { - let byte_vector = vec![ + fn test_parse() { + let byte_vector = [ 0x69, 0x66, 0x00, 0x01, 0x00, 0x14, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, ]; - let skills = parse_skills(&byte_vector, Class::Sorceress).unwrap(); + let skills = parse(&byte_vector, Class::Sorceress).unwrap(); // println!("{0:?}", skills); for skill in skills { if skill.name == "Ice Blast" { diff --git a/src/utils.rs b/src/utils.rs index 37fd4b7..acc2149 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,6 +20,12 @@ impl From for Range { } } +/// Keep track of current byte and bit index in the attributes byte vector. +#[derive(Default, PartialEq, Eq, Debug,)] +pub struct BytePosition { + pub current_byte: usize, + pub current_bit: usize, +} pub fn u32_from(slice: &[u8]) -> u32 { u32::from_le_bytes( From 96566ee5238a1aa1b258790a9802694a8eae9f2f Mon Sep 17 00:00:00 2001 From: feored Date: Wed, 26 Jul 2023 05:54:41 +0200 Subject: [PATCH 8/9] First draft generating save --- assets/Joe.d2s | Bin 0 -> 945 bytes assets/Test.d2s | Bin 0 -> 853 bytes src/attributes.rs | 94 ++++++++---- src/character/mercenary.rs | 10 +- src/character/mod.rs | 298 +++++++++++++++++++++++++------------ src/items/mod.rs | 12 +- src/lib.rs | 259 +++++++++++++++++++++++--------- src/npcs.rs | 16 +- src/quests.rs | 36 ++--- src/skills.rs | 10 +- src/utils.rs | 16 +- src/waypoints.rs | 88 ++++++++--- 12 files changed, 567 insertions(+), 272 deletions(-) create mode 100644 assets/Joe.d2s create mode 100644 assets/Test.d2s diff --git a/assets/Joe.d2s b/assets/Joe.d2s new file mode 100644 index 0000000000000000000000000000000000000000..f45c7b517ca7da4c95f2b4c2a257eb5b1794d852 GIT binary patch literal 945 zcmWGt6}l>!fq`KoGXulT0Ol<~CI(Odim(XCF@hu(PuP?49}E~6{{JT*gUmt(U?W-n z`$NpeBpVnQ_Sg4=oq((jA3GzlLg7CX(CI)$Nu|kPHa_)U`KkD%iINM?&sSsvs?;LM zr9>G$%oK$OgMui4fr$}&P?0LmSZ+dc2&Jbn*l;XpVBlg)Nnm)uCd06Uk%K|Rfr){2 z0Yd|~4#NW`9;W|{nQ8b9_VVRm5MWruAj-gS-XFqZfwMT^EFKt(^~sS593Ty>OP;tZ z2{1G<@Cq{6%f&_6vNALA@%?863HgdTEacdF_@OjoBeR6WA9gQa28OJ(>@)@dUH^BW literal 0 HcmV?d00001 diff --git a/assets/Test.d2s b/assets/Test.d2s new file mode 100644 index 0000000000000000000000000000000000000000..2372bbaee2ba2d8bfee6251b633066d0fe9dab02 GIT binary patch literal 853 zcmWGt6}l>!fq@~EnSp`v=k0bN69XuKL9GUC|m#;1C(KCU Attributes{ + let amazon = (20, 25, 20, 15, 50, 84, 15); + let assassin = (20, 20, 20, 25, 50, 95, 25); + let barbarian = (30, 20, 25, 10, 55, 92, 10); + let paladin = (25, 20, 25, 15, 55, 89, 15); + let necromancer = (15, 25, 15, 25, 45, 79, 25); + let sorceress = (10, 25, 10, 35, 40, 74, 35); + let druid = (15, 20, 25, 20, 55, 84, 20); + + let stats = match class { + Class::Amazon => amazon, + Class::Assassin => assassin, + Class::Barbarian => barbarian, + Class::Paladin => paladin, + Class::Necromancer => necromancer, + Class::Sorceress => sorceress, + Class::Druid => druid + }; + + Attributes { + strength: stats.0, + dexterity: stats.1, + vitality: stats.2, + energy: stats.3, + stat_points_left: 0, + skill_points_left:0, + life_current: FixedPointStat {integer: stats.4, fraction: 0}, + life_base: FixedPointStat {integer: stats.4, fraction: 0}, + mana_current: FixedPointStat {integer: stats.6, fraction: 0}, + mana_base: FixedPointStat {integer: stats.6, fraction: 0}, + stamina_current: FixedPointStat {integer: stats.5, fraction: 0}, + stamina_base: FixedPointStat {integer: stats.5, fraction: 0}, + level: 1, + experience: 0, + gold_inventory: 0, + gold_stash: 0 + } + +} + + /// Write bits_count number of bits (LSB ordering) from bits_source into a vector of bytes. pub fn write_u8( byte_vector: &mut Vec, @@ -194,13 +236,11 @@ pub fn write_u32( } /// Get a byte-aligned vector of bytes representing a character's attribute. -impl From for Vec { - // TODO: MAKE THSI INTO ITS OWN FUNCTION - fn from(attributes: Attributes) -> Vec { - let mut result: Vec = Vec::::new(); +pub fn generate(attributes: &Attributes) -> Vec { + let mut result: Vec = Vec::::new(); let mut byte_position: BytePosition = BytePosition::default(); result.append(&mut SECTION_HEADER.to_vec()); - byte_position.current_byte=2; + byte_position.current_byte = 2; for header in 0..STAT_NUMBER { let stat = &STAT_KEY[header]; let header_as_u32 = header as u32; @@ -254,7 +294,6 @@ impl From for Vec { } result - } } /// Read a certain number of bits in a vector of bytes, starting at a given byte and bit index, and return a u32 with the value. @@ -308,9 +347,14 @@ pub fn parse_with_position( byte_vector: &Vec, byte_position: &mut BytePosition, ) -> Result { - - if byte_vector[0..2] != SECTION_HEADER{ - return Err(ParseError{message: format!("Found wrong header for attributes, expected {0:X?} but found {1:X?}", SECTION_HEADER, &byte_vector[0..2])}) + if byte_vector[0..2] != SECTION_HEADER { + return Err(ParseError { + message: format!( + "Found wrong header for attributes, expected {0:X?} but found {1:X?}", + SECTION_HEADER, + &byte_vector[0..2] + ), + }); } byte_position.current_byte = 2; let mut stats = Attributes::default(); @@ -400,10 +444,10 @@ mod tests { gold_inventory: 0, gold_stash: 45964, }; - let result: Vec = Vec::::from(expected_attributes); - let parsed_attributes = match parse(&result){ + let result: Vec = generate(&expected_attributes); + let parsed_attributes = match parse(&result) { Ok(res) => res, - Err(e) => panic!("Failed test_write_and_read_attributes: {e}") + Err(e) => panic!("Failed test_write_and_read_attributes: {e}"), }; assert_eq!(parsed_attributes, expected_attributes); @@ -452,9 +496,9 @@ mod tests { fn test_parse_attributes_1() { // Level 1 newly-created barbarian let bytes: Vec = vec![ - 0x67, 0x66, 0x00, 0x3C, 0x08, 0xA0, 0x80, 0x00, 0x0A, 0x06, 0x64, 0x60, 0x00, 0xE0, 0x06, 0x1C, - 0x00, 0xB8, 0x01, 0x08, 0x00, 0x14, 0x40, 0x02, 0x00, 0x05, 0xA0, 0x00, 0x80, 0x0B, - 0x2C, 0x00, 0xE0, 0x02, 0x0C, 0x02, 0xFF, 0x01, + 0x67, 0x66, 0x00, 0x3C, 0x08, 0xA0, 0x80, 0x00, 0x0A, 0x06, 0x64, 0x60, 0x00, 0xE0, + 0x06, 0x1C, 0x00, 0xB8, 0x01, 0x08, 0x00, 0x14, 0x40, 0x02, 0x00, 0x05, 0xA0, 0x00, + 0x80, 0x0B, 0x2C, 0x00, 0xE0, 0x02, 0x0C, 0x02, 0xFF, 0x01, ]; let expected_stats = Attributes { @@ -494,9 +538,9 @@ mod tests { gold_stash: 0, }; - let parsed_stats = match parse(&bytes){ + let parsed_stats = match parse(&bytes) { Ok(res) => res, - Err(e) => panic!("Failed test_parse_attributes_1: {e}") + Err(e) => panic!("Failed test_parse_attributes_1: {e}"), }; //println!("Parsed stats:"); @@ -509,10 +553,10 @@ mod tests { fn test_parse_attributes_2() { // Level 92 sorceress let bytes: Vec = vec![ - 0x67, 0x66, 0x00, 0x38, 0x09, 0x30, 0x82, 0x80, 0x11, 0x06, 0x10, 0x65, 0x00, 0x80, 0x9D, 0x1C, - 0x00, 0x98, 0x19, 0x08, 0x98, 0x2A, 0x45, 0x02, 0x80, 0x6C, 0xA0, 0x00, 0xA0, 0x44, - 0x2C, 0x00, 0xF8, 0x0E, 0x0C, 0xB8, 0x0D, 0xDE, 0xA3, 0xD1, 0xF2, 0x1E, 0x30, 0xCE, - 0x02, 0xF8, 0x0F, + 0x67, 0x66, 0x00, 0x38, 0x09, 0x30, 0x82, 0x80, 0x11, 0x06, 0x10, 0x65, 0x00, 0x80, + 0x9D, 0x1C, 0x00, 0x98, 0x19, 0x08, 0x98, 0x2A, 0x45, 0x02, 0x80, 0x6C, 0xA0, 0x00, + 0xA0, 0x44, 0x2C, 0x00, 0xF8, 0x0E, 0x0C, 0xB8, 0x0D, 0xDE, 0xA3, 0xD1, 0xF2, 0x1E, + 0x30, 0xCE, 0x02, 0xF8, 0x0F, ]; let expected_stats = Attributes { @@ -552,9 +596,9 @@ mod tests { gold_stash: 45964, }; - let parsed_stats = match parse(&bytes){ + let parsed_stats = match parse(&bytes) { Ok(res) => res, - Err(e) => panic!("Failed test_parse_attributes_2: {e}") + Err(e) => panic!("Failed test_parse_attributes_2: {e}"), }; // println!("Expected stats:"); // println!("{expected_stats:?}"); diff --git a/src/character/mercenary.rs b/src/character/mercenary.rs index 48d7a79..787c777 100644 --- a/src/character/mercenary.rs +++ b/src/character/mercenary.rs @@ -218,7 +218,6 @@ pub enum Barbarian { Frenzy, } - /// TODO: Make private, add getters and setters that throw GameLogicError #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct Mercenary { @@ -243,7 +242,7 @@ impl Default for Mercenary { } } -fn variant_id(variant: &Variant) -> u16{ +fn variant_id(variant: &Variant) -> u16 { let mut variant_id: u16 = 99; for i in 0..VARIANTS.len() { @@ -253,9 +252,10 @@ fn variant_id(variant: &Variant) -> u16{ } } if (variant_id as usize) > VARIANTS.len() { - panic!("There is no mercenary ID for type {0:?} recruited in {1:?}", - variant.0, variant.1); - + panic!( + "There is no mercenary ID for type {0:?} recruited in {1:?}", + variant.0, variant.1 + ); } variant_id } diff --git a/src/character/mod.rs b/src/character/mod.rs index 381b0d6..e93c834 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -10,16 +10,14 @@ use crate::GameLogicError; use crate::ParseError; use crate::utils::get_sys_time_in_secs; -use crate::utils::FileSection; use crate::utils::u32_from; use crate::utils::u8_from; +use crate::utils::FileSection; use mercenary::Mercenary; - pub mod mercenary; - const TITLES_CLASSIC_STANDARD_MALE: [&'static str; 4] = ["", "Sir", "Lord", "Baron"]; const TITLES_CLASSIC_STANDARD_FEMALE: [&'static str; 4] = ["", "Dame", "Lady", "Baroness"]; const TITLES_CLASSIC_HARDCORE_MALE: [&'static str; 4] = ["", "Count", "Duke", "King"]; @@ -47,32 +45,83 @@ enum Section { MapSeed, Mercenary, ResurrectedMenuAppearance, - Name + Name, } impl From
for FileSection { fn from(section: Section) -> FileSection { match section { - Section::WeaponSet => FileSection { offset: 0, bytes: 4 }, - Section::Status => FileSection { offset: 20, bytes: 1 }, - Section::Progression => FileSection { offset: 21, bytes: 1 }, - Section::Class => FileSection { offset: 24, bytes: 1 }, - Section::Level => FileSection { offset: 27, bytes: 1 }, - Section::LastPlayed => FileSection { offset: 32, bytes: 4 }, - Section::AssignedSkills => FileSection { offset: 40, bytes: 64 }, - Section::LeftMouseSkill => FileSection { offset: 104, bytes: 4 }, - Section::RightMouseSkill => FileSection { offset: 108, bytes: 4 }, - Section::LeftMouseSwitchSkill => FileSection { offset: 112, bytes: 4 }, - Section::RightMouseSwitchSkill => FileSection { offset: 116, bytes: 4 }, - Section::MenuAppearance => FileSection { offset: 120, bytes: 32 }, - Section::Difficulty => FileSection { offset: 152, bytes: 3 }, - Section::MapSeed => FileSection { offset: 155, bytes: 4 }, - Section::Mercenary=> FileSection { offset: 161, bytes: 14 }, - Section::ResurrectedMenuAppearance => FileSection { offset: 203, bytes: 48 }, - Section::Name => FileSection { offset: 251, bytes: 16 }, + Section::WeaponSet => FileSection { + offset: 0, + bytes: 4, + }, + Section::Status => FileSection { + offset: 20, + bytes: 1, + }, + Section::Progression => FileSection { + offset: 21, + bytes: 1, + }, + Section::Class => FileSection { + offset: 24, + bytes: 1, + }, + Section::Level => FileSection { + offset: 27, + bytes: 1, + }, + Section::LastPlayed => FileSection { + offset: 32, + bytes: 4, + }, + Section::AssignedSkills => FileSection { + offset: 40, + bytes: 64, + }, + Section::LeftMouseSkill => FileSection { + offset: 104, + bytes: 4, + }, + Section::RightMouseSkill => FileSection { + offset: 108, + bytes: 4, + }, + Section::LeftMouseSwitchSkill => FileSection { + offset: 112, + bytes: 4, + }, + Section::RightMouseSwitchSkill => FileSection { + offset: 116, + bytes: 4, + }, + Section::MenuAppearance => FileSection { + offset: 120, + bytes: 32, + }, + Section::Difficulty => FileSection { + offset: 152, + bytes: 3, + }, + Section::MapSeed => FileSection { + offset: 155, + bytes: 4, + }, + Section::Mercenary => FileSection { + offset: 161, + bytes: 14, + }, + Section::ResurrectedMenuAppearance => FileSection { + offset: 203, + bytes: 48, + }, + Section::Name => FileSection { + offset: 251, + bytes: 16, + }, } } -} +} #[derive(PartialEq, Eq, Debug)] pub struct Character { @@ -102,7 +151,7 @@ pub struct Status { ladder: bool, expansion: bool, hardcore: bool, - died: bool + died: bool, } #[derive(PartialEq, Eq, Clone, Copy, Debug)] @@ -140,14 +189,20 @@ impl Default for Character { pub fn parse(bytes: &[u8; 319]) -> Result { let mut character: Character = Character::default(); - let active_weapon = u32_from(&bytes[Range::::from(FileSection::from(Section::WeaponSet))]); + let active_weapon = + u32_from(&bytes[Range::::from(FileSection::from(Section::WeaponSet))]); character.weapon_set = WeaponSet::try_from(active_weapon)?; - character.status = Status::from(u8_from(&bytes[Range::::from(FileSection::from(Section::Status))])); + character.status = Status::from(u8_from( + &bytes[Range::::from(FileSection::from(Section::Status))], + )); - character.progression = u8_from(&bytes[Range::::from(FileSection::from(Section::Progression))]); + character.progression = + u8_from(&bytes[Range::::from(FileSection::from(Section::Progression))]); - let class = Class::try_from(u8_from(&bytes[Range::::from(FileSection::from(Section::Class))]))?; + let class = Class::try_from(u8_from( + &bytes[Range::::from(FileSection::from(Section::Class))], + ))?; character.class = match class { Class::Druid | Class::Assassin => { @@ -178,8 +233,9 @@ pub fn parse(bytes: &[u8; 319]) -> Result { _ => level, }; - character.last_played = u32_from(&bytes[Range::::from(FileSection::from(Section::LastPlayed))]); - let assigned_skills = &bytes[Range::::from(FileSection::from(Section::AssignedSkills))]; + character.last_played = + u32_from(&bytes[Range::::from(FileSection::from(Section::LastPlayed))]); + let assigned_skills = &bytes[Range::::from(FileSection::from(Section::AssignedSkills))]; for i in 0..16 { let start = i * 4; let assigned_skill = @@ -187,10 +243,14 @@ pub fn parse(bytes: &[u8; 319]) -> Result { character.assigned_skills[i] = assigned_skill; } - character.left_mouse_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))]); - character.right_mouse_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSkill))]); - character.left_mouse_switch_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))]); - character.right_mouse_switch_skill = u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))]); + character.left_mouse_skill = + u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))]); + character.right_mouse_skill = + u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSkill))]); + character.left_mouse_switch_skill = + u32_from(&bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))]); + character.right_mouse_switch_skill = + u32_from(&bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))]); let last_act = parse_last_act( &bytes[Range::::from(FileSection::from(Section::Difficulty))] @@ -198,9 +258,9 @@ pub fn parse(bytes: &[u8; 319]) -> Result { .unwrap(), ); - character.menu_appearance.clone_from_slice( - &bytes[Range::::from(FileSection::from(Section::MenuAppearance))] - ); + character + .menu_appearance + .clone_from_slice(&bytes[Range::::from(FileSection::from(Section::MenuAppearance))]); match last_act { Ok(last_act) => { @@ -210,27 +270,27 @@ pub fn parse(bytes: &[u8; 319]) -> Result { Err(e) => return Err(e), }; - character.map_seed = u32_from(&bytes[Range::::from(FileSection::from(Section::MapSeed))]); + character.map_seed = + u32_from(&bytes[Range::::from(FileSection::from(Section::MapSeed))]); character.mercenary = mercenary::parse( &bytes[Range::::from(FileSection::from(Section::Mercenary))] .try_into() .unwrap(), )?; - + character.resurrected_menu_appearence.clone_from_slice( - &bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))] + &bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))], ); - let utf8name = match str::from_utf8( - &bytes[Range::::from(FileSection::from(Section::Name))] - ) { - Ok(res) => res.trim_matches(char::from(0)), - Err(e) => { - return Err(ParseError { - message: format!("Invalid utf-8 for character name: {0:?}", e), - }); - } - }; + let utf8name = + match str::from_utf8(&bytes[Range::::from(FileSection::from(Section::Name))]) { + Ok(res) => res.trim_matches(char::from(0)), + Err(e) => { + return Err(ParseError { + message: format!("Invalid utf-8 for character name: {0:?}", e), + }); + } + }; character.name = String::from(utf8name); character.title = character.title(); @@ -239,36 +299,48 @@ pub fn parse(bytes: &[u8; 319]) -> Result { } pub fn generate(character: &Character) -> [u8; 319] { - let mut bytes : [u8;319] = [0x00; 319]; - - bytes[Range::::from(FileSection::from(Section::WeaponSet))].copy_from_slice(&u32::to_le_bytes(u32::from(character.weapon_set))); - bytes[Range::::from(FileSection::from(Section::Status))][0] = u8::from(character.status); - bytes[Range::::from(FileSection::from(Section::Progression))][0] = u8::from(character.progression); - bytes[Range::::from(FileSection::from(Section::Class))][0] = u8::from(character.class); - bytes[Range::::from(FileSection::from(Section::Level))][0] = u8::from(character.level); - bytes[Range::::from(FileSection::from(Section::LastPlayed))].copy_from_slice(&u32::to_le_bytes(u32::from(character.last_played))); - - - let mut assigned_skills : [u8; 64] = [0x00; 64]; + let mut bytes: [u8; 319] = [0x00; 319]; + + bytes[Range::::from(FileSection::from(Section::WeaponSet))] + .copy_from_slice(&u32::to_le_bytes(u32::from(character.weapon_set))); + bytes[Range::::from(FileSection::from(Section::Status)).start] = u8::from(character.status); + bytes[Range::::from(FileSection::from(Section::Progression)).start] = + u8::from(character.progression); + bytes[Range::::from(FileSection::from(Section::Class)).start] = u8::from(character.class); + bytes[Range::::from(FileSection::from(Section::Level)).start] = u8::from(character.level); + bytes[Range::::from(FileSection::from(Section::LastPlayed))] + .copy_from_slice(&u32::to_le_bytes(u32::from(character.last_played))); + + let mut assigned_skills: [u8; 64] = [0x00; 64]; for i in 0..16 { - assigned_skills[(i*4)..((i*4)+4)].copy_from_slice(&u32::to_le_bytes(character.assigned_skills[i])); + assigned_skills[(i * 4)..((i * 4) + 4)] + .copy_from_slice(&u32::to_le_bytes(character.assigned_skills[i])); } - bytes[Range::::from(FileSection::from(Section::AssignedSkills))].copy_from_slice(&assigned_skills); - bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))].copy_from_slice(&u32::to_le_bytes(character.left_mouse_skill)); - bytes[Range::::from(FileSection::from(Section::RightMouseSkill))].copy_from_slice(&u32::to_le_bytes(character.right_mouse_skill)); - bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))].copy_from_slice(&u32::to_le_bytes(character.left_mouse_switch_skill)); - bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))].copy_from_slice(&u32::to_le_bytes(character.right_mouse_switch_skill)); - bytes[Range::::from(FileSection::from(Section::MenuAppearance))].copy_from_slice(&character.menu_appearance); - bytes[Range::::from(FileSection::from(Section::Difficulty))].copy_from_slice(&generate_last_act(character.difficulty, character.act)); - bytes[Range::::from(FileSection::from(Section::MapSeed))].copy_from_slice(&u32::to_le_bytes(character.map_seed)); - bytes[Range::::from(FileSection::from(Section::Mercenary))].copy_from_slice(&mercenary::generate(&character.mercenary)); - bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))].copy_from_slice(&character.resurrected_menu_appearence); - let mut name : [u8; 16] = [0x00;16]; + bytes[Range::::from(FileSection::from(Section::AssignedSkills))] + .copy_from_slice(&assigned_skills); + bytes[Range::::from(FileSection::from(Section::LeftMouseSkill))] + .copy_from_slice(&u32::to_le_bytes(character.left_mouse_skill)); + bytes[Range::::from(FileSection::from(Section::RightMouseSkill))] + .copy_from_slice(&u32::to_le_bytes(character.right_mouse_skill)); + bytes[Range::::from(FileSection::from(Section::LeftMouseSwitchSkill))] + .copy_from_slice(&u32::to_le_bytes(character.left_mouse_switch_skill)); + bytes[Range::::from(FileSection::from(Section::RightMouseSwitchSkill))] + .copy_from_slice(&u32::to_le_bytes(character.right_mouse_switch_skill)); + bytes[Range::::from(FileSection::from(Section::MenuAppearance))] + .copy_from_slice(&character.menu_appearance); + bytes[Range::::from(FileSection::from(Section::Difficulty))] + .copy_from_slice(&generate_last_act(character.difficulty, character.act)); + bytes[Range::::from(FileSection::from(Section::MapSeed))] + .copy_from_slice(&u32::to_le_bytes(character.map_seed)); + bytes[Range::::from(FileSection::from(Section::Mercenary))] + .copy_from_slice(&mercenary::generate(&character.mercenary)); + bytes[Range::::from(FileSection::from(Section::ResurrectedMenuAppearance))] + .copy_from_slice(&character.resurrected_menu_appearence); + let mut name: [u8; 16] = [0x00; 16]; let name_as_bytes = character.name.as_bytes(); name[0..name_as_bytes.len()].clone_from_slice(&name_as_bytes); bytes[Range::::from(FileSection::from(Section::Name))].copy_from_slice(&name); - // Add padding, unknown bytes, etc bytes[25] = 0x10; bytes[26] = 0x1E; @@ -299,21 +371,13 @@ fn parse_last_act(bytes: &[u8; 3]) -> Result<(Difficulty, Act), ParseError> { Ok(last_act) } - - fn generate_last_act(difficulty: Difficulty, act: Act) -> [u8; 3] { let mut active_byte = u8::from(act); active_byte.set_bit(7, true); match difficulty { - Difficulty::Normal => { - [active_byte, 0x00, 0x00] - } - Difficulty::Nightmare => { - [0x00, active_byte, 0x00] - } - Difficulty::Hell => { - [0x00, 0x00, active_byte] - } + Difficulty::Normal => [active_byte, 0x00, 0x00], + Difficulty::Nightmare => [0x00, active_byte, 0x00], + Difficulty::Hell => [0x00, 0x00, active_byte], } } @@ -465,8 +529,6 @@ impl Character { } } - - #[cfg(test)] mod tests { use super::*; @@ -499,7 +561,7 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - let expected_result = Character{ + let expected_result = Character { weapon_set: WeaponSet::Main, status: Status { expansion: true, @@ -512,17 +574,37 @@ mod tests { class: Class::Sorceress, level: 92, last_played: 1690118587, - assigned_skills: [40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535], + assigned_skills: [ + 40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, + 65535, + ], left_mouse_skill: 55, right_mouse_skill: 54, left_mouse_switch_skill: 0, right_mouse_switch_skill: 54, - menu_appearance: [57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + menu_appearance: [ + 57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ], difficulty: Difficulty::Hell, act: Act::Act1, map_seed: 1402285379, - mercenary: Mercenary { dead: false, id: 1547718681, name_id: 7, name: "Emilio", variant: (mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), Difficulty::Hell), experience: 102341590 }, - resurrected_menu_appearence: [111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0, 0, 0, 0], + mercenary: Mercenary { + dead: false, + id: 1547718681, + name_id: 7, + name: "Emilio", + variant: ( + mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), + Difficulty::Hell, + ), + experience: 102341590, + }, + resurrected_menu_appearence: [ + 111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, + 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, + 0, 0, 0, 0, 0, + ], name: String::from("Nyahallo"), }; let parsed_result = match parse(&bytes) { @@ -538,7 +620,7 @@ mod tests { } #[test] - fn test_generate(){ + fn test_generate() { let expected_result: [u8; 319] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, @@ -565,7 +647,7 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - let character = Character{ + let character = Character { weapon_set: WeaponSet::Main, status: Status { expansion: true, @@ -578,23 +660,41 @@ mod tests { class: Class::Sorceress, level: 92, last_played: 1690118587, - assigned_skills: [40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535], + assigned_skills: [ + 40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, + 65535, + ], left_mouse_skill: 55, right_mouse_skill: 54, left_mouse_switch_skill: 0, right_mouse_switch_skill: 54, - menu_appearance: [57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + menu_appearance: [ + 57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ], difficulty: Difficulty::Hell, act: Act::Act1, map_seed: 1402285379, - mercenary: Mercenary { dead: false, id: 1547718681, name_id: 7, name: "Emilio", variant: (mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), Difficulty::Hell), experience: 102341590 }, - resurrected_menu_appearence: [111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0, 0, 0, 0], + mercenary: Mercenary { + dead: false, + id: 1547718681, + name_id: 7, + name: "Emilio", + variant: ( + mercenary::Class::DesertMercenary(mercenary::DesertMercenary::Might), + Difficulty::Hell, + ), + experience: 102341590, + }, + resurrected_menu_appearence: [ + 111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, + 0, 0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, + 0, 0, 0, 0, 0, + ], name: String::from("Nyahallo"), }; let generated_result = generate(&character); - assert_eq!(expected_result, generated_result); - } } diff --git a/src/items/mod.rs b/src/items/mod.rs index 51b7065..5b9b6b4 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -1,18 +1,20 @@ #[derive(PartialEq, Eq, Debug, Default)] pub struct Placeholder { - data: Vec + data: Vec, } -pub fn parse(byte_vector: &mut Vec) -> Placeholder{ - let mut placeholder: Placeholder = Placeholder {data: Vec::::new()}; +pub fn parse(byte_vector: &mut Vec) -> Placeholder { + let mut placeholder: Placeholder = Placeholder { + data: Vec::::new(), + }; placeholder.data.append(byte_vector); placeholder } -pub fn generate(placeholder: &mut Placeholder) -> Vec{ +pub fn generate(placeholder: &mut Placeholder) -> Vec { let mut byte_vector: Vec = Vec::::new(); byte_vector.append(&mut placeholder.data); byte_vector -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index e8c94e9..9edcb27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,17 @@ variant_size_differences )] -use std::ops::Range; use bit::BitIndex; use std::fmt; -use utils::FileSection; +use std::ops::Range; use utils::BytePosition; +use utils::FileSection; -use character::Character as Character; -use quests::Quests as Quests; -use waypoints::Waypoints as Waypoints; -use attributes::Attributes as Attributes; -use skills::SkillSet as SkillSet; +use attributes::Attributes; +use character::Character; +use quests::Quests; +use skills::SkillSet; +use waypoints::Waypoints; pub mod attributes; pub mod character; @@ -33,19 +33,12 @@ pub mod waypoints; const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; -const ATTRIBUTES_OFFSET : usize = 765; +const ATTRIBUTES_OFFSET: usize = 765; + -const VERSION_100: u32 = 71; -const VERSION_107: u32 = 87; -const VERSION_108: u32 = 89; -const VERSION_109: u32 = 92; -const VERSION_110: u32 = 96; -const VERSION_D2R_100: u32 = 97; -const VERSION_D2R_240: u32 = 98; -const VERSION_D2R_250: u32 = 99; #[derive(PartialEq, Eq, Debug)] -enum Section{ +enum Section { Signature, Version, FileSize, @@ -53,24 +46,47 @@ enum Section{ Character, Quests, Waypoints, - Npcs - // Attributes has no fixed length, and therefore the Skills and Item sections that come after have no fixed offset + Npcs, // Attributes has no fixed length, and therefore the Skills and Item sections that come after have no fixed offset } impl From
for FileSection { fn from(section: Section) -> FileSection { match section { - Section::Signature => FileSection{offset:0, bytes:4}, - Section::Version => FileSection{offset:4, bytes:4}, - Section::FileSize => FileSection{offset:8, bytes:4}, - Section::Checksum => FileSection{offset:12, bytes:4}, - Section::Character => FileSection{offset:16, bytes:319}, - Section::Quests => FileSection {offset:335, bytes:298}, - Section::Waypoints => FileSection {offset: 633, bytes: 81}, - Section::Npcs => FileSection{offset:714, bytes: 51}, + Section::Signature => FileSection { + offset: 0, + bytes: 4, + }, + Section::Version => FileSection { + offset: 4, + bytes: 4, + }, + Section::FileSize => FileSection { + offset: 8, + bytes: 4, + }, + Section::Checksum => FileSection { + offset: 12, + bytes: 4, + }, + Section::Character => FileSection { + offset: 16, + bytes: 319, + }, + Section::Quests => FileSection { + offset: 335, + bytes: 298, + }, + Section::Waypoints => FileSection { + offset: 633, + bytes: 81, + }, + Section::Npcs => FileSection { + offset: 714, + bytes: 51, + }, } } -} +} #[derive(PartialEq, Eq, Debug, Default)] pub struct Save { @@ -81,39 +97,101 @@ pub struct Save { npcs: npcs::Placeholder, attributes: Attributes, skills: SkillSet, - items: items::Placeholder + items: items::Placeholder, } - pub fn parse(byte_vector: &Vec) -> Result { - let mut save : Save = Save::default(); - + let mut save: Save = Save::default(); + if byte_vector.len() < (765 + 32 + 16) { // inferior to size of header + skills + minimum attributes, can't be valid - return Err(ParseError{message:format!("File is smaller than 765 bytes, the fixed size of the header. Length: {0:?}", byte_vector.len())}) + return Err(ParseError { + message: format!( + "File is smaller than 765 bytes, the fixed size of the header. Length: {0:?}", + byte_vector.len() + ), + }); } if byte_vector[Range::::from(FileSection::from(Section::Signature))] != SIGNATURE { - return Err(ParseError{message:format!("File signature should be {:0X?} but is {1:X?}", SIGNATURE, &byte_vector[Range::::from(FileSection::from(Section::Signature))])}) + return Err(ParseError { + message: format!( + "File signature should be {:0X?} but is {1:X?}", + SIGNATURE, + &byte_vector[Range::::from(FileSection::from(Section::Signature))] + ), + }); } - - save.character = character::parse(&byte_vector[Range::::from(FileSection::from(Section::Character))].try_into().unwrap())?; - save.quests = quests::parse(&byte_vector[Range::::from(FileSection::from(Section::Quests))].try_into().unwrap())?; - save.waypoints = waypoints::parse(&byte_vector[Range::::from(FileSection::from(Section::Waypoints))].try_into().unwrap())?; - save.npcs = npcs::parse(&byte_vector[Range::::from(FileSection::from(Section::Npcs))].try_into().unwrap()); - - let mut byte_position : BytePosition = BytePosition::default(); - save.attributes = attributes::parse_with_position(&byte_vector[ATTRIBUTES_OFFSET..byte_vector.len()].try_into().unwrap(), &mut byte_position)?; + save.character = character::parse( + &byte_vector[Range::::from(FileSection::from(Section::Character))] + .try_into() + .unwrap(), + )?; + save.quests = quests::parse( + &byte_vector[Range::::from(FileSection::from(Section::Quests))] + .try_into() + .unwrap(), + )?; + save.waypoints = waypoints::parse( + &byte_vector[Range::::from(FileSection::from(Section::Waypoints))] + .try_into() + .unwrap(), + )?; + save.npcs = npcs::parse( + &byte_vector[Range::::from(FileSection::from(Section::Npcs))] + .try_into() + .unwrap(), + ); + + let mut byte_position: BytePosition = BytePosition::default(); + save.attributes = attributes::parse_with_position( + &byte_vector[ATTRIBUTES_OFFSET..byte_vector.len()] + .try_into() + .unwrap(), + &mut byte_position, + )?; let skills_offset = ATTRIBUTES_OFFSET + byte_position.current_byte + 1; - save.skills = skills::parse(&byte_vector[skills_offset..(skills_offset+32)].try_into().unwrap(), save.character.class)?; + save.skills = skills::parse( + &byte_vector[skills_offset..(skills_offset + 32)] + .try_into() + .unwrap(), + save.character.class, + )?; let items_offset = skills_offset + 32; // TODO make byte_vector not mut - save.items = items::parse(&mut byte_vector[items_offset..byte_vector.len()].try_into().unwrap()); + save.items = items::parse( + &mut byte_vector[items_offset..byte_vector.len()] + .try_into() + .unwrap(), + ); Ok(save) } +pub fn generate(save: &mut Save) -> Vec { + let mut result : Vec = Vec::::new(); + result.resize(765, 0x00); + + result[Range::::from(FileSection::from(Section::Signature))].copy_from_slice(&SIGNATURE); + result[Range::::from(FileSection::from(Section::Version))].copy_from_slice(&u32::to_le_bytes(u32::from(save.version))); + result[Range::::from(FileSection::from(Section::Character))].copy_from_slice(&character::generate(&save.character)); + result[Range::::from(FileSection::from(Section::Quests))].copy_from_slice(&quests::generate(&save.quests)); + result[Range::::from(FileSection::from(Section::Waypoints))].copy_from_slice(&waypoints::generate(&save.waypoints)); + result[Range::::from(FileSection::from(Section::Npcs))].copy_from_slice(&npcs::generate(save.npcs)); + result.append(&mut attributes::generate(&save.attributes)); + result.append(&mut skills::generate(&save.skills)); + result.append(&mut items::generate(&mut save.items)); + + let length = result.len() as u32; + result[Range::::from(FileSection::from(Section::FileSize))].copy_from_slice(&u32::to_le_bytes(length)); + let checksum = calc_checksum(&result); + result[Range::::from(FileSection::from(Section::Checksum))].copy_from_slice(&i32::to_le_bytes(checksum)); + + + result +} + #[derive(Debug, Clone)] pub struct ParseError { message: String, @@ -130,7 +208,7 @@ impl fmt::Display for ParseError { } } -#[derive(PartialEq, Eq, Debug, Default)] +#[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub enum Version { V100, V107, @@ -143,6 +221,22 @@ pub enum Version { V250R, } +impl From for u32{ + fn from(version: Version) -> u32 { + match version { + Version::V100 => 71, + Version::V107 => 87, + Version::V108 => 89, + Version::V109 => 92, + Version::V110 => 96, + Version::V200R => 97, + Version::V240R => 98, + Version::V250R => 99 + } + } +} + + #[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Difficulty { #[default] @@ -253,44 +347,61 @@ impl From for u8 { } - -// fn check_valid_signature(bytes: &Vec) -> bool { -// bytes[get_offset_range(OffsetID::Signature)] == SIGNATURE -// } - -// pub fn calc_checksum(bytes: &Vec) -> i32 { -// let mut checksum: i32 = 0; -// let (checksum_start, checksum_end) = get_file_bytes_range(OffsetID::Checksum); -// for i in 0..bytes.len() { -// let mut ch: i32 = bytes[i] as i32; -// if i >= checksum_start && i < checksum_end { -// ch = 0; -// } -// checksum = (checksum << 1) + ch + ((checksum < 0) as i32); -// } -// checksum -// } - +pub fn calc_checksum(bytes: &Vec) -> i32 { + let mut checksum: i32 = 0; + let range = Range::::from(FileSection::from(Section::Checksum)); + for i in 0..bytes.len() { + let mut ch: i32 = bytes[i] as i32; + if i >= range.start && i < range.end { + ch = 0; + } + checksum = (checksum << 1) + ch + ((checksum < 0) as i32); + } + checksum +} #[cfg(test)] mod tests { use super::*; use std::path::Path; - + use std::io::Write; + use std::fs; #[test] fn test_parse_save() { - let path: &Path = Path::new("C:/Users/feord/Saved Games/Diablo II Resurrected/Nyahallo.d2s"); + let path: &Path = + Path::new("assets/Joe.d2s"); let save_file: Vec = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(e) => panic!("File invalid: {e:?}"), - }; + Ok(bytes) => bytes, + Err(e) => panic!("File invalid: {e:?}"), + }; - let save = match parse(&save_file){ - Ok(res) => res, - Err(e) => panic!("PARSE TEST FAILED WITH ERROR: {e}") - }; + let save = match parse(&save_file) { + Ok(res) => res, + Err(e) => panic!("PARSE TEST FAILED WITH ERROR: {e}"), + }; - println!("TEST SUCCESSFUL: {0:?}", save); + //println!("TEST SUCCESSFUL: {0:?}", save); } -} \ No newline at end of file + + #[test] + fn test_generate_save() { + let path: &Path = + Path::new("assets/Test.d2s"); + + let mut save : Save = Save::default(); + save.character.set_name(String::from("test")); + save.attributes = attributes::default_character(Class::Amazon); + + let generated_save = generate(&mut save); + + let mut file = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(path) + .unwrap(); + + + file.write_all(&generated_save).unwrap(); + } +} diff --git a/src/npcs.rs b/src/npcs.rs index 88f48cc..11010ce 100644 --- a/src/npcs.rs +++ b/src/npcs.rs @@ -1,26 +1,26 @@ const SECTION_HEADER: [u8; 3] = [0x77, 0x34, 0x00]; -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct Placeholder { - data: [u8;51] + data: [u8; 51], } impl Default for Placeholder { fn default() -> Self { - Placeholder {data: [0x00;51]} + Placeholder { data: [0x00; 51] } } } -pub fn parse(bytes: &[u8;51]) -> Placeholder{ - let mut placeholder: Placeholder = Placeholder {data: [0x00; 51]}; +pub fn parse(bytes: &[u8; 51]) -> Placeholder { + let mut placeholder: Placeholder = Placeholder { data: [0x00; 51] }; placeholder.data.copy_from_slice(bytes); placeholder } -pub fn generate(placeholder:Placeholder) -> [u8;51]{ - let mut bytes: [u8;51] = [0x00;51]; +pub fn generate(placeholder: Placeholder) -> [u8; 51] { + let mut bytes: [u8; 51] = [0x00; 51]; bytes.copy_from_slice(&placeholder.data[0..51]); bytes -} \ No newline at end of file +} diff --git a/src/quests.rs b/src/quests.rs index c051368..bd9610b 100644 --- a/src/quests.rs +++ b/src/quests.rs @@ -218,11 +218,7 @@ pub struct QuestFlags { impl fmt::Display for QuestFlags { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Completed difficulty: {0:?}", - self.completed_difficulty - ) + write!(f, "Completed difficulty: {0:?}", self.completed_difficulty) } } @@ -234,7 +230,7 @@ impl Quest { self.set_stage(Stage::Completed, true); self.set_stage(Stage::Closed, true); } - fn clear(&mut self){ + fn clear(&mut self) { self.flags = 0; } } @@ -272,8 +268,12 @@ fn write_flags(bytes: &mut Vec, flags: &QuestFlags) { .copy_from_slice(&u16::to_le_bytes(flags.completed_base_game as u16)); bytes[Range::::from(FileSection::from(Section::ResetStats))] .copy_from_slice(&u8::to_le_bytes(flags.reset_stats as u8)); - bytes[Range::::from(FileSection::from(Section::DifficultyComplete))] - .copy_from_slice(match flags.completed_difficulty { true => &[0x80], false => &[0x00]}); + bytes[Range::::from(FileSection::from(Section::DifficultyComplete))].copy_from_slice( + match flags.completed_difficulty { + true => &[0x80], + false => &[0x00], + }, + ); } fn parse_flags(bytes: &[u8; 96]) -> Result { @@ -310,7 +310,7 @@ fn write_quests(byte_vector: &mut Vec, quests: &QuestSet) { let quests_number = match act { 0..=2 | 4 => 6, 3 => 3, - _ => unreachable!() + _ => unreachable!(), }; for i in 0..quests_number { let quest_index = i + match act { @@ -319,7 +319,7 @@ fn write_quests(byte_vector: &mut Vec, quests: &QuestSet) { 2 => 12, 3 => 18, 4 => 21, - _ => unreachable!() + _ => unreachable!(), }; let quest_value = u16::to_le_bytes(quests[quest_index].flags); // println!{"@@@@@ Quest: {0}", quests[quest_index]}; @@ -335,7 +335,7 @@ fn write_quests(byte_vector: &mut Vec, quests: &QuestSet) { 2 => Section::Act3Quests, 3 => Section::Act4Quests, 4 => Section::Act5Quests, - _ => unreachable!() + _ => unreachable!(), }; // println!{"############# Writing quests: {0:X?}", act_quests}; byte_vector[Range::::from(FileSection::from(section))].copy_from_slice(&act_quests); @@ -353,7 +353,7 @@ fn parse_quests(bytes: &[u8; 96], difficulty: Difficulty) -> Result Result Result Result Result Vec { hell.resize(96, 0x00); write_quests(&mut hell, &all_quests.hell.quests); write_flags(&mut hell, &all_quests.hell.flags); - byte_vector[202..298].copy_from_slice(&hell); + byte_vector[202..298].copy_from_slice(&hell); byte_vector } diff --git a/src/skills.rs b/src/skills.rs index 2813217..2456c1d 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -397,10 +397,16 @@ fn get_offset(class: Class) -> usize { } /// Parse a vector of bytes containg a character's skill tree (starting with header 0x69 0x66) and returns a SkillSet on success. -pub fn parse(byte_vector: &[u8;32], class: Class) -> Result { +pub fn parse(byte_vector: &[u8; 32], class: Class) -> Result { let mut skills: SkillSet = SkillSet::default(); if byte_vector[0..2] != SECTION_HEADER { - return Err(ParseError{message: format!("Found wrong header for skills section: expected {0:?}, found {1:?}", SECTION_HEADER, &byte_vector[0..2])}) + return Err(ParseError { + message: format!( + "Found wrong header for skills section: expected {0:?}, found {1:?}", + SECTION_HEADER, + &byte_vector[0..2] + ), + }); } let offset = get_offset(class); for i in 0..30 { diff --git a/src/utils.rs b/src/utils.rs index acc2149..46d5888 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ -use std::time::SystemTime; use std::ops::Range; +use std::time::SystemTime; pub fn get_sys_time_in_secs() -> u32 { match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { @@ -21,26 +21,18 @@ impl From for Range { } /// Keep track of current byte and bit index in the attributes byte vector. -#[derive(Default, PartialEq, Eq, Debug,)] +#[derive(Default, PartialEq, Eq, Debug)] pub struct BytePosition { pub current_byte: usize, pub current_bit: usize, } pub fn u32_from(slice: &[u8]) -> u32 { - u32::from_le_bytes( - slice - .try_into() - .unwrap(), - ) + u32::from_le_bytes(slice.try_into().unwrap()) } pub fn u16_from(slice: &[u8]) -> u16 { - u16::from_le_bytes( - slice - .try_into() - .unwrap(), - ) + u16::from_le_bytes(slice.try_into().unwrap()) } pub fn u8_from(slice: &[u8]) -> u8 { diff --git a/src/waypoints.rs b/src/waypoints.rs index 432fcbe..6e73caf 100644 --- a/src/waypoints.rs +++ b/src/waypoints.rs @@ -3,36 +3,54 @@ use std::ops::Range; use bit::BitIndex; - use crate::utils::FileSection; use crate::Act; use crate::ParseError; -enum Section{ +enum Section { Header, Normal, Nightmare, Hell, Trailer, DifficultyHeader, - DifficultyWaypointsValue + DifficultyWaypointsValue, } - impl From
for FileSection { fn from(section: Section) -> FileSection { match section { - Section::Header => FileSection { offset: 0, bytes: 8 }, - Section::Normal => FileSection { offset: 8, bytes: 24}, - Section::Nightmare => FileSection { offset: 32, bytes: 24}, - Section::Hell => FileSection { offset: 56, bytes: 24}, - Section::Trailer => FileSection { offset: 80, bytes: 1}, - Section::DifficultyHeader => FileSection {offset: 0, bytes: 2}, - Section::DifficultyWaypointsValue => FileSection {offset:2, bytes: 8} + Section::Header => FileSection { + offset: 0, + bytes: 8, + }, + Section::Normal => FileSection { + offset: 8, + bytes: 24, + }, + Section::Nightmare => FileSection { + offset: 32, + bytes: 24, + }, + Section::Hell => FileSection { + offset: 56, + bytes: 24, + }, + Section::Trailer => FileSection { + offset: 80, + bytes: 1, + }, + Section::DifficultyHeader => FileSection { + offset: 0, + bytes: 2, + }, + Section::DifficultyWaypointsValue => FileSection { + offset: 2, + bytes: 8, + }, } } -} - +} const SECTION_HEADER: [u8; 8] = [0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00]; const DIFFICULTY_HEADER: [u8; 2] = [0x02, 0x01]; @@ -320,7 +338,9 @@ impl TryFrom for Waypoint { fn parse_waypoints(bytes: &[u8; 24]) -> Result { let mut waypoints: DifficultyWaypoints = DifficultyWaypoints::default(); - if bytes[Range::::from(FileSection::from(Section::DifficultyHeader))] != DIFFICULTY_HEADER { + if bytes[Range::::from(FileSection::from(Section::DifficultyHeader))] + != DIFFICULTY_HEADER + { return Err(ParseError { message: format!( "Found wrong waypoint difficulty header: {0:X?}", @@ -381,15 +401,30 @@ pub fn parse(bytes: &[u8; 81]) -> Result { let mut waypoints = Waypoints::default(); if bytes[Range::::from(FileSection::from(Section::Header))] != SECTION_HEADER { return Err(ParseError { - message: format!("Found wrong waypoints header: {0:X?}", &bytes[Range::::from(FileSection::from(Section::Header))]), + message: format!( + "Found wrong waypoints header: {0:X?}", + &bytes[Range::::from(FileSection::from(Section::Header))] + ), }); } - waypoints.normal = match parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Normal))].try_into().unwrap()) { + waypoints.normal = match parse_waypoints( + &bytes[Range::::from(FileSection::from(Section::Normal))] + .try_into() + .unwrap(), + ) { Ok(res) => res, Err(e) => return Err(e), }; - waypoints.nightmare = parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Nightmare))].try_into().unwrap())?; - waypoints.hell = parse_waypoints(&bytes[Range::::from(FileSection::from(Section::Hell))].try_into().unwrap())?; + waypoints.nightmare = parse_waypoints( + &bytes[Range::::from(FileSection::from(Section::Nightmare))] + .try_into() + .unwrap(), + )?; + waypoints.hell = parse_waypoints( + &bytes[Range::::from(FileSection::from(Section::Hell))] + .try_into() + .unwrap(), + )?; Ok(waypoints) } @@ -409,17 +444,22 @@ fn generate_difficulty(waypoints: &DifficultyWaypoints) -> [u8; 24] { flags.set_bit_range(18..27, fill_flags(&waypoints.act3, 9).bit_range(0..9)); flags.set_bit_range(27..30, fill_flags(&waypoints.act4, 3).bit_range(0..3)); flags.set_bit_range(30..39, fill_flags(&waypoints.act5, 9).bit_range(0..9)); - bytes[Range::::from(FileSection::from(Section::DifficultyWaypointsValue))].copy_from_slice(&u64::to_le_bytes(flags)); + bytes[Range::::from(FileSection::from(Section::DifficultyWaypointsValue))] + .copy_from_slice(&u64::to_le_bytes(flags)); bytes } pub fn generate(waypoints: &Waypoints) -> [u8; 81] { let mut bytes: [u8; 81] = [0x00; 81]; - bytes[Range::::from(FileSection::from(Section::Header))].copy_from_slice(&SECTION_HEADER); - bytes[Range::::from(FileSection::from(Section::Normal))].copy_from_slice(&generate_difficulty(&waypoints.normal)); - bytes[Range::::from(FileSection::from(Section::Nightmare))].copy_from_slice(&generate_difficulty(&waypoints.nightmare)); - bytes[Range::::from(FileSection::from(Section::Hell))].copy_from_slice(&generate_difficulty(&waypoints.hell)); - bytes[Range::::from(FileSection::from(Section::Trailer))][0] = SECTION_TRAILER; + bytes[Range::::from(FileSection::from(Section::Header))] + .copy_from_slice(&SECTION_HEADER); + bytes[Range::::from(FileSection::from(Section::Normal))] + .copy_from_slice(&generate_difficulty(&waypoints.normal)); + bytes[Range::::from(FileSection::from(Section::Nightmare))] + .copy_from_slice(&generate_difficulty(&waypoints.nightmare)); + bytes[Range::::from(FileSection::from(Section::Hell))] + .copy_from_slice(&generate_difficulty(&waypoints.hell)); + bytes[Range::::from(FileSection::from(Section::Trailer)).start] = SECTION_TRAILER; bytes } From 070568c2c4929c9cb67416c29eb8ee5249f7fda9 Mon Sep 17 00:00:00 2001 From: feored Date: Wed, 26 Jul 2023 19:06:08 +0200 Subject: [PATCH 9/9] Fix npcs/items placeholders, generated level 1 character --- assets/Test.d2s | Bin 853 -> 866 bytes src/attributes.rs | 149 ++++++++++++++++++++++++------------------- src/character/mod.rs | 9 ++- src/items/mod.rs | 14 ++-- src/lib.rs | 62 +++++++++--------- src/npcs.rs | 25 +++++--- 6 files changed, 142 insertions(+), 117 deletions(-) diff --git a/assets/Test.d2s b/assets/Test.d2s index 2372bbaee2ba2d8bfee6251b633066d0fe9dab02..d9935acfb2fb73646355b181794111931b05e394 100644 GIT binary patch delta 77 zcmcc0_J~a&bXDl8WCjL?BxVMN!=^!xCOV3UC;<5k0&SDmbl>b05Igzno;|e7v argD>sj)s%}GDYxu`7$toa8_D&8Up~Dl^3Z1 delta 60 zcmaFFc9l&abXDl8WCjL?P-X@O#-F#_CpwCVC;<5k0&b05Igzno;|e9F MiH^pT|1w1Y04q-v6951J diff --git a/src/attributes.rs b/src/attributes.rs index 9c7f0b4..e39fc97 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -2,8 +2,8 @@ use bit::BitIndex; use std::cmp; use std::fmt; -use crate::Class; use crate::utils::BytePosition; +use crate::Class; use crate::ParseError; const SECTION_TRAILER: u32 = 0x1FF; @@ -120,8 +120,7 @@ pub struct Attributes { gold_stash: u32, } - -pub fn default_character(class : Class) -> Attributes{ +pub fn default_character(class: Class) -> Attributes { let amazon = (20, 25, 20, 15, 50, 84, 15); let assassin = (20, 20, 20, 25, 50, 95, 25); let barbarian = (30, 20, 25, 10, 55, 92, 10); @@ -137,7 +136,7 @@ pub fn default_character(class : Class) -> Attributes{ Class::Paladin => paladin, Class::Necromancer => necromancer, Class::Sorceress => sorceress, - Class::Druid => druid + Class::Druid => druid, }; Attributes { @@ -146,22 +145,38 @@ pub fn default_character(class : Class) -> Attributes{ vitality: stats.2, energy: stats.3, stat_points_left: 0, - skill_points_left:0, - life_current: FixedPointStat {integer: stats.4, fraction: 0}, - life_base: FixedPointStat {integer: stats.4, fraction: 0}, - mana_current: FixedPointStat {integer: stats.6, fraction: 0}, - mana_base: FixedPointStat {integer: stats.6, fraction: 0}, - stamina_current: FixedPointStat {integer: stats.5, fraction: 0}, - stamina_base: FixedPointStat {integer: stats.5, fraction: 0}, + skill_points_left: 0, + life_current: FixedPointStat { + integer: stats.4, + fraction: 0, + }, + life_base: FixedPointStat { + integer: stats.4, + fraction: 0, + }, + mana_current: FixedPointStat { + integer: stats.6, + fraction: 0, + }, + mana_base: FixedPointStat { + integer: stats.6, + fraction: 0, + }, + stamina_current: FixedPointStat { + integer: stats.5, + fraction: 0, + }, + stamina_base: FixedPointStat { + integer: stats.5, + fraction: 0, + }, level: 1, experience: 0, gold_inventory: 0, - gold_stash: 0 + gold_stash: 0, } - } - /// Write bits_count number of bits (LSB ordering) from bits_source into a vector of bytes. pub fn write_u8( byte_vector: &mut Vec, @@ -238,62 +253,62 @@ pub fn write_u32( /// Get a byte-aligned vector of bytes representing a character's attribute. pub fn generate(attributes: &Attributes) -> Vec { let mut result: Vec = Vec::::new(); - let mut byte_position: BytePosition = BytePosition::default(); - result.append(&mut SECTION_HEADER.to_vec()); - byte_position.current_byte = 2; - for header in 0..STAT_NUMBER { - let stat = &STAT_KEY[header]; - let header_as_u32 = header as u32; - - write_u32( - &mut result, - &mut byte_position, - header_as_u32, - STAT_HEADER_LENGTH, - ); + let mut byte_position: BytePosition = BytePosition::default(); + result.append(&mut SECTION_HEADER.to_vec()); + byte_position.current_byte = 2; + for header in 0..STAT_NUMBER { + let stat = &STAT_KEY[header]; + let header_as_u32 = header as u32; + + write_u32( + &mut result, + &mut byte_position, + header_as_u32, + STAT_HEADER_LENGTH, + ); - let value: u32 = match stat { - Stat::Strength => attributes.strength, - Stat::Energy => attributes.energy, - Stat::Dexterity => attributes.dexterity, - Stat::Vitality => attributes.vitality, - Stat::StatPointsLeft => attributes.stat_points_left, - Stat::SkillPointsLeft => attributes.skill_points_left, - Stat::LifeCurrent => u32::from(&attributes.life_current), - Stat::LifeBase => u32::from(&attributes.life_base), - Stat::ManaCurrent => u32::from(&attributes.mana_current), - Stat::ManaBase => u32::from(&attributes.mana_base), - Stat::StaminaCurrent => u32::from(&attributes.stamina_current), - Stat::StaminaBase => u32::from(&attributes.stamina_base), - Stat::Level => attributes.level, - Stat::Experience => attributes.experience, - Stat::GoldInventory => attributes.gold_inventory, - Stat::GoldStash => attributes.gold_stash, - }; - - write_u32( - &mut result, - &mut byte_position, - value, - STAT_BITLENGTH[header], - ); - } - // add trailing 0x1FF to signal end of attributes section - write_u32(&mut result, &mut byte_position, 0x1FF, STAT_HEADER_LENGTH); + let value: u32 = match stat { + Stat::Strength => attributes.strength, + Stat::Energy => attributes.energy, + Stat::Dexterity => attributes.dexterity, + Stat::Vitality => attributes.vitality, + Stat::StatPointsLeft => attributes.stat_points_left, + Stat::SkillPointsLeft => attributes.skill_points_left, + Stat::LifeCurrent => u32::from(&attributes.life_current), + Stat::LifeBase => u32::from(&attributes.life_base), + Stat::ManaCurrent => u32::from(&attributes.mana_current), + Stat::ManaBase => u32::from(&attributes.mana_base), + Stat::StaminaCurrent => u32::from(&attributes.stamina_current), + Stat::StaminaBase => u32::from(&attributes.stamina_base), + Stat::Level => attributes.level, + Stat::Experience => attributes.experience, + Stat::GoldInventory => attributes.gold_inventory, + Stat::GoldStash => attributes.gold_stash, + }; - // If we end in the middle of a byte, add some padding so that the next section - // starts on a new byte - if byte_position.current_bit == 8 { - byte_position.current_byte += 1; - byte_position.current_bit = 0; - } else if byte_position.current_bit != 0 { - let bits_to_fill = 8 - byte_position.current_bit; - write_u8(&mut result, &mut byte_position, 0, bits_to_fill); - byte_position.current_byte += 1; - byte_position.current_bit = 0; - } + write_u32( + &mut result, + &mut byte_position, + value, + STAT_BITLENGTH[header], + ); + } + // add trailing 0x1FF to signal end of attributes section + write_u32(&mut result, &mut byte_position, 0x1FF, STAT_HEADER_LENGTH); + + // If we end in the middle of a byte, add some padding so that the next section + // starts on a new byte + if byte_position.current_bit == 8 { + byte_position.current_byte += 1; + byte_position.current_bit = 0; + } else if byte_position.current_bit != 0 { + let bits_to_fill = 8 - byte_position.current_bit; + write_u8(&mut result, &mut byte_position, 0, bits_to_fill); + byte_position.current_byte += 1; + byte_position.current_bit = 0; + } - result + result } /// Read a certain number of bits in a vector of bytes, starting at a given byte and bit index, and return a u32 with the value. diff --git a/src/character/mod.rs b/src/character/mod.rs index e93c834..673917b 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -303,11 +303,14 @@ pub fn generate(character: &Character) -> [u8; 319] { bytes[Range::::from(FileSection::from(Section::WeaponSet))] .copy_from_slice(&u32::to_le_bytes(u32::from(character.weapon_set))); - bytes[Range::::from(FileSection::from(Section::Status)).start] = u8::from(character.status); + bytes[Range::::from(FileSection::from(Section::Status)).start] = + u8::from(character.status); bytes[Range::::from(FileSection::from(Section::Progression)).start] = u8::from(character.progression); - bytes[Range::::from(FileSection::from(Section::Class)).start] = u8::from(character.class); - bytes[Range::::from(FileSection::from(Section::Level)).start] = u8::from(character.level); + bytes[Range::::from(FileSection::from(Section::Class)).start] = + u8::from(character.class); + bytes[Range::::from(FileSection::from(Section::Level)).start] = + u8::from(character.level); bytes[Range::::from(FileSection::from(Section::LastPlayed))] .copy_from_slice(&u32::to_le_bytes(u32::from(character.last_played))); diff --git a/src/items/mod.rs b/src/items/mod.rs index 5b9b6b4..770f2c2 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -1,20 +1,22 @@ +const SECTION_HEADER : [u8;2] = [0x4A, 0x4D]; + +const NO_ITEMS : [u8;13] = [0x4A, 0x4D, 0x00, 0x00, 0x4A, 0x4D, 0x00, 0x00, 0x6A, 0x66, 0x6B, 0x66, 0x00]; + #[derive(PartialEq, Eq, Debug, Default)] pub struct Placeholder { data: Vec, } -pub fn parse(byte_vector: &mut Vec) -> Placeholder { +pub fn parse(byte_vector: &Vec) -> Placeholder { let mut placeholder: Placeholder = Placeholder { data: Vec::::new(), }; - placeholder.data.append(byte_vector); + placeholder.data = byte_vector.clone(); placeholder } -pub fn generate(placeholder: &mut Placeholder) -> Vec { - let mut byte_vector: Vec = Vec::::new(); - byte_vector.append(&mut placeholder.data); - +pub fn generate(placeholder: &Placeholder) -> Vec { + let byte_vector: Vec = NO_ITEMS.to_vec(); byte_vector } diff --git a/src/lib.rs b/src/lib.rs index 9edcb27..753847e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,6 @@ const SIGNATURE: [u8; 4] = [0x55, 0xAA, 0x55, 0xAA]; const ATTRIBUTES_OFFSET: usize = 765; - - #[derive(PartialEq, Eq, Debug)] enum Section { Signature, @@ -81,8 +79,8 @@ impl From
for FileSection { bytes: 81, }, Section::Npcs => FileSection { - offset: 714, - bytes: 51, + offset: 713, + bytes: 52, }, } } @@ -142,7 +140,7 @@ pub fn parse(byte_vector: &Vec) -> Result { &byte_vector[Range::::from(FileSection::from(Section::Npcs))] .try_into() .unwrap(), - ); + )?; let mut byte_position: BytePosition = BytePosition::default(); save.attributes = attributes::parse_with_position( @@ -168,26 +166,31 @@ pub fn parse(byte_vector: &Vec) -> Result { Ok(save) } - pub fn generate(save: &mut Save) -> Vec { - let mut result : Vec = Vec::::new(); + let mut result: Vec = Vec::::new(); result.resize(765, 0x00); - + result[Range::::from(FileSection::from(Section::Signature))].copy_from_slice(&SIGNATURE); - result[Range::::from(FileSection::from(Section::Version))].copy_from_slice(&u32::to_le_bytes(u32::from(save.version))); - result[Range::::from(FileSection::from(Section::Character))].copy_from_slice(&character::generate(&save.character)); - result[Range::::from(FileSection::from(Section::Quests))].copy_from_slice(&quests::generate(&save.quests)); - result[Range::::from(FileSection::from(Section::Waypoints))].copy_from_slice(&waypoints::generate(&save.waypoints)); - result[Range::::from(FileSection::from(Section::Npcs))].copy_from_slice(&npcs::generate(save.npcs)); + result[Range::::from(FileSection::from(Section::Version))] + .copy_from_slice(&u32::to_le_bytes(u32::from(save.version))); + result[Range::::from(FileSection::from(Section::Character))] + .copy_from_slice(&character::generate(&save.character)); + result[Range::::from(FileSection::from(Section::Quests))] + .copy_from_slice(&quests::generate(&save.quests)); + result[Range::::from(FileSection::from(Section::Waypoints))] + .copy_from_slice(&waypoints::generate(&save.waypoints)); + result[Range::::from(FileSection::from(Section::Npcs))] + .copy_from_slice(&npcs::generate(save.npcs)); result.append(&mut attributes::generate(&save.attributes)); result.append(&mut skills::generate(&save.skills)); result.append(&mut items::generate(&mut save.items)); let length = result.len() as u32; - result[Range::::from(FileSection::from(Section::FileSize))].copy_from_slice(&u32::to_le_bytes(length)); + result[Range::::from(FileSection::from(Section::FileSize))] + .copy_from_slice(&u32::to_le_bytes(length)); let checksum = calc_checksum(&result); - result[Range::::from(FileSection::from(Section::Checksum))].copy_from_slice(&i32::to_le_bytes(checksum)); - + result[Range::::from(FileSection::from(Section::Checksum))] + .copy_from_slice(&i32::to_le_bytes(checksum)); result } @@ -221,7 +224,7 @@ pub enum Version { V250R, } -impl From for u32{ +impl From for u32 { fn from(version: Version) -> u32 { match version { Version::V100 => 71, @@ -231,12 +234,11 @@ impl From for u32{ Version::V110 => 96, Version::V200R => 97, Version::V240R => 98, - Version::V250R => 99 + Version::V250R => 99, } } } - #[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Difficulty { #[default] @@ -346,7 +348,6 @@ impl From for u8 { } } - pub fn calc_checksum(bytes: &Vec) -> i32 { let mut checksum: i32 = 0; let range = Range::::from(FileSection::from(Section::Checksum)); @@ -363,14 +364,13 @@ pub fn calc_checksum(bytes: &Vec) -> i32 { #[cfg(test)] mod tests { use super::*; - use std::path::Path; - use std::io::Write; use std::fs; + use std::io::Write; + use std::path::Path; #[test] fn test_parse_save() { - let path: &Path = - Path::new("assets/Joe.d2s"); + let path: &Path = Path::new("assets/Joe.d2s"); let save_file: Vec = match std::fs::read(path) { Ok(bytes) => bytes, Err(e) => panic!("File invalid: {e:?}"), @@ -386,22 +386,20 @@ mod tests { #[test] fn test_generate_save() { - let path: &Path = - Path::new("assets/Test.d2s"); + let path: &Path = Path::new("assets/Test.d2s"); - let mut save : Save = Save::default(); + let mut save: Save = Save::default(); save.character.set_name(String::from("test")); save.attributes = attributes::default_character(Class::Amazon); let generated_save = generate(&mut save); let mut file = fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(path) - .unwrap(); + .write(true) + .create(true) + .open(path) + .unwrap(); - file.write_all(&generated_save).unwrap(); } } diff --git a/src/npcs.rs b/src/npcs.rs index 11010ce..1377aa9 100644 --- a/src/npcs.rs +++ b/src/npcs.rs @@ -1,26 +1,33 @@ -const SECTION_HEADER: [u8; 3] = [0x77, 0x34, 0x00]; +use crate::ParseError; + +const SECTION_HEADER: [u8; 4] = [0x01, 0x77, 0x34, 0x00]; #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct Placeholder { - data: [u8; 51], + data: [u8; 52], } impl Default for Placeholder { fn default() -> Self { - Placeholder { data: [0x00; 51] } + let mut placeholder = Placeholder { data: [0x00; 52] }; + placeholder.data[0..4].copy_from_slice(&SECTION_HEADER); + placeholder } } -pub fn parse(bytes: &[u8; 51]) -> Placeholder { - let mut placeholder: Placeholder = Placeholder { data: [0x00; 51] }; +pub fn parse(bytes: &[u8; 52]) -> Result { + if bytes[0..4] != SECTION_HEADER{ + return Err(ParseError{message: format!("Found wrong header for NPC section, expected {0:X?} but found {1:X?}", SECTION_HEADER, &bytes[0..4])}) + } + let mut placeholder: Placeholder = Placeholder { data: [0x00; 52] }; placeholder.data.copy_from_slice(bytes); - placeholder + Ok(placeholder) } -pub fn generate(placeholder: Placeholder) -> [u8; 51] { - let mut bytes: [u8; 51] = [0x00; 51]; - bytes.copy_from_slice(&placeholder.data[0..51]); +pub fn generate(placeholder: Placeholder) -> [u8; 52] { + let mut bytes: [u8; 52] = [0x00; 52]; + bytes.copy_from_slice(&placeholder.data[0..52]); bytes }