From 90a2eeda8ad76764ef5321fcc8e340052e271ac1 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Mon, 31 Mar 2025 17:02:30 -0400 Subject: [PATCH] Start giving numerical values/ids for fields when possible This makes it easier to build tools off of, such as Kawari which needs the internal ID for things. --- src/data.rs | 18 ++-- src/html.rs | 14 +-- src/lib.rs | 7 +- src/parser.rs | 32 +++--- src/value.rs | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 336 insertions(+), 29 deletions(-) create mode 100644 src/value.rs diff --git a/src/data.rs b/src/data.rs index ed18b08..7296b5e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,5 +1,9 @@ use serde::Serialize; +use crate::value::{ + CityStateValue, GenderValue, GuardianValue, NamedayValue, RaceValue, TribeValue, WorldValue, +}; + #[derive(Default, Serialize)] pub struct Currencies { pub gil: u32, @@ -38,14 +42,14 @@ pub struct Appearance { #[derive(Default, Serialize)] pub struct CharacterData { pub name: String, - pub world: String, + pub world: WorldValue, pub data_center: String, - pub city_state: String, - pub nameday: String, - pub guardian: String, - pub race: String, - pub gender: String, - pub tribe: String, + pub city_state: CityStateValue, + pub nameday: NamedayValue, + pub guardian: GuardianValue, + pub race: RaceValue, + pub gender: GenderValue, + pub tribe: TribeValue, #[serde(skip_serializing_if = "Option::is_none")] pub currencies: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/html.rs b/src/html.rs index fc3bd41..ff8532c 100644 --- a/src/html.rs +++ b/src/html.rs @@ -14,13 +14,13 @@ pub fn create_character_html(char_data: &CharacterData) -> String { template .render(context! { name => char_data.name, - world => char_data.world, + world => char_data.world.name, data_center => char_data.data_center, - race => char_data.race, - subrace => char_data.tribe, - gender => char_data.gender, - nameday => char_data.nameday, - city_state => char_data.city_state + race => char_data.race.name, + subrace => char_data.tribe.name, + gender => char_data.gender.name, + nameday => char_data.nameday.value, + city_state => char_data.city_state.name }) .unwrap() } @@ -35,7 +35,7 @@ pub fn create_plate_html(char_data: &CharacterData) -> String { template .render(context! { name => char_data.name, - world => char_data.world, + world => char_data.world.name, data_center => char_data.data_center, title => char_data.plate_title, level => char_data.plate_classjob_level, diff --git a/src/lib.rs b/src/lib.rs index 904b856..b0ae808 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod downloader; pub mod html; pub mod package; pub mod parser; +pub mod value; use crate::data::CharacterData; use crate::downloader::download; @@ -168,9 +169,9 @@ pub async fn archive_character( // appearance data char_data.appearance = Some(Appearance { - race: char_data.race.clone(), - tribe: char_data.tribe.clone(), - gender: char_data.gender.clone(), + race: char_data.race.name.clone(), + tribe: char_data.tribe.name.clone(), + gender: char_data.gender.name.clone(), model_type: package.model_type, height: package.height, face_type: package.face_type, diff --git a/src/parser.rs b/src/parser.rs index 9d3e4c1..f68b76d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,9 @@ -use crate::data::CharacterData; +use crate::{ + data::CharacterData, + value::{ + CityStateValue, GenderValue, GuardianValue, NamedayValue, RaceValue, TribeValue, WorldValue, + }, +}; use regex::Regex; use scraper::{Html, Selector}; @@ -50,7 +55,8 @@ pub fn parse_lodestone(data: &str) -> CharacterData { let re = Regex::new(r"(\w+)\s\[(\w+)\]").unwrap(); let inner_html = element.inner_html(); let captures = re.captures(&inner_html).unwrap(); - char_data.world = captures.get(1).unwrap().as_str().to_owned(); + // TODO: use error + char_data.world = WorldValue::try_from(captures.get(1).unwrap().as_str()).unwrap(); char_data.data_center = captures.get(2).unwrap().as_str().to_owned(); } @@ -69,31 +75,33 @@ pub fn parse_lodestone(data: &str) -> CharacterData { let inner_html = block_name.inner_html(); let captures = re.captures(&inner_html).unwrap(); - char_data.race = captures.get(1).unwrap().as_str().to_owned(); - char_data.tribe = captures.get(2).unwrap().as_str().to_owned(); - if captures.get(3).unwrap().as_str() == "♀" { - char_data.gender = "Female".parse().unwrap(); - } else { - char_data.gender = "Male".parse().unwrap(); - } + char_data.race = + RaceValue::try_from(captures.get(1).unwrap().as_str()).unwrap(); + char_data.tribe = + TribeValue::try_from(captures.get(2).unwrap().as_str()).unwrap(); + char_data.gender = + GenderValue::try_from(captures.get(3).unwrap().as_str()).unwrap(); } } else if name == "City-state" { if let Some(block_name) = element .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) .nth(0) { - char_data.city_state = block_name.inner_html(); + char_data.city_state = + CityStateValue::try_from(block_name.inner_html().as_str()).unwrap(); } } else if name == "Nameday" { for element in element.select(&Selector::parse(NAMEDAY_SELECTOR).unwrap()) { - char_data.nameday = element.inner_html(); + char_data.nameday = + NamedayValue::try_from(element.inner_html().as_str()).unwrap(); } if let Some(block_name) = element .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) .nth(0) { - char_data.guardian = block_name.inner_html(); + char_data.guardian = + GuardianValue::try_from(block_name.inner_html().as_str()).unwrap(); } } } diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..ce9c27a --- /dev/null +++ b/src/value.rs @@ -0,0 +1,294 @@ +use regex::Regex; +use serde::Serialize; + +use crate::ArchiveError; + +// TODO: does it make sense to implement Default? +#[derive(Default, Serialize)] +pub struct WorldValue { + /// Name of the world. + pub name: String, + /// Internal ID of the world. + pub value: i32, +} + +impl TryFrom<&str> for WorldValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let value = match name { + "Adamantoise" => 73, + "Cactuar" => 79, + "Faerie" => 54, + "Gilgamesh" => 63, + "Jenova" => 40, + "Midgardsormr" => 65, + "Sargatanas" => 99, + "Siren" => 57, + "Balmung" => 91, + "Brynhildr" => 34, + "Coeurl" => 74, + "Diabolos" => 62, + "Goblin" => 81, + "Malboro" => 75, + "Mateus" => 37, + "Zalera" => 41, + "Behemoth" => 78, + "Excalibur" => 93, + "Exodus" => 53, + "Famfrit" => 35, + "Hyperion" => 95, + "Lamia" => 55, + "Leviathan" => 64, + "Ultros" => 77, + "Halicarnassus" => 406, + "Maduin" => 407, + "Marilith" => 404, + "Seraph" => 405, + "Cuchulainn" => 408, + "Golem" => 411, + "Kraken" => 409, + "Rafflesia" => 410, + "Cerberus" => 80, + "Louisoix" => 83, + "Moogle" => 71, + "Omega" => 39, + "Phantom" => 401, + "Ragnarok" => 97, + "Sagittarius" => 400, + "Spriggan" => 85, + "Alpha" => 402, + "Lich" => 36, + "Odin" => 66, + "Phoenix" => 56, + "Raiden" => 403, + "Shiva" => 67, + "Twintania" => 33, + "Zodiark" => 42, + "Aegis" => 90, + "Atomos" => 68, + "Carbuncle" => 45, + "Garuda" => 58, + "Gungnir" => 94, + "Kujata" => 49, + "Tonberry" => 72, + "Typhon" => 50, + "Alexander" => 43, + "Bahamut" => 69, + "Durandal" => 92, + "Fenrir" => 46, + "Ifrit" => 59, + "Ridill" => 98, + "Tiamat" => 76, + "Ultima" => 51, + "Anima" => 44, + "Asura" => 23, + "Chocobo" => 70, + "Hades" => 47, + "Ixion" => 48, + "Masamune" => 96, + "Pandaemonium" => 28, + "Titan" => 61, + "Belias" => 24, + "Mandragora" => 82, + "Ramuh" => 60, + "Shinryu" => 29, + "Unicorn" => 30, + "Valefor" => 52, + "Yojimbo" => 31, + "Zeromus" => 32, + "Bismarck" => 22, + "Ravana" => 21, + "Sephirot" => 86, + "Sophia" => 87, + "Zurvan" => 88, + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct CityStateValue { + /// Name of the city-state. + pub name: String, + /// Internal ID of the city-state. + pub value: i32, +} + +impl TryFrom<&str> for CityStateValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let value = match name { + "Limsa Lominsa" => 1, + "Gridania" => 2, + "Ul'dah" => 3, + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct GenderValue { + /// Name of the gender. + pub name: String, + /// Internal ID of the gender. + pub value: i32, +} + +impl TryFrom<&str> for GenderValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let (value, name) = match name { + "♂" => (0, "Male"), + "♀" => (1, "Female"), + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct RaceValue { + /// Name of the race. + pub name: String, + /// Internal ID of the race. + pub value: i32, +} + +impl TryFrom<&str> for RaceValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let value = match name { + "Hyur" => 1, + "Elezen" => 2, + "Lalafell" => 3, + "Miqote" => 4, + "Roegadyn" => 5, + "AuRa" => 6, + "Hrothgar" => 7, + "Viera" => 8, + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct TribeValue { + /// Name of the tribe. + pub name: String, + /// Internal ID of the tribe. + pub value: i32, +} + +impl TryFrom<&str> for TribeValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let value = match name { + "Midlander" => 1, + "Highlander" => 2, + "Wildwood" => 3, + "Duskwight" => 4, + "Plainsfolk" => 5, + "Dunesfolk" => 6, + "Seeker" => 7, + "Keeper" => 8, + "SeaWolf" => 9, + "Hellsguard" => 10, + "Raen" => 11, + "Xaela" => 12, + "Hellion" => 13, + "Lost" => 14, + "Rava" => 15, + "Veena" => 16, + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct GuardianValue { + /// Name of the guardian. + pub name: String, + /// Internal ID of the guardian. + pub value: i32, +} + +impl TryFrom<&str> for GuardianValue { + type Error = ArchiveError; + + fn try_from(name: &str) -> Result { + let value = match name { + "Halone, the Fury" => 1, + "Menphina, the Lover" => 2, + "Thaliak, the Scholar" => 3, + "Nymeia, the Spinner" => 4, + "Llymlaen, the Navigator" => 5, + "Oschon, the Wanderer" => 6, + "Byregot, the Builder" => 7, + "Rhalgr, the Destroyer" => 8, + "Azeyma, the Warden" => 9, + "Nald'thal, the Traders" => 10, + "Nophica, the Matron" => 11, + "Althyk, the Keeper" => 12, + _ => return Err(ArchiveError::ParsingError), + }; + + Ok(Self { + name: name.to_string(), + value, + }) + } +} + +#[derive(Default, Serialize)] +pub struct NamedayValue { + /// String represenation of your nameday. + pub value: String, + /// Day part of your nameday. + pub day: i32, + /// Month part of your nameday. + pub month: i32, +} + +impl TryFrom<&str> for NamedayValue { + type Error = ArchiveError; + + fn try_from(value: &str) -> Result { + let re = Regex::new(r"(\d{1,2})[^\d]+(\d{1,2})").unwrap(); + let captures = re.captures(&value).unwrap(); + + Ok(Self { + value: value.to_string(), + day: captures.get(1).unwrap().as_str().parse::().unwrap(), + month: captures.get(2).unwrap().as_str().parse::().unwrap(), + }) + } +}