1
Fork 0
mirror of https://github.com/redstrate/Auracite.git synced 2025-06-30 17:47:45 +00:00

Begin extracting classjob ids, current and max EXP

This commit is contained in:
Joshua Goins 2025-06-28 12:11:40 -04:00
parent a2922d043d
commit aa7c38634d
3 changed files with 122 additions and 32 deletions

View file

@ -12,7 +12,6 @@ use crate::parser::parse_search;
use base64::prelude::*; use base64::prelude::*;
use data::{Appearance, Currencies}; use data::{Appearance, Currencies};
use package::Package; use package::Package;
use physis::race::{Gender, Race, Tribe};
use physis::savedata::chardat; use physis::savedata::chardat;
use regex::Regex; use regex::Regex;
use reqwest::Url; use reqwest::Url;
@ -132,7 +131,20 @@ pub async fn archive_character(id: u64, use_dalamud: bool) -> Result<Vec<u8>, Ar
.map_err(|_| ArchiveError::DownloadFailed(char_page_url.to_string()))?; .map_err(|_| ArchiveError::DownloadFailed(char_page_url.to_string()))?;
let char_page = String::from_utf8(char_page).map_err(|_| ArchiveError::ParsingError)?; let char_page = String::from_utf8(char_page).map_err(|_| ArchiveError::ParsingError)?;
let mut char_data = parser::parse_lodestone(&char_page); let mut char_data = CharacterData::default();
parser::parse_profile(&char_page, &mut char_data);
let classjob_page_url = Url::parse(&format!(
"{lodestone_host}/lodestone/character/{}/class_job/",
id
))
.map_err(|_| ArchiveError::UnknownError)?;
let classjob_page = download(&classjob_page_url)
.await
.map_err(|_| ArchiveError::DownloadFailed(classjob_page_url.to_string()))?;
let char_page = String::from_utf8(classjob_page).map_err(|_| ArchiveError::ParsingError)?;
parser::parse_classjob(&char_page, &mut char_data);
// 2 MiB, for one JSON and two images // 2 MiB, for one JSON and two images
let mut buf = Vec::new(); let mut buf = Vec::new();

View file

