From c127ee4bd4778d55f4beacc70e5e1c51db443060 Mon Sep 17 00:00:00 2001 From: Alula <6276139+alula@users.noreply.github.com> Date: Thu, 10 Feb 2022 08:54:20 +0100 Subject: [PATCH] use path list for resource loading --- src/bmfont_renderer.rs | 15 +++--- src/engine_constants/mod.rs | 45 ++++++++++++---- src/framework/filesystem.rs | 56 +++++++++++++------- src/map.rs | 8 +-- src/menu/settings_menu.rs | 13 +++-- src/player/skin/basic.rs | 7 +-- src/scene/editor_scene.rs | 2 +- src/scene/game_scene.rs | 22 ++++---- src/scene/loading_scene.rs | 26 ++------- src/scripting/tsc/text_script.rs | 2 +- src/shared_game_state.rs | 91 ++++++++++++++++++-------------- src/stage.rs | 58 ++++++++++---------- src/texture_set.rs | 41 +++++--------- 13 files changed, 209 insertions(+), 177 deletions(-) diff --git a/src/bmfont_renderer.rs b/src/bmfont_renderer.rs index 550d6ea..f06d209 100644 --- a/src/bmfont_renderer.rs +++ b/src/bmfont_renderer.rs @@ -16,25 +16,24 @@ pub struct BMFontRenderer { } impl BMFontRenderer { - pub fn load(root: &str, desc_path: &str, ctx: &mut Context) -> GameResult { - let root = PathBuf::from(root); - let full_path = &root.join(PathBuf::from(desc_path)); + pub fn load(roots: &Vec, desc_path: &str, ctx: &mut Context) -> GameResult { + let full_path = PathBuf::from(desc_path); let desc_stem = full_path.file_stem().ok_or_else(|| ResourceLoadError("Cannot extract the file stem.".to_owned()))?; - let stem = full_path.parent().unwrap_or(full_path).join(desc_stem); + let stem = full_path.parent().unwrap_or(&full_path).join(desc_stem); - let font = BMFont::load_from(filesystem::open(ctx, &full_path)?)?; + let font = BMFont::load_from(filesystem::open_find(ctx, roots, &full_path)?)?; let mut pages = Vec::new(); let (zeros, _, _) = FILE_TYPES .iter() .map(|ext| (1, ext, format!("{}_0{}", stem.to_string_lossy(), ext))) - .find(|(_, _, path)| filesystem::exists(ctx, &path)) + .find(|(_, _, path)| filesystem::exists_find(ctx, roots, &path)) .or_else(|| { FILE_TYPES .iter() .map(|ext| (2, ext, format!("{}_00{}", stem.to_string_lossy(), ext))) - .find(|(_, _, path)| filesystem::exists(ctx, &path)) + .find(|(_, _, path)| filesystem::exists_find(ctx, roots, &path)) }) .ok_or_else(|| ResourceLoadError(format!("Cannot find glyph atlas 0 for font: {:?}", desc_path)))?; @@ -164,7 +163,7 @@ impl BMFontRenderer { let batch = texture_set.get_or_load_batch(ctx, constants, page_tex)?; let mut offset_x = x; - for (chr, glyph) in chars.iter() { + for (_chr, glyph) in chars.iter() { if glyph.page == page { batch.add_rect_scaled_tinted( offset_x + (glyph.xoffset as f32 * constants.font_scale), diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index aba5341..2f6b514 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -12,6 +12,8 @@ use crate::framework::error::GameResult; use crate::framework::filesystem; use crate::player::ControlMode; use crate::scripting::tsc::text_script::TextScriptEncoding; +use crate::settings::Settings; +use crate::shared_game_state::Season; use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform}; use crate::sound::SoundManager; @@ -276,6 +278,7 @@ impl Clone for TitleConsts { #[derive(Debug)] pub struct EngineConstants { + pub base_paths: Vec, pub is_cs_plus: bool, pub is_switch: bool, pub supports_og_textures: bool, @@ -303,6 +306,7 @@ pub struct EngineConstants { impl Clone for EngineConstants { fn clone(&self) -> EngineConstants { EngineConstants { + base_paths: self.base_paths.clone(), is_cs_plus: self.is_cs_plus, is_switch: self.is_switch, supports_og_textures: self.supports_og_textures, @@ -332,6 +336,7 @@ impl Clone for EngineConstants { impl EngineConstants { pub fn defaults() -> Self { EngineConstants { + base_paths: Vec::new(), is_cs_plus: false, is_switch: false, supports_og_textures: false, @@ -1664,15 +1669,38 @@ impl EngineConstants { self.game.tile_offset_x = 3; } + pub fn rebuild_path_list(&mut self, mod_path: Option, season: Season, settings: &Settings) { + self.base_paths.clear(); + self.base_paths.push("/".to_owned()); + + if self.is_cs_plus { + self.base_paths.insert(0, "/base/".to_owned()); + + if settings.original_textures { + self.base_paths.insert(0, "/base/ogph/".to_string()) + } else if settings.seasonal_textures { + match season { + Season::Halloween => self.base_paths.insert(0, "/Halloween/season/".to_string()), + Season::Christmas => self.base_paths.insert(0, "/Christmas/season/".to_string()), + _ => {} + } + } + } + + if let Some(mod_path) = mod_path { + self.base_paths.insert(0, mod_path); + } + } + pub fn apply_constant_json_files(&mut self) {} /// Loads bullet.tbl and arms_level.tbl from CS+ files, /// even though they match vanilla 1:1, we should load them for completeness /// or if any crazy person uses it for a CS+ mod... pub fn load_csplus_tables(&mut self, ctx: &mut Context) -> GameResult { - if filesystem::exists(ctx, "/base/bullet.tbl") { + if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "/bullet.tbl") { let mut data = Vec::new(); - filesystem::open(ctx, "/base/bullet.tbl")?.read_to_end(&mut data)?; + file.read_to_end(&mut data)?; let bullets = data.len() / 0x2A; let mut f = Cursor::new(data); @@ -1699,13 +1727,11 @@ impl EngineConstants { self.weapon.bullet_table = new_bullet_table; log::info!("Loaded bullet.tbl."); - } else { - log::warn!("CS+ bullet.tbl not found."); } - if filesystem::exists(ctx, "/base/arms_level.tbl") { + if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "/arms_level.tbl") { let mut data = Vec::new(); - filesystem::open(ctx, "/base/arms_level.tbl")?.read_to_end(&mut data)?; + file.read_to_end(&mut data)?; let mut f = Cursor::new(data); let mut new_level_table = EngineConstants::defaults().weapon.level_table; @@ -1718,8 +1744,6 @@ impl EngineConstants { self.weapon.level_table = new_level_table; log::info!("Loaded arms_level.tbl."); - } else { - log::warn!("CS+ arms_level.tbl not found.") } Ok(()) @@ -1728,8 +1752,9 @@ impl EngineConstants { /// Load in the `faceanm.dat` file that details the Switch extensions to the GameResult { - if filesystem::exists(ctx, "/base/faceanm.dat") { - let file = filesystem::open(ctx, "/base/faceanm.dat")?; + self.animated_face_table.clear(); + + if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "/faceanm.dat") { let buf = BufReader::new(file); let mut face_id = 1; let mut anim_id = 0; diff --git a/src/framework/filesystem.rs b/src/framework/filesystem.rs index c26aa31..924be7b 100644 --- a/src/framework/filesystem.rs +++ b/src/framework/filesystem.rs @@ -2,6 +2,7 @@ use std::fmt; use std::io; use std::io::SeekFrom; use std::path; +use std::path::PathBuf; use crate::framework::context::Context; use crate::framework::error::{GameError, GameResult}; @@ -73,10 +74,7 @@ impl Filesystem { // User data VFS. let user_overlay = vfs::OverlayFS::new(); - Filesystem { - vfs: overlay, - user_vfs: user_overlay, - } + Filesystem { vfs: overlay, user_vfs: user_overlay } } /// Opens the given `path` and returns the resulting `File` @@ -96,12 +94,9 @@ impl Filesystem { /// Note that even if you open a file read-write, it can only /// write to files in the "user" directory. pub(crate) fn open_options>(&self, path: P, options: OpenOptions) -> GameResult { - self.user_vfs - .open_options(path.as_ref(), options) - .map(|f| File::VfsFile(f)) - .map_err(|e| { - GameError::ResourceLoadError(format!("Tried to open {:?} but got error: {:?}", path.as_ref(), e)) - }) + self.user_vfs.open_options(path.as_ref(), options).map(|f| File::VfsFile(f)).map_err(|e| { + GameError::ResourceLoadError(format!("Tried to open {:?} but got error: {:?}", path.as_ref(), e)) + }) } /// Creates a new file in the user directory and opens it @@ -140,10 +135,7 @@ impl Filesystem { /// Check whether a path points at a file. pub(crate) fn user_is_file>(&self, path: P) -> bool { - self.user_vfs - .metadata(path.as_ref()) - .map(|m| m.is_file()) - .unwrap_or(false) + self.user_vfs.metadata(path.as_ref()).map(|m| m.is_file()).unwrap_or(false) } /// Check whether a path points at a file. @@ -153,10 +145,7 @@ impl Filesystem { /// Check whether a path points at a directory. pub(crate) fn user_is_dir>(&self, path: P) -> bool { - self.user_vfs - .metadata(path.as_ref()) - .map(|m| m.is_dir()) - .unwrap_or(false) + self.user_vfs.metadata(path.as_ref()).map(|m| m.is_dir()).unwrap_or(false) } /// Check whether a path points at a directory. @@ -240,6 +229,23 @@ pub fn open>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.open(path) } +pub fn open_find>(ctx: &Context, roots: &Vec, path: P) -> GameResult { + let mut errors = Vec::new(); + for root in roots { + let mut full_path = root.to_string(); + full_path.push_str(path.as_ref().to_string_lossy().as_ref()); + + let result = ctx.filesystem.open(&full_path); + if result.is_ok() { + return result; + } + + errors.push((PathBuf::from(full_path), result.err().unwrap())); + } + + Err(GameError::ResourceNotFound("File not found".to_owned(), errors)) +} + /// Opens the given path in the user directory and returns the resulting `File` /// in read-only mode. pub fn user_open>(ctx: &Context, path: P) -> GameResult { @@ -306,6 +312,20 @@ pub fn exists>(ctx: &Context, path: P) -> bool { ctx.filesystem.exists(path.as_ref()) } +pub fn exists_find>(ctx: &Context, roots: &Vec, path: P) -> bool { + for root in roots { + let mut full_path = root.to_string(); + full_path.push_str(path.as_ref().to_string_lossy().as_ref()); + + if ctx.filesystem.exists(full_path) { + return true; + } + } + + false +} + + /// Check whether a path points at a file. pub fn is_file>(ctx: &Context, path: P) -> bool { ctx.filesystem.is_file(path) diff --git a/src/map.rs b/src/map.rs index 264c85b..23423d4 100644 --- a/src/map.rs +++ b/src/map.rs @@ -69,7 +69,7 @@ impl Map { pub fn load_pxpack( mut map_data: R, - root: &str, + roots: &Vec, data: &mut StageData, ctx: &mut Context, ) -> GameResult { @@ -206,11 +206,13 @@ impl Map { )?; } - if let Ok(mut attrib_data) = filesystem::open(ctx, [root, "Stage/", &tileset_fg, ".pxa"].join("")) { + if let Ok(mut attrib_data) = filesystem::open_find(ctx, roots, ["/Stage/", &tileset_fg, ".pxa"].join("")) { if attrib_data.read_exact(&mut attrib).is_err() { log::warn!("Map attribute data is shorter than 256 bytes!"); } - } else if let Ok(mut attrib_data) = filesystem::open(ctx, [root, "Stage/", &tileset_fg, ".pxattr"].join("")) { + } else if let Ok(mut attrib_data) = + filesystem::open_find(ctx, roots, ["/Stage/", &tileset_fg, ".pxattr"].join("")) + { attrib_data.read_exact(&mut magic)?; if &magic != b"pxMAP01\0" { diff --git a/src/menu/settings_menu.rs b/src/menu/settings_menu.rs index 3e2816a..dfaffe8 100644 --- a/src/menu/settings_menu.rs +++ b/src/menu/settings_menu.rs @@ -47,9 +47,14 @@ impl SettingsMenu { .push_entry(MenuEntry::Toggle("Motion interpolation:".to_string(), state.settings.motion_interpolation)); self.graphics.push_entry(MenuEntry::Toggle("Subpixel scrolling:".to_string(), state.settings.subpixel_coords)); + // NS version uses two different maps, therefore we can't dynamically switch between graphics presets. if state.constants.supports_og_textures { - self.graphics - .push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); + if !state.constants.is_switch || self.on_title { + self.graphics + .push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); + } else { + self.graphics.push_entry(MenuEntry::Disabled("Original textures".to_string())); + } } else { self.graphics.push_entry(MenuEntry::Hidden); } @@ -227,7 +232,7 @@ impl SettingsMenu { MenuSelectionResult::Selected(4, toggle) => { if let MenuEntry::Toggle(_, value) = toggle { state.settings.original_textures = !state.settings.original_textures; - state.reload_textures(); + state.reload_resources(ctx)?; let _ = state.settings.save(ctx); *value = state.settings.original_textures; @@ -236,7 +241,7 @@ impl SettingsMenu { MenuSelectionResult::Selected(5, toggle) => { if let MenuEntry::Toggle(_, value) = toggle { state.settings.seasonal_textures = !state.settings.seasonal_textures; - state.reload_textures(); + state.reload_graphics(); let _ = state.settings.save(ctx); *value = state.settings.seasonal_textures; diff --git a/src/player/skin/basic.rs b/src/player/skin/basic.rs index 524f152..c91a5a7 100644 --- a/src/player/skin/basic.rs +++ b/src/player/skin/basic.rs @@ -85,16 +85,17 @@ pub struct BasicPlayerSkin { impl BasicPlayerSkin { pub fn new(texture_name: String, state: &SharedGameState, ctx: &mut Context) -> BasicPlayerSkin { - let metapath = format!("{}/{}.dskinmeta", state.base_path, texture_name); let mut metadata = DEFAULT_SKINMETA.clone(); - if let Ok(file) = filesystem::open(ctx, metapath) { + let meta_path = format!("/{}.dskinmeta", texture_name); + + if let Ok(file) = filesystem::open_find(ctx, &state.constants.base_paths, &meta_path) { match serde_json::from_reader::(file) { Ok(meta) if SUPPORTED_SKINMETA_VERSIONS.contains(&meta.version) => { metadata = meta; } Ok(meta) => { - log::warn!("Unsupported skin metadata file version: {}", meta.version); + log::warn!("{}: Unsupported skin metadata file version: {}", meta_path, meta.version); } Err(err) => { log::warn!("Failed to load skin metadata file: {:?}", err); diff --git a/src/scene/editor_scene.rs b/src/scene/editor_scene.rs index ffb1d26..7680307 100644 --- a/src/scene/editor_scene.rs +++ b/src/scene/editor_scene.rs @@ -71,7 +71,7 @@ impl EditorScene { } if let Some(stage) = state.stages.get(stage_id) { - let stage = Stage::load(&state.base_path, stage, ctx)?; + let stage = Stage::load(&state.constants.base_paths, stage, ctx)?; let new_instance = EditorInstance::new(stage_id, stage); self.instances.push(new_instance); diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 15a1a16..0232f4c 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -102,7 +102,7 @@ const CUTSCENE_SKIP_WAIT: u16 = 50; impl GameScene { pub fn new(state: &mut SharedGameState, ctx: &mut Context, id: usize) -> GameResult { info!("Loading stage {} ({})", id, &state.stages[id].map); - let stage = Stage::load(&state.base_path, &state.stages[id], ctx)?; + let stage = Stage::load(&state.constants.base_paths, &state.stages[id], ctx)?; info!("Loaded stage: {}", stage.data.name); GameScene::from_stage(state, ctx, stage, id) @@ -112,13 +112,17 @@ impl GameScene { let mut water_params = WaterParams::new(); let mut water_renderer = WaterRenderer::new(); - if let Ok(water_param_file) = - filesystem::open(ctx, [&state.base_path, "Stage/", &state.stages[id].tileset.name, ".pxw"].join("")) - { - water_params.load_from(water_param_file)?; - info!("Loaded water parameters file."); + if !state.settings.original_textures { + if let Ok(water_param_file) = filesystem::open_find( + ctx, + &state.constants.base_paths, + ["Stage/", &state.stages[id].tileset.name, ".pxw"].join(""), + ) { + water_params.load_from(water_param_file)?; + info!("Loaded water parameters file."); - water_renderer.initialize(stage.map.find_water_regions()); + water_renderer.initialize(stage.map.find_water_regions()); + } } let stage_textures = { @@ -1524,7 +1528,7 @@ impl Scene for GameScene { .wrapping_add(self.stage_id as i32) .rotate_right(7); state.game_rng = XorShift::new(seed); - state.textscript_vm.set_scene_script(self.stage.load_text_script(&state.base_path, &state.constants, ctx)?); + state.textscript_vm.set_scene_script(self.stage.load_text_script(&state.constants.base_paths, &state.constants, ctx)?); state.textscript_vm.suspend = false; state.tile_size = self.stage.map.tile_size; #[cfg(feature = "scripting-lua")] @@ -1533,7 +1537,7 @@ impl Scene for GameScene { self.player1.controller = state.settings.create_player1_controller(); self.player2.controller = state.settings.create_player2_controller(); - let npcs = self.stage.load_npcs(&state.base_path, ctx)?; + let npcs = self.stage.load_npcs(&state.constants.base_paths, ctx)?; for npc_data in npcs.iter() { log::info!("creating npc: {:?}", npc_data); diff --git a/src/scene/loading_scene.rs b/src/scene/loading_scene.rs index aadc475..61aaf9b 100644 --- a/src/scene/loading_scene.rs +++ b/src/scene/loading_scene.rs @@ -1,11 +1,12 @@ +use imgui::Ui; + use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::framework::filesystem; +use crate::framework::ui::Components; use crate::npc::NPCTable; use crate::scene::no_data_scene::NoDataScene; use crate::scene::Scene; -use crate::scripting::tsc::credit_script::CreditScript; -use crate::scripting::tsc::text_script::TextScript; use crate::shared_game_state::SharedGameState; use crate::stage::StageData; @@ -19,26 +20,7 @@ impl LoadingScene { } fn load_stuff(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - let stages = StageData::load_stage_table(ctx, &state.base_path)?; - state.stages = stages; - let npc_tbl = filesystem::open(ctx, [&state.base_path, "/npc.tbl"].join(""))?; - let npc_table = NPCTable::load_from(npc_tbl)?; - state.npc_table = npc_table; - let head_tsc = filesystem::open(ctx, [&state.base_path, "/Head.tsc"].join(""))?; - let head_script = TextScript::load_from(head_tsc, &state.constants)?; - state.textscript_vm.set_global_script(head_script); - - let arms_item_tsc = filesystem::open(ctx, [&state.base_path, "/ArmsItem.tsc"].join(""))?; - let arms_item_script = TextScript::load_from(arms_item_tsc, &state.constants)?; - state.textscript_vm.set_inventory_script(arms_item_script); - - let stage_select_tsc = filesystem::open(ctx, [&state.base_path, "/StageSelect.tsc"].join(""))?; - let stage_select_script = TextScript::load_from(stage_select_tsc, &state.constants)?; - state.textscript_vm.set_stage_select_script(stage_select_script); - - let credit_tsc = filesystem::open(ctx, [&state.base_path, "/Credit.tsc"].join(""))?; - let credit_script = CreditScript::load_from(credit_tsc, &state.constants)?; - state.creditscript_vm.set_script(credit_script); + state.reload_resources(ctx)?; if ctx.headless { log::info!("Headless mode detected, skipping intro and loading last saved game."); diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index 99099f1..dca51c2 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -1630,7 +1630,7 @@ impl TextScriptVM { for path in &state.constants.credit_illustration_paths { let path = format!("{}Credit{:02}", path, number); - if state.texture_set.find_texture(ctx, &path).is_some() { + if state.texture_set.find_texture(ctx, &state.constants.base_paths, &path).is_some() { state.textscript_vm.current_illustration = Some(path); break; } diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 5750220..29288d8 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -6,15 +6,15 @@ use chrono::{Datelike, Local}; use crate::bmfont_renderer::BMFontRenderer; use crate::caret::{Caret, CaretType}; use crate::common::{ControlFlags, Direction, FadeState}; -use crate::components::draw_common::{draw_number, Alignment}; +use crate::components::draw_common::{Alignment, draw_number}; use crate::engine_constants::EngineConstants; +use crate::framework::{filesystem, graphics}; use crate::framework::backend::BackendTexture; use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::framework::graphics::{create_texture_mutable, set_render_target}; use crate::framework::keyboard::ScanCode; use crate::framework::vfs::OpenOptions; -use crate::framework::{filesystem, graphics}; #[cfg(feature = "hooks")] use crate::hooks::init_hooks; use crate::input::touch_controls::TouchControls; @@ -22,12 +22,12 @@ use crate::npc::NPCTable; use crate::profile::GameProfile; use crate::rng::XorShift; use crate::scene::game_scene::GameScene; -use crate::scene::title_scene::TitleScene; use crate::scene::Scene; +use crate::scene::title_scene::TitleScene; #[cfg(feature = "scripting-lua")] use crate::scripting::lua::LuaScriptingState; -use crate::scripting::tsc::credit_script::CreditScriptVM; -use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; +use crate::scripting::tsc::credit_script::{CreditScript, CreditScriptVM}; +use crate::scripting::tsc::text_script::{ScriptMode, TextScript, TextScriptExecutionState, TextScriptVM}; use crate::settings::Settings; use crate::sound::SoundManager; use crate::stage::StageData; @@ -158,7 +158,7 @@ pub struct SharedGameState { pub teleporter_slots: Vec<(u16, u16)>, pub carets: Vec, pub touch_controls: TouchControls, - pub base_path: String, + pub mod_path: Option, pub npc_table: NPCTable, pub npc_super_pos: (i32, i32), pub npc_curly_target: (i32, i32), @@ -192,7 +192,6 @@ impl SharedGameState { pub fn new(ctx: &mut Context) -> GameResult { let mut constants = EngineConstants::defaults(); let sound_manager = SoundManager::new(ctx)?; - let mut base_path = "/"; let settings = Settings::load(ctx)?; if filesystem::exists(ctx, "/base/lighting.tbl") { @@ -200,50 +199,33 @@ impl SharedGameState { ctx.size_hint = (854, 480); constants.apply_csplus_patches(&sound_manager); constants.apply_csplus_nx_patches(); - constants.load_csplus_tables(ctx)?; - constants.load_animated_faces(ctx)?; - base_path = "/base/"; } else if filesystem::exists(ctx, "/base/Nicalis.bmp") || filesystem::exists(ctx, "/base/Nicalis.png") { info!("Cave Story+ (PC) data files detected."); constants.apply_csplus_patches(&sound_manager); - constants.load_csplus_tables(ctx)?; - base_path = "/base/"; } else if filesystem::exists(ctx, "/mrmap.bin") { info!("CSE2E data files detected."); } else if filesystem::exists(ctx, "/stage.dat") { info!("NXEngine-evo data files detected."); } - let font = BMFontRenderer::load(base_path, &constants.font_path, ctx) - .or_else(|_| BMFontRenderer::load("/", "builtin/builtin_font.fnt", ctx))?; let season = Season::current(); - let mut texture_set = TextureSet::new(base_path); + constants.rebuild_path_list(None, season, &settings); - if constants.is_cs_plus { - texture_set.apply_seasonal_content(season, &settings); - } + let font = BMFontRenderer::load(&constants.base_paths, &constants.font_path, ctx) + .or_else(|e| { + log::warn!("Failed to load font, using built-in: {}", e); + BMFontRenderer::load(&vec!["/".to_owned()], "/builtin/builtin_font.fnt", ctx) + })?; for i in 0..0xffu8 { - let path = format!("{}/pxt/fx{:02x}.pxt", base_path, i); - if let Ok(file) = filesystem::open(ctx, path) { - sound_manager.set_sample_params_from_file(i, file)?; - continue; - } - let path = format!("/pxt/fx{:02x}.pxt", i); - if let Ok(file) = filesystem::open(ctx, path) { - sound_manager.set_sample_params_from_file(i, file)?; - continue; - } - - let path = format!("{}/PixTone/{:03}.pxt", base_path, i); - if let Ok(file) = filesystem::open(ctx, path) { + if let Ok(file) = filesystem::open_find(ctx, &constants.base_paths, path) { sound_manager.set_sample_params_from_file(i, file)?; continue; } let path = format!("/PixTone/{:03}.pxt", i); - if let Ok(file) = filesystem::open(ctx, path) { + if let Ok(file) = filesystem::open_find(ctx, &constants.base_paths, path) { sound_manager.set_sample_params_from_file(i, file)?; continue; } @@ -269,7 +251,7 @@ impl SharedGameState { teleporter_slots: Vec::with_capacity(8), carets: Vec::with_capacity(32), touch_controls: TouchControls::new(), - base_path: base_path.to_owned(), + mod_path: None, npc_table: NPCTable::new(), npc_super_pos: (0, 0), npc_curly_target: (0, 0), @@ -290,7 +272,7 @@ impl SharedGameState { menu_character: MenuCharacter::Quote, constants, font, - texture_set, + texture_set: TextureSet::new(), #[cfg(feature = "scripting-lua")] lua: LuaScriptingState::new(), sound_manager, @@ -324,18 +306,45 @@ impl SharedGameState { } } - pub fn reload_textures(&mut self) { - let mut texture_set = TextureSet::new(&self.base_path); + pub fn reload_resources(&mut self, ctx: &mut Context) -> GameResult { + self.constants.rebuild_path_list(self.mod_path.clone(), self.season, &self.settings); + self.constants.load_csplus_tables(ctx)?; + self.constants.load_animated_faces(ctx)?; + let stages = StageData::load_stage_table(ctx, &self.constants.base_paths)?; + self.stages = stages; - if self.constants.is_cs_plus { - texture_set.apply_seasonal_content(self.season, &self.settings); - } + let npc_tbl = filesystem::open_find(ctx, &self.constants.base_paths, "/npc.tbl")?; + let npc_table = NPCTable::load_from(npc_tbl)?; + self.npc_table = npc_table; - self.texture_set = texture_set; + let head_tsc = filesystem::open_find(ctx, &self.constants.base_paths, "/Head.tsc")?; + let head_script = TextScript::load_from(head_tsc, &self.constants)?; + self.textscript_vm.set_global_script(head_script); + + let arms_item_tsc = filesystem::open_find(ctx, &self.constants.base_paths, "/ArmsItem.tsc")?; + let arms_item_script = TextScript::load_from(arms_item_tsc, &self.constants)?; + self.textscript_vm.set_inventory_script(arms_item_script); + + let stage_select_tsc = filesystem::open_find(ctx, &self.constants.base_paths, "/StageSelect.tsc")?; + let stage_select_script = TextScript::load_from(stage_select_tsc, &self.constants)?; + self.textscript_vm.set_stage_select_script(stage_select_script); + + let credit_tsc = filesystem::open_find(ctx, &self.constants.base_paths, "/Credit.tsc")?; + let credit_script = CreditScript::load_from(credit_tsc, &self.constants)?; + self.creditscript_vm.set_script(credit_script); + + self.texture_set.unload_all(); + + Ok(()) + } + + pub fn reload_graphics(&mut self) { + self.constants.rebuild_path_list(self.mod_path.clone(), self.season, &self.settings); + self.texture_set.unload_all(); } pub fn graphics_reset(&mut self) { - self.reload_textures(); + self.texture_set.unload_all(); } pub fn start_new_game(&mut self, ctx: &mut Context) -> GameResult { diff --git a/src/stage.rs b/src/stage.rs index aa779a0..41e6757 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -14,6 +14,7 @@ use crate::framework::error::GameResult; use crate::framework::filesystem; use crate::map::{Map, NPCData}; use crate::scripting::tsc::text_script::TextScript; +use crate::GameError; #[derive(Debug, PartialEq, Eq, Hash)] pub struct NpcType { @@ -274,20 +275,20 @@ fn from_shift_jis(s: &[u8]) -> String { } impl StageData { - pub fn load_stage_table(ctx: &mut Context, root: &str) -> GameResult> { - let stage_tbl_path = [root, "stage.tbl"].join(""); - let stage_sect_path = [root, "stage.sect"].join(""); - let mrmap_bin_path = [root, "mrmap.bin"].join(""); - let stage_dat_path = [root, "stage.dat"].join(""); + pub fn load_stage_table(ctx: &mut Context, roots: &Vec) -> GameResult> { + let stage_tbl_path = "/stage.tbl"; + let stage_sect_path = "/stage.sect"; + let mrmap_bin_path = "/mrmap.bin"; + let stage_dat_path = "/stage.dat"; - if filesystem::exists(ctx, &stage_tbl_path) { + if let Ok(mut file) = filesystem::open_find(ctx, roots, stage_tbl_path) { // Cave Story+ stage table. let mut stages = Vec::new(); info!("Loading Cave Story+ stage table from {}", &stage_tbl_path); let mut data = Vec::new(); - filesystem::open(ctx, stage_tbl_path)?.read_to_end(&mut data)?; + file.read_to_end(&mut data)?; let count = data.len() / 0xe5; let mut f = Cursor::new(data); @@ -333,14 +334,14 @@ impl StageData { } return Ok(stages); - } else if filesystem::exists(ctx, &stage_sect_path) { + } else if let Ok(mut file) = filesystem::open_find(ctx, roots, stage_sect_path) { // Cave Story freeware executable dump. let mut stages = Vec::new(); info!("Loading Cave Story freeware exe dump stage table from {}", &stage_sect_path); let mut data = Vec::new(); - filesystem::open(ctx, stage_sect_path)?.read_to_end(&mut data)?; + file.read_to_end(&mut data)?; let count = data.len() / 0xc8; let mut f = Cursor::new(data); @@ -389,17 +390,16 @@ impl StageData { } return Ok(stages); - } else if filesystem::exists(ctx, &mrmap_bin_path) { + } else if let Ok(mut file) = filesystem::open_find(ctx, roots, mrmap_bin_path) { // Moustache Rider stage table let mut stages = Vec::new(); info!("Loading Moustache Rider stage table from {}", &mrmap_bin_path); let mut data = Vec::new(); - let mut fh = filesystem::open(ctx, &mrmap_bin_path)?; - let count = fh.read_u32::()?; - fh.read_to_end(&mut data)?; + let count = file.read_u32::()?; + file.read_to_end(&mut data)?; if data.len() < count as usize * 0x74 { return Err(ResourceLoadError( @@ -448,16 +448,15 @@ impl StageData { } return Ok(stages); - } else if filesystem::exists(ctx, &stage_dat_path) { + } else if let Ok(mut file) = filesystem::open_find(ctx, roots, stage_dat_path) { let mut stages = Vec::new(); info!("Loading NXEngine stage table from {}", &stage_dat_path); let mut data = Vec::new(); - let mut fh = filesystem::open(ctx, &stage_dat_path)?; - let count = fh.read_u8()? as usize; - fh.read_to_end(&mut data)?; + let count = file.read_u8()? as usize; + file.read_to_end(&mut data)?; if data.len() < count * 0x49 { return Err(ResourceLoadError( @@ -518,40 +517,41 @@ pub struct Stage { } impl Stage { - pub fn load(root: &str, data: &StageData, ctx: &mut Context) -> GameResult { + pub fn load(roots: &Vec, data: &StageData, ctx: &mut Context) -> GameResult { let mut data = data.clone(); - if let Ok(pxpack_file) = filesystem::open(ctx, [root, "Stage/", &data.map, ".pxpack"].join("")) { - let map = Map::load_pxpack(pxpack_file, root, &mut data, ctx)?; + if let Ok(pxpack_file) = filesystem::open_find(ctx, roots, ["/Stage/", &data.map, ".pxpack"].join("")) { + let map = Map::load_pxpack(pxpack_file, roots, &mut data, ctx)?; let stage = Self { map, data }; - Ok(stage) - } else { - let map_file = filesystem::open(ctx, [root, "Stage/", &data.map, ".pxm"].join(""))?; - let attrib_file = filesystem::open(ctx, [root, "Stage/", &data.tileset.name, ".pxa"].join(""))?; + return Ok(stage); + } else if let Ok(map_file) = filesystem::open_find(ctx, roots, ["/Stage/", &data.map, ".pxm"].join("")) { + let attrib_file = filesystem::open_find(ctx, roots, ["/Stage/", &data.tileset.name, ".pxa"].join(""))?; let map = Map::load_pxm(map_file, attrib_file)?; let stage = Self { map, data }; - Ok(stage) + return Ok(stage); } + + Err(GameError::ResourceLoadError(format!("Stage {} not found", data.map))) } pub fn load_text_script( &self, - root: &str, + roots: &Vec, constants: &EngineConstants, ctx: &mut Context, ) -> GameResult { - let tsc_file = filesystem::open(ctx, [root, "Stage/", &self.data.map, ".tsc"].join(""))?; + let tsc_file = filesystem::open_find(ctx, roots, ["/Stage/", &self.data.map, ".tsc"].join(""))?; let text_script = TextScript::load_from(tsc_file, constants)?; Ok(text_script) } - pub fn load_npcs(&self, root: &str, ctx: &mut Context) -> GameResult> { - let pxe_file = filesystem::open(ctx, [root, "Stage/", &self.data.map, ".pxe"].join(""))?; + pub fn load_npcs(&self, roots: &Vec, ctx: &mut Context) -> GameResult> { + let pxe_file = filesystem::open_find(ctx, roots, ["/Stage/", &self.data.map, ".pxe"].join(""))?; let npc_data = NPCData::load_from(pxe_file)?; Ok(npc_data) diff --git a/src/texture_set.rs b/src/texture_set.rs index 296bce5..8d4df3c 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use log::info; use crate::common; -use crate::common::{Rect, FILE_TYPES}; +use crate::common::{FILE_TYPES, Rect}; use crate::engine_constants::EngineConstants; use crate::framework::backend::{BackendTexture, SpriteBatchCommand}; use crate::framework::context::Context; @@ -409,29 +409,16 @@ impl SpriteBatch for CombinedBatch { pub struct TextureSet { pub tex_map: HashMap>, - pub paths: Vec, dummy_batch: Box, } impl TextureSet { - pub fn new(base_path: &str) -> TextureSet { - TextureSet { - tex_map: HashMap::new(), - paths: vec![base_path.to_string(), "".to_string()], - dummy_batch: Box::new(DummyBatch), - } + pub fn new() -> TextureSet { + TextureSet { tex_map: HashMap::new(), dummy_batch: Box::new(DummyBatch) } } - pub fn apply_seasonal_content(&mut self, season: Season, settings: &Settings) { - if settings.original_textures { - self.paths.insert(0, "/base/ogph/".to_string()) - } else if settings.seasonal_textures { - match season { - Season::Halloween => self.paths.insert(0, "/Halloween/season/".to_string()), - Season::Christmas => self.paths.insert(0, "/Christmas/season/".to_string()), - _ => {} - } - } + pub fn unload_all(&mut self) { + self.tex_map.clear(); } fn make_transparent(rgba: &mut RgbaImage) { @@ -442,10 +429,10 @@ impl TextureSet { } } - fn load_image(&self, ctx: &mut Context, path: &str) -> GameResult> { + fn load_image(&self, ctx: &mut Context, roots: &Vec, path: &str) -> GameResult> { let img = { let mut buf = [0u8; 8]; - let mut reader = filesystem::open(ctx, path)?; + let mut reader = filesystem::open_find(ctx, roots,path)?; reader.read_exact(&mut buf)?; reader.seek(SeekFrom::Start(0))?; @@ -461,10 +448,8 @@ impl TextureSet { create_texture(ctx, width as u16, height as u16, &img) } - pub fn find_texture(&self, ctx: &mut Context, name: &str) -> Option { - self.paths.iter().find_map(|s| { - FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| filesystem::exists(ctx, path)) - }) + pub fn find_texture(&self, ctx: &mut Context, roots: &Vec, name: &str) -> Option { + FILE_TYPES.iter().map(|ext| [name, ext].join("")).find(|path| filesystem::exists_find(ctx, roots, path)) } pub fn load_texture( @@ -474,10 +459,10 @@ impl TextureSet { name: &str, ) -> GameResult> { let path = self - .find_texture(ctx, name) + .find_texture(ctx, &constants.base_paths, name) .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {} does not exist.", name)))?; - let glow_path = self.find_texture(ctx, &[name, ".glow"].join("")); + let glow_path = self.find_texture(ctx, &constants.base_paths, &[name, ".glow"].join("")); info!("Loading texture: {} -> {}", name, path); @@ -500,9 +485,9 @@ impl TextureSet { } } - let main_batch = make_batch(name, constants, self.load_image(ctx, &path)?); + let main_batch = make_batch(name, constants, self.load_image(ctx, &constants.base_paths, &path)?); let glow_batch = if let Some(glow_path) = glow_path { - self.load_image(ctx, &glow_path).ok().map(|b| make_batch(name, constants, b)) + self.load_image(ctx, &constants.base_paths, &glow_path).ok().map(|b| make_batch(name, constants, b)) } else { None };