From e9c84be5fadd6a7c014a6dd206277f04afd30eb3 Mon Sep 17 00:00:00 2001 From: Ethan O'Brien Date: Sun, 10 May 2026 11:40:04 -0500 Subject: [PATCH] Ability to load masterdata from a folder --- src/main.rs | 4 +++- src/options.rs | 5 ++++- src/router/asset_lists.rs | 21 +++++++++++++++++--- src/router/databases/csv.rs | 25 +++++++++++++++++++----- src/runtime.rs | 39 ++++++++++++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index eff21fd..cf3fd56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ #[cfg(not(feature = "library"))] fn main() -> std::io::Result<()> { - ew::runtime::update_data_path(&ew::get_args().path); + let args = ew::get_args(); + ew::runtime::update_data_path(&args.path); + ew::runtime::update_masterdata_path(&args.masterdata); ew::run_server(false) } diff --git a/src/options.rs b/src/options.rs index 4bcefce..2789cf9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -60,7 +60,10 @@ pub struct Args { pub jp_android_asset_hash: String, #[arg(long, default_value = "", help = "Path to image assets.")] - pub image_asset_path: String + pub image_asset_path: String, + + #[arg(long, default_value = "", help = "Optional directory to load asset lists and master data CSVs from at runtime. Layout mirrors the bundled assets (asset_lists/, csv/, csv-en/). Missing files fall back to the internal copies.")] + pub masterdata: String } pub fn get_args() -> Args { diff --git a/src/router/asset_lists.rs b/src/router/asset_lists.rs index 44d7725..6d763e0 100644 --- a/src/router/asset_lists.rs +++ b/src/router/asset_lists.rs @@ -13,9 +13,9 @@ pub fn routes(cfg: &mut web::ServiceConfig) { async fn get(_req: HttpRequest) -> impl Responder { let mut response = object!{}; - response["Bundle"] = include_file!("src/router/asset_lists/Bundle.json").into(); - response["Movie"] = include_file!("src/router/asset_lists/Movie.json").into(); - response["Sound"] = include_file!("src/router/asset_lists/Sound.json").into(); + response["Bundle"] = load_list("Bundle").into(); + response["Movie"] = load_list("Movie").into(); + response["Sound"] = load_list("Sound").into(); let body = jzon::stringify(response); HttpResponse::Ok() @@ -24,6 +24,21 @@ async fn get(_req: HttpRequest) -> impl Responder { .body(body) } +fn load_list(name: &str) -> String { + let rel = format!("asset_lists/{}.json", name); + if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) { + if let Ok(s) = String::from_utf8(bytes) { + return s; + } + } + match name { + "Bundle" => include_file!("src/router/asset_lists/Bundle.json"), + "Movie" => include_file!("src/router/asset_lists/Movie.json"), + "Sound" => include_file!("src/router/asset_lists/Sound.json"), + _ => unreachable!(), + } +} + async fn supported() -> impl Responder { "SUPPORTED" } diff --git a/src/router/databases/csv.rs b/src/router/databases/csv.rs index ec9c9e9..cd5f190 100644 --- a/src/router/databases/csv.rs +++ b/src/router/databases/csv.rs @@ -33,6 +33,13 @@ fn dir_for(region: Region) -> &'static Dir<'static> { } } +fn region_subdir(region: Region) -> &'static str { + match region { + Region::Jp => "csv", + Region::En => "csv-en", + } +} + pub fn get_all(region: Region) -> JsonValue { let mut rv = object!{}; @@ -42,18 +49,26 @@ pub fn get_all(region: Region) -> JsonValue { .and_then(|s| s.to_str()) .unwrap_or_default(); - if !table_name.is_empty() { - rv[table_name] = file.contents_utf8().unwrap_or_default().into(); + if table_name.is_empty() { + continue; + } + + if let Some(bytes) = csv_bytes(region, table_name) { + rv[table_name] = String::from_utf8(bytes).unwrap_or_default().into(); } } rv } -pub fn csv_bytes(region: Region, name: &str) -> Option<&'static [u8]> { +pub fn csv_bytes(region: Region, name: &str) -> Option> { + let rel = format!("{}/{}.csv", region_subdir(region), name); + if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) { + return Some(bytes); + } dir_for(region) .get_file(format!("{name}.csv")) - .map(|f| f.contents()) + .map(|f| f.contents().to_vec()) } pub fn table(region: Region, name: &str) -> JsonValue { @@ -65,7 +80,7 @@ pub fn table(region: Region, name: &str) -> JsonValue { let bytes = csv_bytes(region, name).unwrap_or_else(|| { panic!("masterdata CSV not bundled: {name}.csv ({region:?})") }); - let parsed = parse_csv(name, bytes); + let parsed = parse_csv(name, &bytes); TABLE_CACHE.lock().unwrap().insert(key, parsed.clone()); parsed diff --git a/src/runtime.rs b/src/runtime.rs index 636cd48..4460658 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,10 +1,14 @@ use lazy_static::lazy_static; -use std::sync::RwLock; +use std::collections::HashSet; +use std::path::Path; +use std::sync::{Mutex, RwLock}; use std::fs; lazy_static! { static ref RUNNING: RwLock = RwLock::new(false); static ref DATAPATH: RwLock = RwLock::new(String::new()); + static ref MASTERDATA_PATH: RwLock = RwLock::new(String::new()); + static ref MASTERDATA_WARNED: Mutex> = Mutex::new(HashSet::new()); static ref EASTER: RwLock = RwLock::new(false); } @@ -33,6 +37,39 @@ pub fn update_data_path(path: &str) { *w = path.to_string(); } +pub fn update_masterdata_path(path: &str) { + let trimmed = path.trim_end_matches('/').to_string(); + let mut w = MASTERDATA_PATH.write().unwrap(); + if trimmed.is_empty() { + *w = String::new(); + return; + } + if !Path::new(&trimmed).is_dir() { + println!("Couldn't find masterdata directory {}", trimmed); + *w = String::new(); + return; + } + *w = trimmed; +} + +pub fn read_masterdata_file(rel_path: &str) -> Option> { + let base = MASTERDATA_PATH.read().unwrap().clone(); + if base.is_empty() { + return None; + } + let full_path = format!("{}/{}", base, rel_path); + match fs::read(&full_path) { + Ok(bytes) => Some(bytes), + Err(_) => { + let mut warned = MASTERDATA_WARNED.lock().unwrap(); + if warned.insert(rel_path.to_string()) { + println!("Couldn't find masterdata {}", rel_path); + } + None + } + } +} + // Only currently editable by the android so pub fn set_easter_mode(enabled: bool) { let mut w = EASTER.write().unwrap();