mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-08-26 23:12:20 +08:00
Compare commits
5 Commits
6eec06e164
...
042e92cb24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
042e92cb24 | ||
|
|
e7d865ebb8 | ||
|
|
3a29fde575 | ||
|
|
4ebcc06d56 | ||
|
|
59ac1f55c3 |
@@ -4,7 +4,6 @@ fn main() -> std::io::Result<()> {
|
||||
let args = ew::get_args();
|
||||
ew::runtime::update_data_path(&args.path);
|
||||
ew::runtime::update_masterdata_path(&args.masterdata);
|
||||
ew::runtime::update_mod_paths(&args.mods);
|
||||
ew::run_server(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ pub struct Args {
|
||||
#[arg(long, default_value = "", help = "Asset hash for JP Android client.")]
|
||||
pub jp_android_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Asset version for windows client.")]
|
||||
pub windows_asset_version: String,
|
||||
#[arg(long, default_value = "", help = "Asset version for overridden asset hashes.")]
|
||||
pub asset_version: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Asset hash for windows client.")]
|
||||
pub windows_asset_hash: String,
|
||||
@@ -72,10 +72,7 @@ pub struct Args {
|
||||
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,
|
||||
|
||||
#[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 masterdata: String
|
||||
}
|
||||
|
||||
pub fn get_args() -> Args {
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::Mutex;
|
||||
use crate::include_file;
|
||||
|
||||
lazy_static! {
|
||||
static ref MERGED_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
|
||||
static ref LIST_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||
@@ -32,11 +32,11 @@ async fn get(_req: HttpRequest) -> impl Responder {
|
||||
}
|
||||
|
||||
fn load_list(name: &str) -> String {
|
||||
if let Some(cached) = MERGED_CACHE.lock().unwrap().get(name) {
|
||||
if let Some(cached) = LIST_CACHE.lock().unwrap().get(name) {
|
||||
return cached.clone();
|
||||
}
|
||||
let rel = format!("asset_lists/{}.json", name);
|
||||
let base = crate::runtime::read_masterdata_file(&rel)
|
||||
let list = crate::runtime::read_masterdata_file(&rel)
|
||||
.and_then(|b| String::from_utf8(b).ok())
|
||||
.unwrap_or_else(|| match name {
|
||||
"Bundle" => include_file!("src/router/asset_lists/Bundle.json"),
|
||||
@@ -45,62 +45,8 @@ fn load_list(name: &str) -> String {
|
||||
_ => 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);
|
||||
}
|
||||
}
|
||||
|
||||
for (mod_dir, bytes) in mod_files {
|
||||
let Ok(s) = String::from_utf8(bytes) else { continue };
|
||||
let Ok(mod_root) = jzon::parse(&s) else { continue };
|
||||
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)
|
||||
LIST_CACHE.lock().unwrap().insert(name.to_string(), list.clone());
|
||||
list
|
||||
}
|
||||
|
||||
async fn supported() -> impl Responder {
|
||||
|
||||
@@ -223,6 +223,18 @@ lazy_static! {
|
||||
|
||||
pub static ref MISSION_REWARD: JsonValue = index_by(&t("mission_reward"), "id");
|
||||
|
||||
pub static ref MISSION_REWARDS: JsonValue = {
|
||||
let mut info = object! {};
|
||||
for data in t("mission_reward").members() {
|
||||
let id = data["id"].to_string();
|
||||
if info[&id].is_null() {
|
||||
info[&id] = array![];
|
||||
}
|
||||
info[&id].push(data.clone()).unwrap();
|
||||
}
|
||||
info
|
||||
};
|
||||
|
||||
pub static ref ITEM_INFO: JsonValue = index_by(&t("item"), "id");
|
||||
|
||||
pub static ref MUSIC: JsonValue = {
|
||||
@@ -259,6 +271,18 @@ lazy_static! {
|
||||
|
||||
pub static ref RANKS: JsonValue = t("user_rank");
|
||||
|
||||
pub static ref USER_RANK_REWARD: JsonValue = {
|
||||
let mut info = object! {};
|
||||
for data in t("user_rank_reward").members() {
|
||||
let id = data["id"].to_string();
|
||||
if info[&id].is_null() {
|
||||
info[&id] = array![];
|
||||
}
|
||||
info[&id].push(data.clone()).unwrap();
|
||||
}
|
||||
info
|
||||
};
|
||||
|
||||
pub static ref EVOLVE_COST: JsonValue = {
|
||||
let mut info = object! {};
|
||||
for data in t("card_evolve").members() {
|
||||
|
||||
@@ -33,7 +33,7 @@ static ASSET_VERSIONS: &[AssetVersion] = &[
|
||||
|
||||
// Re-written client versions 2.2.0 -
|
||||
AssetVersion { region: "JP", platform: "Windows", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "765a689bd642f3c4545d8d8e69c57d3e", latest: true },
|
||||
AssetVersion { region: "JP", platform: "Android", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "5ba946aef8cc3fbf95867dc465cad336", latest: false },
|
||||
AssetVersion { region: "JP", platform: "Android", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "704828bd25330314c47de28938875e46", latest: false },
|
||||
AssetVersion { region: "JP", platform: "iOS", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "6cfbebbe32a5cd9391f4b44d1a384f62", latest: false },
|
||||
|
||||
//AssetVersion { region: "JP", platform: "WebGL", version: "4c921d2443335e574a82e04ec9ea243c", hash: "e1ff7c74b20c8d216507972b6f24b9df", latest: true },
|
||||
@@ -51,13 +51,14 @@ impl AssetVersion {
|
||||
|
||||
fn override_pair(&self) -> Option<(String, String)> {
|
||||
let args = crate::get_args();
|
||||
let (ov, oh) = match (self.region, self.platform) {
|
||||
("JP", "Windows") => (args.windows_asset_version.as_str(), args.windows_asset_hash.as_str()),
|
||||
("JP", "Android") => ("", args.jp_android_asset_hash.as_str()),
|
||||
("JP", "iOS") => ("", args.jp_ios_asset_hash.as_str()),
|
||||
("GL", "Android") => ("", args.en_android_asset_hash.as_str()),
|
||||
("GL", "iOS") => ("", args.en_ios_asset_hash.as_str()),
|
||||
_ => ("", ""),
|
||||
let ov = args.asset_version.as_str();
|
||||
let oh = match (self.region, self.platform) {
|
||||
("JP", "Windows") => args.windows_asset_hash.as_str(),
|
||||
("JP", "Android") => args.jp_android_asset_hash.as_str(),
|
||||
("JP", "iOS") => args.jp_ios_asset_hash.as_str(),
|
||||
("GL", "Android") => args.en_android_asset_hash.as_str(),
|
||||
("GL", "iOS") => args.en_ios_asset_hash.as_str(),
|
||||
_ => "",
|
||||
};
|
||||
if ov.is_empty() && oh.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -367,6 +367,22 @@ pub fn give_exp(amount: i32, user: &mut JsonValue, mission: &mut JsonValue, rv:
|
||||
if current_rank["rank"] != new_rank["rank"] {
|
||||
user["stamina"]["stamina"] = (user["stamina"]["stamina"].as_i64().unwrap() + new_rank["maxLp"].as_i64().unwrap()).into();
|
||||
|
||||
let from = current_rank["rank"].as_i64().unwrap();
|
||||
let to = new_rank["rank"].as_i64().unwrap();
|
||||
for rank in databases::RANKS.members() {
|
||||
let num = rank["rank"].as_i64().unwrap();
|
||||
if num <= from || num > to {
|
||||
continue;
|
||||
}
|
||||
for reward in databases::USER_RANK_REWARD[rank["masterUserRankRewardId"].to_string()].members() {
|
||||
give_gift(&object!{
|
||||
reward_type: reward["type"].clone(),
|
||||
value: reward["value"].clone(),
|
||||
amount: reward["amount"].clone()
|
||||
}, user, mission, &mut array![], &mut array![]);
|
||||
}
|
||||
}
|
||||
|
||||
let status = get_mission_status(get_variable_mission_num(1101001, 1101030, mission), mission);
|
||||
if status.is_empty() {
|
||||
return;
|
||||
|
||||
@@ -70,27 +70,28 @@ async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Respo
|
||||
for mission in body["master_mission_ids"].members() {
|
||||
let mid = mission.as_i64().unwrap();
|
||||
let mission_info = databases::MISSION_LIST[mid.to_string()].clone();
|
||||
let master = databases::MISSION_REWARD[mission_info["masterMissionRewardId"].to_string()].clone();
|
||||
let reward_type = master["type"].as_i64().unwrap();
|
||||
for master in databases::MISSION_REWARDS[mission_info["masterMissionRewardId"].to_string()].members() {
|
||||
let reward_type = master["type"].as_i64().unwrap();
|
||||
|
||||
rewards.push(object!{
|
||||
give_type: master["giveType"].clone(),
|
||||
type: master["type"].clone(),
|
||||
value: master["value"].clone(),
|
||||
level: master["level"].clone(),
|
||||
amount: master["amount"].clone()
|
||||
}).unwrap();
|
||||
rewards.push(object!{
|
||||
give_type: master["giveType"].clone(),
|
||||
type: master["type"].clone(),
|
||||
value: master["value"].clone(),
|
||||
level: master["level"].clone(),
|
||||
amount: master["amount"].clone()
|
||||
}).unwrap();
|
||||
|
||||
items::give_gift(&object!{
|
||||
reward_type: reward_type,
|
||||
value: master["value"].clone(),
|
||||
amount: master["amount"].clone()
|
||||
}, &mut user, &mut missions, &mut array![], &mut chats);
|
||||
items::give_gift(&object!{
|
||||
reward_type: reward_type,
|
||||
value: master["value"].clone(),
|
||||
amount: master["amount"].clone()
|
||||
}, &mut user, &mut missions, &mut array![], &mut chats);
|
||||
|
||||
match reward_type {
|
||||
1 => touched_gem = true,
|
||||
4 => touched_coin = true,
|
||||
_ => { touched_items.push(master["value"].clone()).unwrap(); }
|
||||
match reward_type {
|
||||
1 => touched_gem = true,
|
||||
4 => touched_coin = true,
|
||||
_ => { touched_items.push(master["value"].clone()).unwrap(); }
|
||||
}
|
||||
}
|
||||
|
||||
let mut variable = false;
|
||||
|
||||
@@ -9,7 +9,6 @@ lazy_static! {
|
||||
static ref DATAPATH: 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 MOD_PATHS: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
||||
static ref EASTER: RwLock<bool> = RwLock::new(false);
|
||||
static ref HOST_CONFIG: RwLock<HostConfig> = RwLock::new(HostConfig::default());
|
||||
}
|
||||
@@ -100,42 +99,6 @@ pub fn get_easter_mode() -> bool {
|
||||
*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
|
||||
}
|
||||
|
||||
pub fn apply_config_json(json: &str) {
|
||||
let parsed = match jzon::parse(json) {
|
||||
Ok(p) => p,
|
||||
|
||||
Reference in New Issue
Block a user