mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-07-12 00:32:20 +08:00
Add support for mods (Probably)
This commit is contained in:
2
build.rs
2
build.rs
@@ -11,6 +11,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !std::fs::exists("webui/index.html").unwrap_or(false) {
|
if !std::fs::exists("webui/index.html").unwrap_or(false) {
|
||||||
panic!("Could not compile crate! Missing webui! Did you pull submodules?");
|
panic!("Missing webui! Did you pull submodules?\nRun `git submodule update --init --recursive`");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
let args = ew::get_args();
|
let args = ew::get_args();
|
||||||
ew::runtime::update_data_path(&args.path);
|
ew::runtime::update_data_path(&args.path);
|
||||||
ew::runtime::update_masterdata_path(&args.masterdata);
|
ew::runtime::update_masterdata_path(&args.masterdata);
|
||||||
|
ew::runtime::update_mod_paths(&args.mods);
|
||||||
ew::run_server(false)
|
ew::run_server(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ pub struct Args {
|
|||||||
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.")]
|
#[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 masterdata: String,
|
||||||
|
|
||||||
|
#[arg(long = "mod", value_name = "DIR", action = clap::ArgAction::Append, help = "Path to a mod directory layered on top of --masterdata + the bundled defaults. May be passed multiple times. Each mod dir mirrors the masterdata layout (asset_lists/, csv/, csv-en/, userdata/) but only needs to include the files it adds rows to. CSV rows merge by primary key (first column), asset_lists entries merge by m_identifier, new_user.json top-level arrays union. Later --mod wins on collisions.")]
|
||||||
|
pub mods: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_args() -> Args {
|
pub fn get_args() -> Args {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
use actix_web::{HttpRequest, web, Responder, HttpResponse};
|
use actix_web::{HttpRequest, web, Responder, HttpResponse};
|
||||||
use actix_web::http::header::ContentType;
|
use actix_web::http::header::ContentType;
|
||||||
use jzon::object;
|
use jzon::object;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
use crate::include_file;
|
use crate::include_file;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref MERGED_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes(cfg: &mut web::ServiceConfig) {
|
pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/assetLists")
|
web::scope("/assetLists")
|
||||||
@@ -25,18 +32,75 @@ async fn get(_req: HttpRequest) -> impl Responder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_list(name: &str) -> String {
|
fn load_list(name: &str) -> String {
|
||||||
|
if let Some(cached) = MERGED_CACHE.lock().unwrap().get(name) {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
let rel = format!("asset_lists/{}.json", name);
|
let rel = format!("asset_lists/{}.json", name);
|
||||||
if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) {
|
let base = crate::runtime::read_masterdata_file(&rel)
|
||||||
if let Ok(s) = String::from_utf8(bytes) {
|
.and_then(|b| String::from_utf8(b).ok())
|
||||||
return s;
|
.unwrap_or_else(|| 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!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mod_files = crate::runtime::read_mod_files(&rel);
|
||||||
|
let merged = if mod_files.is_empty() {
|
||||||
|
base
|
||||||
|
} else {
|
||||||
|
merge_asset_list(name, base, mod_files)
|
||||||
|
};
|
||||||
|
MERGED_CACHE.lock().unwrap().insert(name.to_string(), merged.clone());
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_asset_list(name: &str, base: String, mod_files: Vec<(String, Vec<u8>)>) -> String {
|
||||||
|
let mut root = match jzon::parse(&base) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return base,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut by_ident: HashMap<String, usize> = HashMap::new();
|
||||||
|
if let jzon::JsonValue::Array(ref arr) = root["m_manifestCollection"] {
|
||||||
|
for (i, e) in arr.iter().enumerate() {
|
||||||
|
let id = e["m_identifier"].to_string();
|
||||||
|
by_ident.insert(id.to_string(), i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match name {
|
|
||||||
"Bundle" => include_file!("src/router/asset_lists/Bundle.json"),
|
for (mod_dir, bytes) in mod_files {
|
||||||
"Movie" => include_file!("src/router/asset_lists/Movie.json"),
|
let Ok(s) = String::from_utf8(bytes) else { continue };
|
||||||
"Sound" => include_file!("src/router/asset_lists/Sound.json"),
|
let Ok(mod_root) = jzon::parse(&s) else { continue };
|
||||||
_ => unreachable!(),
|
let mod_entries = match mod_root["m_manifestCollection"] {
|
||||||
|
jzon::JsonValue::Array(ref arr) => arr.clone(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
let mut added = 0usize;
|
||||||
|
let mut replaced = 0usize;
|
||||||
|
for entry in mod_entries {
|
||||||
|
let ident = entry["m_identifier"].as_str().unwrap_or("").to_string();
|
||||||
|
if ident.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(&idx) = by_ident.get(&ident) {
|
||||||
|
root["m_manifestCollection"][idx] = entry;
|
||||||
|
replaced += 1;
|
||||||
|
} else {
|
||||||
|
let idx = root["m_manifestCollection"].len();
|
||||||
|
let _ = root["m_manifestCollection"].push(entry);
|
||||||
|
by_ident.insert(ident, idx);
|
||||||
|
added += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if added > 0 || replaced > 0 {
|
||||||
|
println!(
|
||||||
|
"[mod {}] {}.json: +{} new entries, {} replaced",
|
||||||
|
mod_dir, name, added, replaced
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
jzon::stringify(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn supported() -> impl Responder {
|
async fn supported() -> impl Responder {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ lazy_static! {
|
|||||||
static ref DATAPATH: RwLock<String> = RwLock::new(String::new());
|
static ref DATAPATH: RwLock<String> = RwLock::new(String::new());
|
||||||
static ref MASTERDATA_PATH: RwLock<String> = RwLock::new(String::new());
|
static ref MASTERDATA_PATH: RwLock<String> = RwLock::new(String::new());
|
||||||
static ref MASTERDATA_WARNED: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
|
static ref MASTERDATA_WARNED: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
|
||||||
|
static ref MOD_PATHS: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
||||||
static ref EASTER: RwLock<bool> = RwLock::new(false);
|
static ref EASTER: RwLock<bool> = RwLock::new(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,3 +80,39 @@ pub fn set_easter_mode(enabled: bool) {
|
|||||||
pub fn get_easter_mode() -> bool {
|
pub fn get_easter_mode() -> bool {
|
||||||
*EASTER.read().unwrap()
|
*EASTER.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_mod_paths(paths: &[String]) {
|
||||||
|
let cleaned: Vec<String> = paths.iter()
|
||||||
|
.map(|p| p.trim_end_matches('/').to_string())
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.filter_map(|p| {
|
||||||
|
if Path::new(&p).is_dir() {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
println!("Couldn't find mod directory {} — skipping", p);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !cleaned.is_empty() {
|
||||||
|
println!("Loaded {} mod overlay{}", cleaned.len(),
|
||||||
|
if cleaned.len() == 1 { "" } else { "s" });
|
||||||
|
for p in &cleaned {
|
||||||
|
println!(" mod: {}", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut w = MOD_PATHS.write().unwrap();
|
||||||
|
*w = cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_mod_files(rel_path: &str) -> Vec<(String, Vec<u8>)> {
|
||||||
|
let paths = MOD_PATHS.read().unwrap().clone();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for p in paths {
|
||||||
|
let full = format!("{}/{}", p, rel_path);
|
||||||
|
if let Ok(bytes) = fs::read(&full) {
|
||||||
|
out.push((p, bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user