@ -41,16 +41,13 @@ const CHARACTER_BLOCK_NAME_SELECTOR: &str = ".character-block__name";
const FACE_IMG_SELECTOR: &str = ".frame__chara__face > img"; const FACE_IMG_SELECTOR: &str = ".frame__chara__face > img";
const PORTRAIT_IMG_SELECTOR: &str = ".character__detail__image > a > img"; const PORTRAIT_IMG_SELECTOR: &str = ".character__detail__image > a > img";
const NAMEDAY_SELECTOR: &str = ".character-block__birth"; const NAMEDAY_SELECTOR: &str = ".character-block__birth";
const CLASSJOB_SELECTOR: &str = ".character__level__list > ul > li";
const FREE_COMPANY_SELECTOR: &str = ".character__freecompany__name > h4 > a"; const FREE_COMPANY_SELECTOR: &str = ".character__freecompany__name > h4 > a";
const TITLE_SELECTOR: &str = ".frame__chara__title"; const TITLE_SELECTOR: &str = ".frame__chara__title";
/// Parses the HTML from `data` and returns `CharacterData`. The data may be incomplete. /// Parses the HTML from `data` and returns `CharacterData`. The data may be incomplete.
pub fn parse_lodestone(data: &str) -> CharacterData { pub fn parse_profile(data: &str, char_data: &mut CharacterData) {
let document = Html::parse_document(data); let document = Html::parse_document(data);
let mut char_data = CharacterData::default();
for element in document.select(&Selector::parse(CHARACTER_NAME_SELECTOR).unwrap()) { for element in document.select(&Selector::parse(CHARACTER_NAME_SELECTOR).unwrap()) {
char_data.name = element.inner_html(); char_data.name = element.inner_html();
} }
@ -148,31 +145,6 @@ pub fn parse_lodestone(data: &str) -> CharacterData {
char_data.portrait_url = element.attr("src").unwrap().parse().unwrap(); char_data.portrait_url = element.attr("src").unwrap().parse().unwrap();
} }
for element in document.select(&Selector::parse(CLASSJOB_SELECTOR).unwrap()) {
let img = element.first_child().unwrap();
let name = img
.value()
.as_element()
.unwrap()
.attr("data-tooltip")
.unwrap();
// ignore "-" and other invalid level values
if let Ok(level) = element
.last_child()
.unwrap()
.value()
.as_text()
.unwrap()
.parse::<i32>()
{
char_data.classjob_levels.push(ClassJobValue {
name: name.to_string(),
level,
});
}
}
// TODO: support facewear // TODO: support facewear
let item_slot_selectors = [ let item_slot_selectors = [
".icon-c--0", // Main Hand ".icon-c--0", // Main Hand
@ -215,8 +187,48 @@ pub fn parse_lodestone(data: &str) -> CharacterData {
} }
} }
} }
}
char_data const CLASSJOB_SELECTOR: &str = ".character__job > li";
const CLASSJOB_LEVEL_SELECTOR: &str = ".character__job__level";
const CLASSJOB_NAME_SELECTOR: &str = ".character__job__name";
const CLASSJOB_EXP_SELECTOR: &str = ".character__job__exp";
/// Parses the HTML from `data` and returns `CharacterData`. The data may be incomplete.
pub fn parse_classjob(data: &str, char_data: &mut CharacterData) {
let document = Html::parse_document(data);
for element in document.select(&Selector::parse(CLASSJOB_SELECTOR).unwrap()) {
let level = element
.select(&Selector::parse(CLASSJOB_LEVEL_SELECTOR).unwrap())
.nth(0)
.unwrap();
let name = element
.select(&Selector::parse(CLASSJOB_NAME_SELECTOR).unwrap())
.nth(0)
.unwrap();
let exp_element = element
.select(&Selector::parse(CLASSJOB_EXP_SELECTOR).unwrap())
.nth(0)
.unwrap();
let mut exp = None;
let mut max_exp = None;
if let Some((exp_text, max_exp_text)) = exp_element.inner_html().split_once(" / ") {
exp = exp_text.replace(",", "").parse().ok();
max_exp = max_exp_text.replace(",", "").parse().ok();
}
// skip levels that are -, which means they don't even have the classjob
if let Ok(level) = level.inner_html().parse() {
let mut class_job_value =
ClassJobValue::try_from(name.inner_html().as_str()).unwrap_or_default();
class_job_value.level = level;
class_job_value.exp = exp;
class_job_value.max_exp = max_exp;
char_data.classjob_levels.push(class_job_value);
}
}
} }
fn parse_item_tooltip(element: &scraper::ElementRef<'_>) -> Option<ItemValue> { fn parse_item_tooltip(element: &scraper::ElementRef<'_>) -> Option<ItemValue> {

View file

@ -299,6 +299,72 @@ pub struct ClassJobValue {
pub name: String, pub name: String,
/// Level of the class or job. /// Level of the class or job.
pub level: i32, pub level: i32,
/// The EXP of the job, can be None to indicate either: the job isn't unlocked or that they have max EXP.
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<i32>,
/// The maximum EXP of the job, can be None to indicate either: the job isn't unlocked or that they have max EXP.
#[serde(skip_serializing_if = "Option::is_none")]
pub max_exp: Option<i32>,
/// Internal ID of the class or job.
pub value: i32,
}
impl TryFrom<&str> for ClassJobValue {
type Error = ArchiveError;
fn try_from(name: &str) -> Result<Self, ArchiveError> {
let value = match name {
"Gladiator" => 1,
"Pugilist" => 2,
"Marauder" => 3,
"Lancer" => 4,
"Archer" => 5,
"Conjurer" => 6,
"Thaumaturge" => 7,
"Carpenter" => 8,
"Blacksmith" => 9,
"Armorer" => 10,
"Goldsmith" => 11,
"Leatherworker" => 12,
"Weaver" => 13,
"Alchemist" => 14,
"Culinarian" => 15,
"Miner" => 16,
"Botanist" => 17,
"Fisher" => 18,
"Paladin" => 19,
"Monk" => 20,
"Warrior" => 21,
"Dragoon" => 22,
"Bard" => 23,
"White Mage" => 24,
"Black Mage" => 25,
"Arcanist" => 26,
"Summoner" => 27,
"Scholar" => 28,
"Rogue" => 29,
"Ninja" => 30,
"Machinist" => 31,
"Dark Knight" => 32,
"Astrologian" => 33,
"Samurai" => 34,
"Red Mage" => 35,
"Blue Mage" => 36,
"Gunbreaker" => 37,
"Dancer" => 38,
"Reaper" => 39,
"Sage" => 40,
"Viper" => 41,
"Pictomancer" => 42,
_ => return Err(ArchiveError::ParsingError),
};
Ok(Self {
name: name.to_string(),
value,
..Default::default()
})
}
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]