2024-10-30 16:27:32 -04:00
|
|
|
pub mod data;
|
|
|
|
pub mod downloader;
|
|
|
|
pub mod html;
|
2024-10-30 16:32:50 -04:00
|
|
|
pub mod parser;
|
|
|
|
|
|
|
|
use serde::Deserialize;
|
2024-10-31 18:16:42 -04:00
|
|
|
use std::io::Write;
|
2025-03-10 20:20:36 -04:00
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
2024-11-01 16:41:05 -04:00
|
|
|
use physis::race::{Gender, Race, Subrace};
|
2024-10-31 18:16:42 -04:00
|
|
|
use reqwest::Url;
|
2024-10-31 19:43:52 -04:00
|
|
|
use zip::result::ZipError;
|
2024-10-31 18:16:42 -04:00
|
|
|
use zip::write::SimpleFileOptions;
|
|
|
|
use zip::ZipWriter;
|
2024-10-30 16:32:50 -04:00
|
|
|
use crate::downloader::download;
|
2024-11-01 18:58:38 -04:00
|
|
|
use crate::html::{create_character_html, create_plate_html};
|
2024-10-30 16:32:50 -04:00
|
|
|
use crate::parser::parse_search;
|
2024-10-31 18:16:42 -04:00
|
|
|
use base64::prelude::*;
|
2024-10-31 18:35:50 -04:00
|
|
|
#[cfg(target_family = "wasm")]
|
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
2024-10-31 19:56:08 -04:00
|
|
|
#[cfg(target_family = "wasm")]
|
|
|
|
use wasm_bindgen::JsValue;
|
2024-11-01 16:41:05 -04:00
|
|
|
use crate::data::CharacterData;
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-10-31 23:11:26 -04:00
|
|
|
/// The main Lodestone domain
|
2024-10-30 16:32:50 -04:00
|
|
|
const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com";
|
|
|
|
|
2024-10-31 23:11:26 -04:00
|
|
|
/// The Lodestone proxy used in WebAssembly builds. Needed for CORS and cookie injection.
|
|
|
|
const LODESTONE_TUNNEL_HOST: &str = "https://lodestone-tunnel.ryne.moe";
|
|
|
|
|
|
|
|
/// The image domain.
|
|
|
|
const IMAGE_HOST: &str = "img2.finalfantasyxiv.com";
|
|
|
|
|
|
|
|
/// The image proxy used in WebAssembly builds. Needed for CORS.
|
|
|
|
const IMAGE_TUNNEL_HOST: &str = "img-tunnel.ryne.moe";
|
|
|
|
|
2024-10-30 16:32:50 -04:00
|
|
|
#[derive(Default, Deserialize, Clone)]
|
|
|
|
struct Package {
|
|
|
|
playtime: String,
|
|
|
|
gil: u32,
|
|
|
|
is_battle_mentor: bool,
|
|
|
|
is_trade_mentor: bool,
|
|
|
|
is_novice: bool,
|
|
|
|
is_returner: bool,
|
|
|
|
player_commendations: i32,
|
2024-11-01 16:41:05 -04:00
|
|
|
pub portrait: String,
|
|
|
|
pub plate_title: String,
|
|
|
|
pub plate_title_is_prefix: bool,
|
|
|
|
pub plate_class_job: String,
|
|
|
|
pub plate_class_job_level: i32,
|
|
|
|
pub search_comment: String,
|
2024-11-01 18:58:38 -04:00
|
|
|
pub base_plate: Option<String>,
|
|
|
|
pub pattern_overlay: Option<String>,
|
|
|
|
pub backing: Option<String>,
|
|
|
|
pub top_border: Option<String>,
|
|
|
|
pub bottom_border: Option<String>,
|
|
|
|
pub portrait_frame: Option<String>,
|
|
|
|
pub plate_frame: Option<String>,
|
|
|
|
pub accent: Option<String>,
|
2024-11-01 16:41:05 -04:00
|
|
|
|
|
|
|
// Appearance
|
|
|
|
pub race: i32,
|
|
|
|
pub gender: i32,
|
|
|
|
pub model_type: i32,
|
|
|
|
pub height: i32,
|
|
|
|
pub tribe: i32,
|
|
|
|
pub face_type: i32,
|
|
|
|
pub hair_style: i32,
|
|
|
|
pub has_highlights: bool,
|
|
|
|
pub skin_color: i32,
|
|
|
|
pub eye_color: i32,
|
|
|
|
pub hair_color: i32,
|
|
|
|
pub hair_color2: i32,
|
|
|
|
pub face_features: i32,
|
|
|
|
pub face_features_color: i32,
|
|
|
|
pub eyebrows: i32,
|
|
|
|
pub eye_color2: i32,
|
|
|
|
pub eye_shape: i32,
|
|
|
|
pub nose_shape: i32,
|
|
|
|
pub jaw_shape: i32,
|
|
|
|
pub lip_style: i32,
|
|
|
|
pub lip_color: i32,
|
|
|
|
pub race_feature_size: i32,
|
|
|
|
pub race_feature_type: i32,
|
|
|
|
pub bust_size: i32,
|
|
|
|
pub facepaint: i32,
|
|
|
|
pub facepaint_color: i32,
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum ArchiveError {
|
|
|
|
DownloadFailed(String),
|
|
|
|
CharacterNotFound,
|
|
|
|
ParsingError,
|
2024-10-31 21:46:50 -04:00
|
|
|
CouldNotConnectToDalamud,
|
2024-10-31 19:43:52 -04:00
|
|
|
UnknownError
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ZipError> for ArchiveError {
|
|
|
|
fn from(_: ZipError) -> Self {
|
|
|
|
ArchiveError::UnknownError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<std::io::Error> for ArchiveError {
|
|
|
|
fn from(_: std::io::Error) -> Self {
|
|
|
|
ArchiveError::UnknownError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-31 19:56:08 -04:00
|
|
|
#[cfg(target_family = "wasm")]
|
|
|
|
impl From<ArchiveError> for JsValue {
|
|
|
|
fn from(err: ArchiveError) -> Self {
|
|
|
|
match err {
|
|
|
|
// TODO: give JS the URL that failed to download
|
|
|
|
ArchiveError::DownloadFailed(_) => { JsValue::from_str(&"download_failed".to_string()) }
|
|
|
|
ArchiveError::CharacterNotFound => { JsValue::from_str(&"character_not_found".to_string()) }
|
|
|
|
ArchiveError::ParsingError => { JsValue::from_str(&"parsing_error".to_string())}
|
|
|
|
ArchiveError::UnknownError => { JsValue::from_str(&"unknown_error".to_string()) }
|
2024-10-31 23:11:26 -04:00
|
|
|
ArchiveError::CouldNotConnectToDalamud => { JsValue::from_str(&"could_not_connect_to_dalamud".to_string()) }
|
2024-10-31 19:56:08 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-10 20:20:36 -04:00
|
|
|
// FIXME: this is stupid and also just copied from physis
|
|
|
|
fn convert_dat_race(x: i32) -> Race {
|
|
|
|
match x {
|
|
|
|
1 => Race::Hyur,
|
|
|
|
2 => Race::Elezen,
|
|
|
|
3 => Race::Lalafell,
|
|
|
|
4 => Race::Miqote,
|
|
|
|
5 => Race::Roegadyn,
|
|
|
|
6 => Race::AuRa,
|
|
|
|
7 => Race::Hrothgar,
|
|
|
|
8 => Race::Viera,
|
|
|
|
_ => Race::Hyur,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn convert_dat_gender(x: i32) -> Gender {
|
|
|
|
match x {
|
|
|
|
0 => Gender::Male,
|
|
|
|
1 => Gender::Female,
|
|
|
|
_ => Gender::Male,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn convert_dat_subrace(x: i32) -> Subrace {
|
|
|
|
match x {
|
|
|
|
1 => Subrace::Midlander,
|
|
|
|
2 => Subrace::Highlander,
|
|
|
|
3 => Subrace::Wildwood,
|
|
|
|
4 => Subrace::Duskwight,
|
|
|
|
5 => Subrace::Plainsfolk,
|
|
|
|
6 => Subrace::Dunesfolk,
|
|
|
|
7 => Subrace::Seeker,
|
|
|
|
8 => Subrace::Keeper,
|
|
|
|
9 => Subrace::SeaWolf,
|
|
|
|
10 => Subrace::Hellsguard,
|
|
|
|
11 => Subrace::Raen,
|
|
|
|
12 => Subrace::Xaela,
|
|
|
|
13 => Subrace::Hellion,
|
|
|
|
14 => Subrace::Lost,
|
|
|
|
15 => Subrace::Rava,
|
|
|
|
16 => Subrace::Veena,
|
|
|
|
_ => Subrace::Midlander,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-31 18:16:42 -04:00
|
|
|
/// Archives the character named `character_name` and gives a ZIP file as bytes that can be written to disk.
|
2024-10-31 19:43:52 -04:00
|
|
|
pub async fn archive_character(character_name: &str, use_dalamud: bool) -> Result<Vec<u8>, ArchiveError> {
|
2024-10-31 23:11:26 -04:00
|
|
|
let lodestone_host = if cfg!(target_family = "wasm") {
|
|
|
|
LODESTONE_TUNNEL_HOST
|
|
|
|
} else {
|
|
|
|
LODESTONE_HOST
|
|
|
|
};
|
|
|
|
|
|
|
|
let search_url = Url::parse_with_params(&format!("{lodestone_host}/lodestone/character?"), &[("q", character_name)]).map_err(|_| ArchiveError::UnknownError)?;
|
2024-10-31 19:43:52 -04:00
|
|
|
let search_page = download(&search_url)
|
2024-10-31 18:16:42 -04:00
|
|
|
.await
|
2024-10-31 19:43:52 -04:00
|
|
|
.map_err(|_| ArchiveError::DownloadFailed(search_url.to_string()))?;
|
|
|
|
let search_page = String::from_utf8(search_page).map_err(|_| ArchiveError::ParsingError)?;
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
let href = parse_search(&search_page);
|
2024-10-30 16:32:50 -04:00
|
|
|
if href.is_empty() {
|
2024-10-31 19:43:52 -04:00
|
|
|
return Err(ArchiveError::CharacterNotFound);
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
|
|
|
|
2024-10-31 23:11:26 -04:00
|
|
|
let char_page_url = Url::parse(&format!("{lodestone_host}{}", href)).map_err(|_| ArchiveError::UnknownError)?;
|
2024-10-31 19:43:52 -04:00
|
|
|
let char_page = download(&char_page_url)
|
2024-10-31 18:16:42 -04:00
|
|
|
.await
|
2024-10-31 19:43:52 -04:00
|
|
|
.map_err(|_| ArchiveError::DownloadFailed(char_page_url.to_string()))?;
|
|
|
|
let char_page = String::from_utf8(char_page).map_err(|_| ArchiveError::ParsingError)?;
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
let mut char_data = parser::parse_lodestone(&char_page);
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-10-31 18:16:42 -04:00
|
|
|
// 2 MiB, for one JSON and two images
|
2024-10-31 23:11:26 -04:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf));
|
2024-10-31 18:16:42 -04:00
|
|
|
|
|
|
|
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
2024-10-30 16:32:50 -04:00
|
|
|
|
|
|
|
if !char_data.portrait_url.is_empty() {
|
2024-10-31 23:11:26 -04:00
|
|
|
let portrait_url = if cfg!(target_family = "wasm") {
|
|
|
|
&char_data.portrait_url.replace(IMAGE_HOST, IMAGE_TUNNEL_HOST)
|
|
|
|
} else {
|
|
|
|
&char_data.portrait_url
|
|
|
|
};
|
2024-10-31 19:43:52 -04:00
|
|
|
let portrait_url = Url::parse(&portrait_url).map_err(|_| ArchiveError::UnknownError)?;
|
2024-10-31 18:16:42 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
let portrait = download(&portrait_url)
|
2024-10-31 18:16:42 -04:00
|
|
|
.await
|
2024-10-31 19:43:52 -04:00
|
|
|
.map_err(|_| ArchiveError::DownloadFailed(portrait_url.to_string()))?;
|
2024-10-31 18:16:42 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
zip.start_file("portrait.jpg", options)?;
|
|
|
|
zip.write_all(&*portrait)?;
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
|
|
|
if !char_data.face_url.is_empty() {
|
2024-10-31 23:11:26 -04:00
|
|
|
let face_url = if cfg!(target_family = "wasm") {
|
|
|
|
&char_data.face_url.replace(IMAGE_HOST, IMAGE_TUNNEL_HOST)
|
|
|
|
} else {
|
|
|
|
&char_data.face_url
|
|
|
|
};
|
2024-10-31 19:43:52 -04:00
|
|
|
let face_url = Url::parse(&face_url).map_err(|_| ArchiveError::UnknownError)?;
|
2024-10-31 18:16:42 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
let face = download(&face_url)
|
2024-10-31 18:16:42 -04:00
|
|
|
.await
|
2024-10-31 19:43:52 -04:00
|
|
|
.map_err(|_| ArchiveError::DownloadFailed(face_url.to_string()))?;
|
2024-10-31 18:16:42 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
zip.start_file("face.jpg", options)?;
|
|
|
|
zip.write_all(&*face)?;
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if use_dalamud {
|
2024-10-31 21:46:50 -04:00
|
|
|
let dalamud_url = Url::parse(&"http://localhost:42072/package").map_err(|_| ArchiveError::UnknownError)?;
|
|
|
|
let package = download(&dalamud_url).await.map_err(|_| ArchiveError::CouldNotConnectToDalamud)?;
|
|
|
|
let package = String::from_utf8(package).map_err(|_| ArchiveError::ParsingError)?;
|
|
|
|
// Remove BOM at the start
|
|
|
|
let package = package.trim_start_matches("\u{feff}");
|
|
|
|
let package: Package = serde_json::from_str(&package.trim_start()).unwrap();
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-11-01 16:41:05 -04:00
|
|
|
// appearance data
|
|
|
|
char_data.appearance.model_type = package.model_type;
|
2024-10-30 16:32:50 -04:00
|
|
|
char_data.appearance.height = package.height;
|
2024-11-01 16:41:05 -04:00
|
|
|
char_data.appearance.face_type = package.face_type;
|
|
|
|
char_data.appearance.hair_style = package.hair_style;
|
|
|
|
char_data.appearance.has_highlights = package.has_highlights;
|
|
|
|
char_data.appearance.skin_color = package.skin_color;
|
|
|
|
char_data.appearance.eye_color = package.eye_color;
|
|
|
|
char_data.appearance.hair_color = package.hair_color;
|
|
|
|
char_data.appearance.hair_color2 = package.hair_color2;
|
|
|
|
char_data.appearance.face_features = package.face_features;
|
|
|
|
char_data.appearance.face_features_color = package.face_features_color;
|
|
|
|
char_data.appearance.eyebrows = package.eyebrows;
|
|
|
|
char_data.appearance.eye_color2 = package.eye_color2;
|
|
|
|
char_data.appearance.eye_shape = package.eye_color2;
|
|
|
|
char_data.appearance.nose_shape = package.nose_shape;
|
|
|
|
char_data.appearance.jaw_shape = package.jaw_shape;
|
|
|
|
char_data.appearance.lip_style = package.lip_style;
|
|
|
|
char_data.appearance.lip_color = package.lip_color;
|
|
|
|
char_data.appearance.race_feature_size = package.race_feature_size;
|
|
|
|
char_data.appearance.race_feature_type = package.race_feature_type;
|
2024-10-30 16:32:50 -04:00
|
|
|
char_data.appearance.bust_size = package.bust_size;
|
2024-11-01 16:41:05 -04:00
|
|
|
char_data.appearance.facepaint = package.facepaint;
|
|
|
|
char_data.appearance.facepaint_color = package.facepaint_color;
|
|
|
|
|
|
|
|
char_data.playtime = package.playtime.parse().unwrap();
|
2024-10-30 16:32:50 -04:00
|
|
|
char_data.currencies.gil = package.gil; // TODO: also fetch from the lodestone
|
|
|
|
char_data.is_battle_mentor = package.is_battle_mentor;
|
|
|
|
char_data.is_trade_mentor = package.is_trade_mentor;
|
|
|
|
char_data.is_novice = package.is_novice;
|
|
|
|
char_data.is_returner = package.is_returner;
|
|
|
|
char_data.player_commendations = package.player_commendations; // TODO: fetch from the lodestone?
|
2024-11-01 18:58:38 -04:00
|
|
|
char_data.plate_title = package.plate_title;
|
|
|
|
char_data.plate_classjob = package.plate_class_job;
|
|
|
|
char_data.plate_classjob_level = package.plate_class_job_level;
|
|
|
|
char_data.search_comment = package.search_comment;
|
2024-10-31 23:11:26 -04:00
|
|
|
|
2024-11-01 16:41:05 -04:00
|
|
|
zip.start_file("plate-portrait.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(package.portrait.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
|
2024-11-01 18:58:38 -04:00
|
|
|
if let Some(base_plate) = package.base_plate {
|
|
|
|
zip.start_file("base-plate.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(base_plate.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(pattern_overlay) = package.pattern_overlay {
|
|
|
|
zip.start_file("pattern-overlay.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(pattern_overlay.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(backing) = package.backing {
|
|
|
|
zip.start_file("backing.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(backing.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(top_border) = package.top_border {
|
|
|
|
zip.start_file("top-border.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(top_border.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(bottom_border) = package.bottom_border {
|
|
|
|
zip.start_file("bottom-border.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(bottom_border.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(portrait_frame) = package.portrait_frame {
|
|
|
|
zip.start_file("portrait-frame.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(portrait_frame.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(plate_frame) = package.plate_frame {
|
|
|
|
zip.start_file("plate-frame.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(plate_frame.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(accent) = package.accent {
|
|
|
|
zip.start_file("accent.png", options)?;
|
|
|
|
zip.write_all(&*BASE64_STANDARD.decode(accent.trim_start_matches("data:image/png;base64,")).unwrap())?;
|
|
|
|
}
|
|
|
|
|
2025-03-10 20:20:36 -04:00
|
|
|
let timestamp: u32 = SystemTime::now()
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.expect("Failed to get UNIX timestamp!")
|
|
|
|
.as_secs()
|
|
|
|
.try_into()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let char_dat = physis::chardat::CharacterData {
|
|
|
|
version: 7,
|
|
|
|
customize: physis::chardat::CustomizeData {
|
|
|
|
race: convert_dat_race(package.race),
|
|
|
|
gender: convert_dat_gender(package.gender),
|
|
|
|
age: package.model_type as u8,
|
|
|
|
height: package.height as u8,
|
|
|
|
subrace: convert_dat_subrace(package.tribe),
|
|
|
|
face: package.face_type as u8,
|
|
|
|
hair: package.hair_style as u8,
|
|
|
|
enable_highlights: package.has_highlights,
|
|
|
|
skin_tone: package.skin_color as u8,
|
|
|
|
right_eye_color: package.eye_color as u8,
|
|
|
|
hair_tone: package.hair_color as u8,
|
|
|
|
highlights: package.hair_color2 as u8,
|
|
|
|
facial_features: package.face_features as u8,
|
|
|
|
facial_feature_color: package.face_features_color as u8,
|
|
|
|
eyebrows: package.eyebrows as u8,
|
|
|
|
left_eye_color: package.eye_color2 as u8,
|
|
|
|
eyes: package.eye_shape as u8,
|
|
|
|
nose: package.nose_shape as u8,
|
|
|
|
jaw: package.jaw_shape as u8,
|
|
|
|
mouth: package.lip_style as u8,
|
|
|
|
lips_tone_fur_pattern: package.lip_color as u8,
|
|
|
|
race_feature_size: package.race_feature_size as u8,
|
|
|
|
race_feature_type: package.race_feature_type as u8,
|
|
|
|
bust: package.bust_size as u8,
|
|
|
|
face_paint: package.facepaint as u8,
|
|
|
|
face_paint_color: package.facepaint_color as u8,
|
|
|
|
voice: 0, // TODO: need to get from game
|
|
|
|
},
|
|
|
|
timestamp,
|
|
|
|
comment: "Generated by Auracite".to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
zip.start_file("FFXIV_CHARA_01.dat", options)?;
|
|
|
|
zip.write_all(&*char_dat.write_to_buffer().unwrap())?;
|
|
|
|
|
2024-10-31 23:11:26 -04:00
|
|
|
// Stop the HTTP server
|
2024-11-01 19:03:30 -04:00
|
|
|
let stop_url = Url::parse(&"http://localhost:42072/stop").map_err(|_| ArchiveError::UnknownError)?;
|
2025-03-10 20:20:36 -04:00
|
|
|
// I'm intentionally ignoring the message because it doesn't matter if it fails - and it usually does
|
|
|
|
let _ = download(&stop_url).await;
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
|
|
|
|
2024-10-31 21:46:50 -04:00
|
|
|
zip.start_file("character.json", options)?;
|
|
|
|
zip.write_all(serde_json::to_string(&char_data).unwrap().as_ref())?;
|
|
|
|
|
2024-11-01 18:58:38 -04:00
|
|
|
zip.start_file("character.html", options)?;
|
|
|
|
zip.write_all(create_character_html(
|
2024-10-31 18:20:45 -04:00
|
|
|
&char_data
|
2024-11-01 18:58:38 -04:00
|
|
|
).as_ref())?;
|
2024-10-31 18:20:45 -04:00
|
|
|
|
2024-11-01 18:58:38 -04:00
|
|
|
zip.start_file("plate.html", options)?;
|
|
|
|
zip.write_all(create_plate_html(
|
|
|
|
&char_data
|
|
|
|
).as_ref())?;
|
2024-10-31 18:20:45 -04:00
|
|
|
|
2024-10-31 21:46:50 -04:00
|
|
|
zip.finish()?;
|
2024-10-30 16:32:50 -04:00
|
|
|
|
2024-10-31 19:43:52 -04:00
|
|
|
Ok(buf)
|
2024-10-30 16:32:50 -04:00
|
|
|
}
|
2024-10-31 18:16:42 -04:00
|
|
|
|
|
|
|
/// Archives the character named `character_name` and converts the ZIP file to Base64. Useful for downloading via data URIs.
|
|
|
|
#[cfg(target_family = "wasm")]
|
|
|
|
#[wasm_bindgen]
|
2024-10-31 19:56:08 -04:00
|
|
|
pub async extern fn archive_character_base64(character_name: &str, use_dalamud: bool) -> Result<String, ArchiveError> {
|
2025-03-10 18:55:52 -04:00
|
|
|
#[cfg(feature = "debug")]
|
|
|
|
console_error_panic_hook::set_once();
|
|
|
|
|
2024-10-31 19:56:08 -04:00
|
|
|
let buf: String = archive_character(character_name, use_dalamud).await.map(|x| BASE64_STANDARD.encode(x))?;
|
|
|
|
return Ok(format!("data:application/octet-stream;charset=utf-16le;base64,{buf}").into());
|
2025-03-10 18:55:52 -04:00
|
|
|
}
|