initial switch data support

This commit is contained in:
Alula 2020-09-25 19:14:52 +02:00
parent 2b148fe3ed
commit 449a503fc5
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
11 changed files with 170 additions and 56 deletions

View File

@ -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<usize>,
pub textbox_rect_middle: Rect<usize>,
pub textbox_rect_bottom: Rect<usize>,
@ -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;
}
}

View File

@ -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<T>(mut self, path: T) -> Self
where
T: Into<path::PathBuf>,
where
T: Into<path::PathBuf>,
{
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;

View File

@ -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,

View File

@ -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);
}

View File

@ -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);

View File

@ -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<Self> {
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::<usize>::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)?;

View File

@ -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()));

View File

@ -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)?;

View File

@ -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<TextScript> {
pub fn load_text_script(&mut self, root: &str, constants: &EngineConstants, ctx: &mut Context) -> GameResult<TextScript> {
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)
}

View File

@ -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 {
/// <ACHXXXX, triggers a Steam achievement.
ACH,
// ---- Cave Story+ (Switch) specific opcodes ----
/// <HM2, in "you've never been seen again" script, name and context of other opcodes suggests it might be second player related
/// HMC for player 2 i think?
HM2,
/// <2MV:xxxx, context suggests it's probably MOV for player 2 but what's the operand for?
#[strum(serialize = "2MV")]
S2MV,
/// <INJ:xxxx:yyyy:zzzz, xxxx = item id, yyyy = ???, zzzz = event id, a variant of <ITJ
/// seems like a ITJ which jumps if there's a specific number of items but i'm not sure
INJ,
// ---- Custom opcodes, for use by modders ----
}
@ -1067,7 +1080,7 @@ impl TextScriptVM {
OpCode::CAT | OpCode::CIL | OpCode::CPS |
OpCode::CRE | OpCode::CSS | OpCode::FLA | OpCode::MLP |
OpCode::SAT | OpCode::SLP | OpCode::SPS |
OpCode::STC | OpCode::SVP | OpCode::TUR => {
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<R: io::Read>(mut data: R) -> GameResult<TextScript> {
pub fn load_from<R: io::Read>(mut data: R, constants: &EngineConstants) -> GameResult<TextScript> {
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<u16> {
@ -1184,7 +1200,7 @@ impl TextScript {
}
/// Compiles a decrypted text script data into internal bytecode.
pub fn compile(data: &[u8], strict: bool) -> GameResult<TextScript> {
pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult<TextScript> {
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)?;

View File

@ -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(())