use std::fs::File; use std::io::BufReader; use paste::paste; pub use misc_data::*; mod misc_data; #[derive(thiserror::Error, Debug)] pub enum LoadDataError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("Failed to parse json: {0}")] Json(#[from] serde_json::Error), } macro_rules! json_data { ($($table_type:ident;)*) => { $(paste! { mod [<$table_type:snake>]; pub use [<$table_type:snake>]::[<$table_type Data>]; })* $(paste! { pub mod [<$table_type:snake _data>] { use std::sync::OnceLock; type Data = super::[<$table_type Data>]; pub(crate) static TABLE: OnceLock> = OnceLock::new(); pub fn iter() -> std::slice::Iter<'static, Data> { TABLE.get().unwrap().iter() } } })* fn load_json_data(base_path: &str) -> Result<(), LoadDataError> { $(paste! { let file = File::open(&format!("{}/{}.json", base_path, stringify!($table_type)))?; let reader = BufReader::new(file); let _ = [<$table_type:snake _data>]::TABLE.set(serde_json::from_reader(reader)?); })* Ok(()) } }; } macro_rules! json_hash_table_data { ($($table_type:ident, $key_param:expr, $key_type:ty;)*) => { $(paste! { mod [<$table_type:snake>]; pub use [<$table_type:snake>]::[<$table_type Data>]; })* $(paste! { pub mod [<$table_type:snake _data>] { use std::collections::HashMap; use std::sync::OnceLock; pub(crate) type Data = super::[<$table_type Data>]; pub(crate) static TABLE: OnceLock> = OnceLock::new(); pub fn iter() -> std::collections::hash_map::Iter<'static, $key_type, Data> { TABLE.get().unwrap().iter() } pub fn get(k: &$key_type) -> Option<&Data> { TABLE.get().unwrap().get(k) } } })* fn load_json_hash_table_data(base_path: &str) -> Result<(), LoadDataError> { $(paste! { let file = File::open(&format!("{}/{}.json", base_path, stringify!($table_type)))?; let reader = BufReader::new(file); let _ = [<$table_type:snake _data>]::TABLE.set( serde_json::from_reader::, Vec<[<$table_type:snake _data>]::Data>>(reader)? .into_iter() .map(|element| (element.$key_param, element)) .collect::>() ); })* Ok(()) } }; } pub fn load_all_json_data(base_path: &str) -> Result<(), LoadDataError> { load_json_data(base_path)?; load_json_hash_table_data(base_path)?; Ok(()) } json_data! { AdventureTask; Area; BaseProperty; ExploreTools; FunctionCondition; Gacha; GachaPool; GachaViewInfo; GuideTutorial; InstanceDungeon; LordGym; RoleInfo; Teleporter; WeaponConf; } json_hash_table_data! { DragonPool, id, i32; LevelEntityConfig, entity_id, i64; // TemplateConfig, blueprint_type, String; } mod textmap; pub mod text_map_data { use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::BufReader; use std::sync::OnceLock; use crate::LoadDataError; use crate::textmap::TextMapData; use crate::gacha_view_info_data; static EMPTY: OnceLock> = OnceLock::new(); static TABLE: OnceLock>> = OnceLock::new(); pub fn load_textmaps(base_path: &str) -> Result<(), LoadDataError> { // TODO: Ideally we would expose a function here to allow other components to add to the // filter, since right now only gacha uses it, we can do this let filters = get_filters(); let languages = std::fs::read_dir(base_path)? .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().is_dir()) .collect::>(); let mut result: HashMap> = HashMap::new(); for language in languages { let lang_id = language.file_name().to_str().unwrap().to_string(); let file = File::open(&format!("{base_path}/{lang_id}/multi_text/MultiText.json"))?; let reader = BufReader::new(file); result.insert( lang_id, serde_json::from_reader::, Vec>(reader)? .into_iter() .filter(|element| filters.contains(&element.id)) .map(|element| (element.id, element.content)) .collect::>(), ); } let _ = TABLE.set(result); Ok(()) } pub fn get_textmap(language: i32) -> &'static HashMap { let (text_code, _audio_code) = get_language_from_i32(language); TABLE.get_or_init(|| HashMap::new()) .get(text_code) .unwrap_or(EMPTY.get_or_init(|| HashMap::new())) } fn get_language_from_i32(language: i32) -> (&'static str, &'static str) { match language { 0 => ("zh-Hans", "zh"), 1 => ("en", "en"), 2 => ("ja", "ja"), 3 => ("ko", "ko"), 4 => ("ru", "en"), 5 => ("zh-Hant", "zh"), 6 => ("de", "en"), 7 => ("es", "en"), 8 => ("pt", "en"), 9 => ("id", "en"), 10 => ("fr", "en"), 11 => ("vi", "en"), 12 => ("th", "en"), _ => ("en", "en"), } } fn get_filters() -> HashSet { let mut filters = HashSet::new(); for gacha_view_info in gacha_view_info_data::iter() { filters.insert(gacha_view_info.summary_title.clone()); filters.insert(gacha_view_info.summary_describe.clone()); } filters } }