diff --git a/src/engine_constants.rs b/src/engine_constants.rs index 7e3ac67..d2d2674 100644 --- a/src/engine_constants.rs +++ b/src/engine_constants.rs @@ -200,6 +200,8 @@ pub struct NPCConsts { #[derive(Debug, Copy, Clone)] pub struct TextScriptConsts { pub encoding: TextScriptEncoding, + pub encrypted: bool, + pub animated_face_pics: bool, pub textbox_rect_top: Rect, pub textbox_rect_middle: Rect, pub textbox_rect_bottom: Rect, @@ -230,6 +232,7 @@ pub struct TitleConsts { #[derive(Debug)] pub struct EngineConstants { pub is_cs_plus: bool, + pub is_switch: bool, pub my_char: MyCharConsts, pub booster: BoosterConsts, pub caret: CaretConsts, @@ -249,6 +252,7 @@ impl Clone for EngineConstants { fn clone(&self) -> EngineConstants { EngineConstants { is_cs_plus: self.is_cs_plus, + is_switch: self.is_switch, my_char: self.my_char, booster: self.booster, caret: self.caret.clone(), @@ -270,6 +274,7 @@ impl EngineConstants { pub fn defaults() -> Self { EngineConstants { is_cs_plus: false, + is_switch: false, my_char: MyCharConsts { display_bounds: Rect { left: 8 * 0x200, top: 8 * 0x200, right: 8 * 0x200, bottom: 8 * 0x200 }, hit_bounds: Rect { left: 5 * 0x200, top: 8 * 0x200, right: 5 * 0x200, bottom: 8 * 0x200 }, @@ -1069,6 +1074,11 @@ impl EngineConstants { "Face_0" => (288, 240), // nxengine "Face_1" => (288, 240), // nxengine "Face_2" => (288, 240), // nxengine + "Face1" => (288, 240), // switch + "Face2" => (288, 240), // switch + "Face3" => (288, 240), // switch + "Face4" => (288, 240), // switch + "Face5" => (288, 240), // switch "Fade" => (256, 32), "ItemImage" => (256, 128), "Loading" => (64, 8), @@ -1154,7 +1164,9 @@ impl EngineConstants { "Title" => (320, 48), }, textscript: TextScriptConsts { - encoding: TextScriptEncoding::UTF8, + encoding: TextScriptEncoding::ShiftJIS, + encrypted: true, + animated_face_pics: false, textbox_rect_top: Rect { left: 0, top: 0, right: 244, bottom: 8 }, textbox_rect_middle: Rect { left: 0, top: 8, right: 244, bottom: 16 }, textbox_rect_bottom: Rect { left: 0, top: 16, right: 244, bottom: 24 }, @@ -1183,7 +1195,7 @@ impl EngineConstants { font_space_offset: -3.0, organya_paths: vec![ str!("/org/"), // NXEngine - str!("/base/Org/"), // CS+ + str!("/base/Org/"), // CS+ PC str!("/Resource/ORG/"), // CSE2E ], } @@ -1205,5 +1217,13 @@ impl EngineConstants { pub fn apply_csplus_nx_patches(&mut self) { info!("Applying Switch-specific Cave Story+ constants patches..."); + + self.is_switch = true; + self.tex_sizes.insert(str!("bkMoon"), (427, 240)); + self.tex_sizes.insert(str!("bkFog"), (427, 240)); + self.title.logo_rect = Rect { left: 0, top: 0, right: 214, bottom: 62 }; + self.textscript.encoding = TextScriptEncoding::UTF8; + self.textscript.encrypted = false; + self.textscript.animated_face_pics = true; } } diff --git a/src/ggez/context.rs b/src/ggez/context.rs index 4d32b46..f3ced62 100644 --- a/src/ggez/context.rs +++ b/src/ggez/context.rs @@ -1,4 +1,9 @@ +use std::borrow::Cow; use std::fmt; +use std::path; +#[cfg(debug_assertions)] +use std::sync::atomic::{AtomicUsize, Ordering}; + /// We re-export winit so it's easy for people to use the same version as we are /// without having to mess around figuring it out. pub use winit; @@ -7,7 +12,7 @@ use crate::ggez::conf; use crate::ggez::error::GameResult; use crate::ggez::event::winit_event; use crate::ggez::filesystem::Filesystem; -use crate::ggez::graphics::{self, Point2}; +use crate::ggez::graphics::{self, FilterMode, Point2}; use crate::ggez::input::{gamepad, keyboard, mouse}; use crate::ggez::timer; @@ -56,6 +61,8 @@ pub struct Context { /// Set this with `ggez::event::quit()`. pub continuing: bool, + pub filter_mode: FilterMode, + /// Context-specific unique ID. /// Compiles to nothing in release mode, and so /// vanishes; meanwhile we get dead-code warnings. @@ -102,6 +109,7 @@ impl Context { keyboard_context, gamepad_context, mouse_context, + filter_mode: FilterMode::Nearest, debug_id, }; @@ -143,12 +151,12 @@ impl Context { } winit_event::WindowEvent::KeyboardInput { input: - winit::KeyboardInput { - state, - virtual_keycode: Some(keycode), - modifiers, - .. - }, + winit::KeyboardInput { + state, + virtual_keycode: Some(keycode), + modifiers, + .. + }, .. } => { let pressed = match state { @@ -176,9 +184,6 @@ impl Context { } } -use std::borrow::Cow; -use std::path; - /// A builder object for creating a [`Context`](struct.Context.html). #[derive(Debug, Clone, PartialEq)] pub struct ContextBuilder { @@ -237,8 +242,8 @@ impl ContextBuilder { /// Add a new read-only filesystem path to the places to search /// for resources. pub fn add_resource_path(mut self, path: T) -> Self - where - T: Into, + where + T: Into, { self.paths.push(path.into()); self @@ -271,8 +276,6 @@ impl ContextBuilder { } } -#[cfg(debug_assertions)] -use std::sync::atomic::{AtomicUsize, Ordering}; #[cfg(debug_assertions)] static DEBUG_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -285,6 +288,7 @@ static DEBUG_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg(debug_assertions)] pub(crate) struct DebugId(u32); + #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg(not(debug_assertions))] pub(crate) struct DebugId; diff --git a/src/ggez/graphics/types.rs b/src/ggez/graphics/types.rs index bdc7747..b849123 100644 --- a/src/ggez/graphics/types.rs +++ b/src/ggez/graphics/types.rs @@ -472,7 +472,7 @@ impl DrawMode { } /// Specifies what blending method to use when scaling up/down images. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum FilterMode { /// Use linear interpolation (ie, smooth) Linear, diff --git a/src/inventory.rs b/src/inventory.rs index 4653f58..fc02c72 100644 --- a/src/inventory.rs +++ b/src/inventory.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use crate::engine_constants::EngineConstants; use crate::shared_game_state::SharedGameState; use crate::weapon::{Weapon, WeaponLevel, WeaponType}; @@ -41,6 +43,11 @@ impl Inventory { pub fn add_item(&mut self, item_id: u16) { if !self.has_item(item_id) { self.items.push(Item(item_id, 1)); + } else { + if let Some(item) = self.get_item(item_id) { + item.1 += 1; + return; + } } } @@ -67,6 +74,16 @@ impl Inventory { self.items.iter().any(|item| item.0 == item_id) } + pub fn has_item_amount(&self, item_id: u16, operator: Ordering, amount: u16) -> bool { + let result = self.items.iter().any(|item| item.0 == item_id && item.1.cmp(&amount) == operator); + + if (operator == Ordering::Equal && amount == 0 && !result) || (operator == Ordering::Less && !self.has_item(item_id)) { + return true; + } + + result + } + pub fn add_weapon(&mut self, weapon_id: WeaponType, max_ammo: u16) -> &mut Weapon { if !self.has_weapon(weapon_id) { self.weapons.push(Weapon::new( @@ -230,3 +247,34 @@ impl Inventory { self.weapons.iter().any(|weapon| weapon.wtype == wtype) } } + +#[test] +fn inventory_test() { + let mut inventory = Inventory::new(); + + inventory.add_item(3); + assert_eq!(inventory.has_item(2), false); + assert_eq!(inventory.has_item(3), true); + + assert_eq!(inventory.has_item_amount(3, Ordering::Equal, 1), true); + assert_eq!(inventory.has_item_amount(3, Ordering::Less, 2), true); + inventory.consume_item(3); + + assert_eq!(inventory.has_item_amount(3, Ordering::Equal, 0), true); + assert_eq!(inventory.has_item_amount(3, Ordering::Less, 2), true); + + inventory.add_item(2); + assert_eq!(inventory.has_item(2), true); + assert_eq!(inventory.has_item_amount(2, Ordering::Equal, 1), true); + assert_eq!(inventory.has_item_amount(2, Ordering::Less, 1), false); + + inventory.add_item(4); + inventory.add_item(4); + inventory.add_item(4); + inventory.add_item(4); + + assert_eq!(inventory.has_item(4), true); + assert_eq!(inventory.has_item_amount(4, Ordering::Greater, 3), true); + assert_eq!(inventory.has_item_amount(4, Ordering::Equal, 4), true); + assert_eq!(inventory.has_item_amount(4, Ordering::Less, 2), false); +} diff --git a/src/main.rs b/src/main.rs index 2939a35..d469765 100644 --- a/src/main.rs +++ b/src/main.rs @@ -178,12 +178,14 @@ impl Game { pub fn main() -> GameResult { pretty_env_logger::env_logger::init_from_env(Env::default().default_filter_or("info")); - let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + let resource_dir = if let Ok(data_dir) = env::var("CAVESTORY_DATA_DIR") { + path::PathBuf::from(data_dir) + } else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { let mut path = path::PathBuf::from(manifest_dir); path.push("data"); path } else { - path::PathBuf::from(&env::var("CAVESTORY_DATA_DIR").unwrap_or(str!("data"))) + path::PathBuf::from("data") }; info!("Resource directory: {:?}", resource_dir); diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index fcd46cd..b9f0f2e 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -50,6 +50,9 @@ pub enum Alignment { Right, } +static FACE_TEX: &str = "Face"; +static SWITCH_FACE_TEX: [&str; 4] = ["Face1", "Face2", "Face3", "Face4"]; + impl GameScene { pub fn new(state: &mut SharedGameState, ctx: &mut Context, id: usize) -> GameResult { info!("Loading stage {} ({})", id, &state.stages[id].map); @@ -454,7 +457,12 @@ impl GameScene { } if state.textscript_vm.face != 0 { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Face")?; + let tex_name = if state.constants.textscript.animated_face_pics { + SWITCH_FACE_TEX[0] + } else { + FACE_TEX + }; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?; batch.add_rect(left_pos + 14.0, top_pos + 8.0, &Rect::::new_size( (state.textscript_vm.face as usize % 6) * 48, @@ -668,7 +676,7 @@ impl GameScene { impl Scene for GameScene { fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - state.textscript_vm.set_scene_script(self.stage.load_text_script(&state.base_path, ctx)?); + state.textscript_vm.set_scene_script(self.stage.load_text_script(&state.base_path, &state.constants, ctx)?); state.textscript_vm.suspend = false; let npcs = self.stage.load_npcs(&state.base_path, ctx)?; diff --git a/src/scene/loading_scene.rs b/src/scene/loading_scene.rs index 2a11b54..e993877 100644 --- a/src/scene/loading_scene.rs +++ b/src/scene/loading_scene.rs @@ -24,9 +24,11 @@ impl Scene for LoadingScene { if self.tick == 1 { let stages = StageData::load_stage_table(ctx, &state.base_path)?; state.stages = stages; - let npc_table = NPCTable::load_from(filesystem::open(ctx, [&state.base_path, "/npc.tbl"].join(""))?)?; + 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_script = TextScript::load_from(filesystem::open(ctx, [&state.base_path, "/Head.tsc"].join(""))?)?; + 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); state.next_scene = Some(Box::new(TitleScene::new())); diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index bfba45f..8b8f9de 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -1,6 +1,6 @@ use crate::common::Rect; use crate::ggez::{Context, GameResult, graphics}; -use crate::ggez::graphics::Color; +use crate::ggez::graphics::{Color, FilterMode}; use crate::menu::{Menu, MenuEntry, MenuSelectionResult}; use crate::scene::Scene; use crate::shared_game_state::{SharedGameState, TimingMode}; @@ -91,6 +91,7 @@ impl Scene for TitleScene { self.main_menu.push_entry(MenuEntry::Active("Quit".to_string())); self.option_menu.push_entry(MenuEntry::Toggle("50 FPS timing".to_string(), state.timing_mode == TimingMode::_50Hz)); + self.option_menu.push_entry(MenuEntry::Toggle("Linear scaling".to_string(), ctx.filter_mode == FilterMode::Linear)); self.option_menu.push_entry(MenuEntry::Toggle("2x Speed hack".to_string(), state.speed_hack)); self.option_menu.push_entry(MenuEntry::Active("Join our Discord".to_string())); self.option_menu.push_entry(MenuEntry::Disabled(DISCORD_LINK.to_owned())); @@ -140,17 +141,27 @@ impl Scene for TitleScene { } } MenuSelectionResult::Selected(1, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + match ctx.filter_mode { + FilterMode::Linear => { ctx.filter_mode = FilterMode::Nearest } + FilterMode::Nearest => { ctx.filter_mode = FilterMode::Linear } + } + + *value = ctx.filter_mode == FilterMode::Linear; + } + } + MenuSelectionResult::Selected(2, toggle) => { if let MenuEntry::Toggle(_, value) = toggle { *value = !(*value); state.set_speed_hack(*value); } } - MenuSelectionResult::Selected(2, _) => { + MenuSelectionResult::Selected(3, _) => { if let Err(e) = webbrowser::open(DISCORD_LINK) { log::warn!("Error opening web browser: {}", e); } } - MenuSelectionResult::Selected(4, _) | MenuSelectionResult::Canceled => { + MenuSelectionResult::Selected(5, _) | MenuSelectionResult::Canceled => { self.current_menu = CurrentMenu::MainMenu; } _ => {} @@ -192,7 +203,9 @@ impl Scene for TitleScene { self.draw_text_centered(ENGINE_VERSION, state.canvas_size.1 - 15.0, state, ctx)?; - if state.constants.is_cs_plus { + if state.constants.is_switch { + self.draw_text_centered(COPYRIGHT_NICALIS_SWITCH, state.canvas_size.1 - 30.0, state, ctx)?; + } else if state.constants.is_cs_plus { self.draw_text_centered(COPYRIGHT_NICALIS, state.canvas_size.1 - 30.0, state, ctx)?; } else { self.draw_text_centered(COPYRIGHT_PIXEL, state.canvas_size.1 - 30.0, state, ctx)?; diff --git a/src/stage.rs b/src/stage.rs index 419f5d9..5b081de 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -5,11 +5,12 @@ use byteorder::LE; use byteorder::ReadBytesExt; use log::info; +use crate::encoding::read_cur_shift_jis; +use crate::engine_constants::EngineConstants; use crate::ggez::{Context, filesystem, GameResult}; use crate::ggez::GameError::ResourceLoadError; use crate::map::{Map, NPCData}; use crate::text_script::TextScript; -use crate::encoding::read_cur_shift_jis; #[derive(Debug, PartialEq, Eq, Hash)] pub struct NpcType { @@ -385,9 +386,9 @@ impl Stage { Ok(stage) } - pub fn load_text_script(&mut self, root: &str, ctx: &mut Context) -> GameResult { + pub fn load_text_script(&mut self, root: &str, constants: &EngineConstants, ctx: &mut Context) -> GameResult { let tsc_file = filesystem::open(ctx, [root, "Stage/", &self.data.map, ".tsc"].join(""))?; - let text_script = TextScript::load_from(tsc_file)?; + let text_script = TextScript::load_from(tsc_file, constants)?; Ok(text_script) } diff --git a/src/text_script.rs b/src/text_script.rs index 385b5d8..2dfb0f3 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -15,6 +15,7 @@ use num_traits::{clamp, FromPrimitive}; use crate::bitfield; use crate::common::{Direction, FadeDirection, FadeState}; use crate::encoding::{read_cur_shift_jis, read_cur_wtf8}; +use crate::engine_constants::EngineConstants; use crate::entity::GameEntity; use crate::ggez::{Context, GameResult}; use crate::ggez::GameError::ParseError; @@ -166,6 +167,18 @@ pub enum OpCode { /// { + OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::HM2 => { log::warn!("unimplemented opcode: {:?}", op); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); @@ -1076,7 +1089,7 @@ impl TextScriptVM { OpCode::BOA | OpCode::BSL | OpCode::FOB | OpCode::NUM | OpCode::DNA | OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL | - OpCode::SSS | OpCode::ACH => { + OpCode::SSS | OpCode::ACH | OpCode::S2MV => { let par_a = read_cur_varint(&mut cursor)?; log::warn!("unimplemented opcode: {:?} {}", op, par_a); @@ -1093,7 +1106,7 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // Three operand codes - OpCode::TAM => { + OpCode::TAM | OpCode::INJ => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; let par_c = read_cur_varint(&mut cursor)?; @@ -1157,26 +1170,29 @@ impl TextScript { } /// Loads, decrypts and compiles a text script from specified stream. - pub fn load_from(mut data: R) -> GameResult { + pub fn load_from(mut data: R, constants: &EngineConstants) -> GameResult { let mut buf = Vec::new(); data.read_to_end(&mut buf)?; - let half = buf.len() / 2; - let key = if let Some(0) = buf.get(half) { - 0xf9 - } else { - (-(*buf.get(half).unwrap() as isize)) as u8 - }; + if constants.textscript.encrypted { + let half = buf.len() / 2; + let key = if let Some(0) = buf.get(half) { + 0xf9 + } else { + (-(*buf.get(half).unwrap() as isize)) as u8 + }; + log::info!("Decrypting TSC using key {:#x}", key); - for (idx, byte) in buf.iter_mut().enumerate() { - if idx == half { - continue; + for (idx, byte) in buf.iter_mut().enumerate() { + if idx == half { + continue; + } + + *byte = byte.wrapping_add(key); } - - *byte = byte.wrapping_add(key); } - TextScript::compile(&buf, false) + TextScript::compile(&buf, false, constants.textscript.encoding) } pub fn get_event_ids(&self) -> Vec { @@ -1184,7 +1200,7 @@ impl TextScript { } /// Compiles a decrypted text script data into internal bytecode. - pub fn compile(data: &[u8], strict: bool) -> GameResult { + pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult { log::info!("data: {}", String::from_utf8_lossy(data)); let mut event_map = HashMap::new(); @@ -1210,7 +1226,7 @@ impl TextScript { } } - let bytecode = TextScript::compile_event(&mut iter, strict, TextScriptEncoding::ShiftJIS)?; + let bytecode = TextScript::compile_event(&mut iter, strict, encoding)?; log::info!("Successfully compiled event #{} ({} bytes generated).", event_num, bytecode.len()); event_map.insert(event_num, bytecode); } @@ -1281,11 +1297,11 @@ impl TextScript { let mut chars = 0; while remaining > 0 { - let (consumed, chr) = if encoding == TextScriptEncoding::UTF8 { - read_cur_wtf8(&mut cursor, remaining) - } else { - read_cur_shift_jis(&mut cursor, remaining) + let (consumed, chr) = match encoding { + TextScriptEncoding::UTF8 => read_cur_wtf8(&mut cursor, remaining), + TextScriptEncoding::ShiftJIS => read_cur_shift_jis(&mut cursor, remaining), }; + remaining -= consumed; chars += 1; @@ -1339,7 +1355,7 @@ impl TextScript { OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP | OpCode::MM0 | OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | OpCode::NOD | OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS | - OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM => { + OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 => { TextScript::put_varint(instr as i32, out); } // One operand codes @@ -1349,7 +1365,7 @@ impl TextScript { OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp | OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ | OpCode::EVE | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU | - OpCode::SSS | OpCode::ACH => { + OpCode::SSS | OpCode::ACH | OpCode::S2MV => { let operand = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); TextScript::put_varint(operand as i32, out); @@ -1366,7 +1382,7 @@ impl TextScript { TextScript::put_varint(operand_b as i32, out); } // Three operand codes - OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP => { + OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => { let operand_a = TextScript::read_number(iter)?; if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } let operand_b = TextScript::read_number(iter)?; diff --git a/src/texture_set.rs b/src/texture_set.rs index 470f701..a46fe55 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -96,7 +96,7 @@ impl SizedBatch { } pub fn draw(&mut self, ctx: &mut Context) -> GameResult { - self.batch.set_filter(FilterMode::Nearest); + self.batch.set_filter(ctx.filter_mode); self.batch.draw(ctx, DrawParam::new())?; self.batch.clear(); Ok(())