1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2024-09-28 21:19:24 +00:00

tsc refactor/optimization, credits interpreter

This commit is contained in:
Alula 2021-10-16 04:37:42 +02:00
parent 66106c7e82
commit df1fbbf0d1
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
14 changed files with 1701 additions and 1215 deletions

49
src/components/credits.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
pub struct Credits {}
impl Credits {
pub fn new() -> Credits {
Credits {}
}
}
impl GameEntity<()> for Credits {
fn tick(&mut self, state: &mut SharedGameState, custom: ()) -> GameResult {
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
if state.creditscript_vm.lines.is_empty() {
return Ok(());
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Casts")?;
for line in state.creditscript_vm.lines.iter() {
let x = (line.cast_id % 13) * 24;
let y = ((line.cast_id / 13) & 0xff) * 24;
let rect = Rect::new_size(x, y, 24, 24);
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
}
batch.draw(ctx)?;
for line in state.creditscript_vm.lines.iter() {
state.font.draw_text_with_shadow(
line.text.chars(),
line.pos_x,
line.pos_y,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
Ok(())
}
}

View file

@ -1,4 +1,5 @@
pub mod boss_life_bar;
pub mod credits;
pub mod draw_common;
pub mod flash;
pub mod hud;

View file

@ -64,7 +64,7 @@ impl LiveDebugger {
.resizable(false)
.collapsed(true, Condition::FirstUseEver)
.position([5.0, 5.0], Condition::FirstUseEver)
.size([400.0, 170.0], Condition::FirstUseEver)
.size([400.0, 190.0], Condition::FirstUseEver)
.build(ui, || {
ui.text(format!(
"Player position: ({:.1},{:.1}), velocity: ({:.1},{:.1})",
@ -112,7 +112,7 @@ impl LiveDebugger {
}
ui.same_line(0.0);
if ui.button(im_str!("Events"), [0.0, 0.0]) {
if ui.button(im_str!("TSC Scripts"), [0.0, 0.0]) {
self.events_visible = !self.events_visible;
}
@ -124,7 +124,7 @@ impl LiveDebugger {
#[cfg(feature = "scripting-lua")]
{
ui.same_line(0.0);
if ui.button(im_str!("Reload Scripts"), [0.0, 0.0]) {
if ui.button(im_str!("Reload Lua Scripts"), [0.0, 0.0]) {
if let Err(err) = state.lua.reload_scripts(ctx) {
log::error!("Error reloading scripts: {:?}", err);
self.error = Some(ImString::new(err.to_string()));
@ -204,39 +204,40 @@ impl LiveDebugger {
}
if self.events_visible {
Window::new(im_str!("Events"))
Window::new(im_str!("TSC Scripts"))
.resizable(false)
.position([80.0, 80.0], Condition::Appearing)
.size([300.0, 300.0], Condition::Appearing)
.size([300.0, 320.0], Condition::Appearing)
.build(ui, || {
if self.events.is_empty() {
self.event_ids.clear();
let vm = &state.textscript_vm;
let scripts = state.textscript_vm.scripts.borrow();
for event in vm.scripts.scene_script.get_event_ids() {
for event in scripts.scene_script.get_event_ids() {
self.events.push(ImString::new(format!("Scene: #{:04}", event)));
self.event_ids.push((ScriptType::Scene, event));
}
for event in vm.scripts.global_script.get_event_ids() {
for event in scripts.global_script.get_event_ids() {
self.events.push(ImString::new(format!("Global: #{:04}", event)));
self.event_ids.push((ScriptType::Global, event));
}
for event in vm.scripts.inventory_script.get_event_ids() {
for event in scripts.inventory_script.get_event_ids() {
self.events.push(ImString::new(format!("Inventory: #{:04}", event)));
self.event_ids.push((ScriptType::Inventory, event));
}
for event in vm.scripts.stage_select_script.get_event_ids() {
for event in scripts.stage_select_script.get_event_ids() {
self.events.push(ImString::new(format!("Stage Select: #{:04}", event)));
self.event_ids.push((ScriptType::StageSelect, event));
}
}
let events: Vec<&ImStr> = self.events.iter().map(|e| e.as_ref()).collect();
ui.text_wrapped(&ImString::new(format!("Execution state: {:?}", state.textscript_vm.state)));
ui.text_wrapped(&ImString::new(format!("TextScript execution state: {:?}", state.textscript_vm.state)));
ui.text_wrapped(&ImString::new(format!("CreditScript execution state: {:?}", state.creditscript_vm.state)));
ui.push_item_width(-1.0);
ui.list_box(im_str!(""), &mut self.selected_event, &events, 10);
@ -256,11 +257,12 @@ impl LiveDebugger {
if let Some((stype, event_num)) = self.event_ids.get(self.selected_event as usize) {
let id = ((*stype as u32) << 16) | (*event_num as u32);
if !self.text_windows.iter().any(|(e, _, _)| *e == id) {
let scripts = state.textscript_vm.scripts.borrow();
let script = match stype {
ScriptType::Scene => &state.textscript_vm.scripts.scene_script,
ScriptType::Global => &state.textscript_vm.scripts.global_script,
ScriptType::Inventory => &state.textscript_vm.scripts.inventory_script,
ScriptType::StageSelect => &state.textscript_vm.scripts.stage_select_script,
ScriptType::Scene => &scripts.scene_script,
ScriptType::Global => &scripts.global_script,
ScriptType::Inventory => &scripts.inventory_script,
ScriptType::StageSelect => &scripts.stage_select_script,
};
match script.decompile_event(*event_num) {

View file

@ -5,6 +5,7 @@ use log::info;
use crate::caret::CaretType;
use crate::common::{interpolate_fix9_scale, Color, Direction, FadeDirection, FadeState, Rect};
use crate::components::boss_life_bar::BossLifeBar;
use crate::components::credits::Credits;
use crate::components::draw_common::Alignment;
use crate::components::flash::Flash;
use crate::components::hud::HUD;
@ -30,6 +31,7 @@ use crate::player::{Player, TargetPlayer};
use crate::rng::XorShift;
use crate::scene::title_scene::TitleScene;
use crate::scene::Scene;
use crate::scripting::tsc::credit_script::CreditScriptVM;
use crate::shared_game_state::{SharedGameState, TileSize};
use crate::stage::{BackgroundType, Stage};
use crate::scripting::tsc::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM};
@ -45,6 +47,7 @@ pub struct GameScene {
pub boss_life_bar: BossLifeBar,
pub stage_select: StageSelect,
pub flash: Flash,
pub credits: Credits,
pub inventory_ui: InventoryUI,
pub hud_player1: HUD,
pub hud_player2: HUD,
@ -132,6 +135,7 @@ impl GameScene {
boss_life_bar: BossLifeBar::new(),
stage_select: StageSelect::new(),
flash: Flash::new(),
credits: Credits::new(),
inventory_ui: InventoryUI::new(),
hud_player1: HUD::new(Alignment::Left),
hud_player2: HUD::new(Alignment::Right),
@ -1930,6 +1934,11 @@ impl Scene for GameScene {
}
}
if state.control_flags.credits_running() {
self.skip_counter = 0;
CreditScriptVM::run(state, ctx)?;
}
match state.fade_state {
FadeState::FadeOut(tick, direction) if tick < 15 => {
state.fade_state = FadeState::FadeOut(tick + 1, direction);
@ -2134,7 +2143,12 @@ impl Scene for GameScene {
)?;
}
if state.control_flags.credits_running() {
self.credits.draw(state, ctx, &self.frame)?;
}
self.draw_text_boxes(state, ctx)?;
if self.skip_counter > 0 {
let text = format!("Hold {:?} to skip the cutscene", state.settings.player1_key_map.inventory);
let width = state.font.text_width(text.chars(), &state.constants);

View file

@ -4,6 +4,7 @@ use crate::framework::filesystem;
use crate::npc::NPCTable;
use crate::scene::no_data_scene::NoDataScene;
use crate::scene::Scene;
use crate::scripting::tsc::credit_script::CreditScript;
use crate::shared_game_state::SharedGameState;
use crate::stage::StageData;
use crate::scripting::tsc::text_script::TextScript;
@ -37,6 +38,10 @@ impl LoadingScene {
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);
if ctx.headless {
log::info!("Headless mode detected, skipping intro and loading last saved game.");
state.load_or_start_game(ctx)?;

View file

@ -3,7 +3,6 @@ use std::io::{Cursor, Read};
use crate::encoding::{read_cur_shift_jis, read_cur_wtf8};
use crate::framework::error::GameError::ParseError;
use crate::framework::error::GameResult;
use crate::scripting::tsc::opcodes::OpCode;
use crate::scripting::tsc::text_script::TextScriptEncoding;
pub fn put_varint(val: i32, out: &mut Vec<u8>) {
@ -83,7 +82,6 @@ pub fn put_string(buffer: &mut Vec<u8>, out: &mut Vec<u8>, encoding: TextScriptE
buffer.clear();
put_varint(OpCode::_STR as i32, out);
put_varint(chars, out);
out.append(&mut tmp_buf);
}

View file

@ -4,11 +4,11 @@ use std::str::FromStr;
use itertools::Itertools;
use crate::encoding::read_cur_wtf8;
use crate::framework::error::GameError::ParseError;
use crate::framework::error::GameResult;
use crate::scripting::tsc::bytecode_utils::{put_string, put_varint};
use crate::scripting::tsc::opcodes::OpCode;
use crate::scripting::tsc::credit_script::CreditScript;
use crate::scripting::tsc::opcodes::{CreditOpCode, TSCOpCode};
use crate::scripting::tsc::parse_utils::{expect_char, read_number, skip_until};
use crate::scripting::tsc::text_script::{TextScript, TextScriptEncoding};
@ -80,11 +80,12 @@ impl TextScript {
match chr {
b'#' if allow_next_event => {
if !char_buf.is_empty() {
put_varint(TSCOpCode::_STR as i32, &mut bytecode);
put_string(&mut char_buf, &mut bytecode, encoding);
}
// some events end without <END marker.
put_varint(OpCode::_END as i32, &mut bytecode);
put_varint(TSCOpCode::_END as i32, &mut bytecode);
break;
}
b'<' => {
@ -98,6 +99,7 @@ impl TextScript {
}
if !char_buf.is_empty() {
put_varint(TSCOpCode::_STR as i32, &mut bytecode);
put_string(&mut char_buf, &mut bytecode, encoding);
}
@ -138,111 +140,111 @@ impl TextScript {
iter: &mut Peekable<I>,
out: &mut Vec<u8>,
) -> GameResult {
let instr = OpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?;
let instr = TSCOpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?;
match instr {
// Zero operand codes
OpCode::AEp
| OpCode::CAT
| OpCode::CIL
| OpCode::CLO
| OpCode::CLR
| OpCode::CPS
| OpCode::CRE
| OpCode::CSS
| OpCode::END
| OpCode::ESC
| OpCode::FLA
| OpCode::FMU
| 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::HM2
| OpCode::POP
| OpCode::KE2
| OpCode::FR2 => {
TSCOpCode::AEp
| TSCOpCode::CAT
| TSCOpCode::CIL
| TSCOpCode::CLO
| TSCOpCode::CLR
| TSCOpCode::CPS
| TSCOpCode::CRE
| TSCOpCode::CSS
| TSCOpCode::END
| TSCOpCode::ESC
| TSCOpCode::FLA
| TSCOpCode::FMU
| TSCOpCode::FRE
| TSCOpCode::HMC
| TSCOpCode::INI
| TSCOpCode::KEY
| TSCOpCode::LDP
| TSCOpCode::MLP
| TSCOpCode::MM0
| TSCOpCode::MNA
| TSCOpCode::MS2
| TSCOpCode::MS3
| TSCOpCode::MSG
| TSCOpCode::NOD
| TSCOpCode::PRI
| TSCOpCode::RMU
| TSCOpCode::SAT
| TSCOpCode::SLP
| TSCOpCode::SMC
| TSCOpCode::SPS
| TSCOpCode::STC
| TSCOpCode::SVP
| TSCOpCode::TUR
| TSCOpCode::WAS
| TSCOpCode::ZAM
| TSCOpCode::HM2
| TSCOpCode::POP
| TSCOpCode::KE2
| TSCOpCode::FR2 => {
put_varint(instr as i32, out);
}
// One operand codes
OpCode::BOA
| OpCode::BSL
| OpCode::FOB
| OpCode::FOM
| OpCode::QUA
| OpCode::UNI
| OpCode::MYB
| OpCode::MYD
| OpCode::FAI
| OpCode::FAO
| OpCode::WAI
| OpCode::FAC
| OpCode::GIT
| OpCode::NUM
| OpCode::DNA
| OpCode::DNP
| OpCode::FLm
| OpCode::FLp
| 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::S2MV
| OpCode::S2PJ
| OpCode::PSH => {
TSCOpCode::BOA
| TSCOpCode::BSL
| TSCOpCode::FOB
| TSCOpCode::FOM
| TSCOpCode::QUA
| TSCOpCode::UNI
| TSCOpCode::MYB
| TSCOpCode::MYD
| TSCOpCode::FAI
| TSCOpCode::FAO
| TSCOpCode::WAI
| TSCOpCode::FAC
| TSCOpCode::GIT
| TSCOpCode::NUM
| TSCOpCode::DNA
| TSCOpCode::DNP
| TSCOpCode::FLm
| TSCOpCode::FLp
| TSCOpCode::MPp
| TSCOpCode::SKm
| TSCOpCode::SKp
| TSCOpCode::EQp
| TSCOpCode::EQm
| TSCOpCode::MLp
| TSCOpCode::ITp
| TSCOpCode::ITm
| TSCOpCode::AMm
| TSCOpCode::UNJ
| TSCOpCode::MPJ
| TSCOpCode::YNJ
| TSCOpCode::EVE
| TSCOpCode::XX1
| TSCOpCode::SIL
| TSCOpCode::LIp
| TSCOpCode::SOU
| TSCOpCode::CMU
| TSCOpCode::SSS
| TSCOpCode::ACH
| TSCOpCode::S2MV
| TSCOpCode::S2PJ
| TSCOpCode::PSH => {
let operand = read_number(iter)?;
put_varint(instr as i32, out);
put_varint(operand as i32, out);
}
// Two operand codes
OpCode::FON
| OpCode::MOV
| OpCode::AMp
| OpCode::NCJ
| OpCode::ECJ
| OpCode::FLJ
| OpCode::ITJ
| OpCode::SKJ
| OpCode::AMJ
| OpCode::SMP
| OpCode::PSp
| OpCode::IpN
| OpCode::FFm => {
TSCOpCode::FON
| TSCOpCode::MOV
| TSCOpCode::AMp
| TSCOpCode::NCJ
| TSCOpCode::ECJ
| TSCOpCode::FLJ
| TSCOpCode::ITJ
| TSCOpCode::SKJ
| TSCOpCode::AMJ
| TSCOpCode::SMP
| TSCOpCode::PSp
| TSCOpCode::IpN
| TSCOpCode::FFm => {
let operand_a = read_number(iter)?;
if strict {
expect_char(b':', iter)?;
@ -256,7 +258,7 @@ impl TextScript {
put_varint(operand_b as i32, out);
}
// Three operand codes
OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => {
TSCOpCode::ANP | TSCOpCode::CNP | TSCOpCode::INP | TSCOpCode::TAM | TSCOpCode::CMP | TSCOpCode::INJ => {
let operand_a = read_number(iter)?;
if strict {
expect_char(b':', iter)?;
@ -277,7 +279,7 @@ impl TextScript {
put_varint(operand_c as i32, out);
}
// Four operand codes
OpCode::TRA | OpCode::MNP | OpCode::SNP => {
TSCOpCode::TRA | TSCOpCode::MNP | TSCOpCode::SNP => {
let operand_a = read_number(iter)?;
if strict {
expect_char(b':', iter)?;
@ -304,7 +306,7 @@ impl TextScript {
put_varint(operand_c as i32, out);
put_varint(operand_d as i32, out);
}
OpCode::_NOP | OpCode::_UNI | OpCode::_STR | OpCode::_END => {
TSCOpCode::_NOP | TSCOpCode::_UNI | TSCOpCode::_STR | TSCOpCode::_END => {
unreachable!()
}
}
@ -312,3 +314,102 @@ impl TextScript {
Ok(())
}
}
impl CreditScript {
pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult<CreditScript> {
println!("data: {}", String::from_utf8_lossy(data));
let mut labels = HashMap::new();
let mut bytecode = Vec::new();
let mut iter = data.iter().copied().peekable();
while let Some(chr) = iter.next() {
match chr {
b'/' => {
put_varint(CreditOpCode::StopCredits as i32, &mut bytecode);
}
b'[' => {
let mut char_buf = Vec::new();
while let Some(&chr) = iter.peek() {
if chr == b']' {
iter.next();
break;
}
char_buf.push(chr);
iter.next();
}
if let Ok(cast_tile) = read_number(&mut iter) {
put_varint(CreditOpCode::PushLine as i32, &mut bytecode);
put_varint((cast_tile as u16) as i32, &mut bytecode);
put_string(&mut char_buf, &mut bytecode, encoding);
}
}
b'-' => {
let ticks = read_number(&mut iter)? as u16;
put_varint(CreditOpCode::Wait as i32, &mut bytecode);
put_varint(ticks as i32, &mut bytecode);
}
b'+' => {
let offset = read_number(&mut iter)?;
put_varint(CreditOpCode::ChangeXOffset as i32, &mut bytecode);
put_varint(offset, &mut bytecode);
}
b'!' => {
let music = read_number(&mut iter)? as u16;
put_varint(CreditOpCode::ChangeMusic as i32, &mut bytecode);
put_varint(music as i32, &mut bytecode);
}
b'~' => {
put_varint(CreditOpCode::FadeMusic as i32, &mut bytecode);
}
b'l' => {
let label = read_number(&mut iter)? as u16;
let pos = bytecode.len() as u32;
labels.insert(label, pos);
}
b'j' => {
let label = read_number(&mut iter)? as u16;
put_varint(CreditOpCode::JumpLabel as i32, &mut bytecode);
put_varint(label as i32, &mut bytecode);
}
b'f' => {
let flag = read_number(&mut iter)? as u16;
if strict {
expect_char(b':', &mut iter)?;
} else {
iter.next().ok_or_else(|| ParseError("Script unexpectedly ended.".to_owned()))?;
}
let label = read_number(&mut iter)? as u16;
put_varint(CreditOpCode::JumpFlag as i32, &mut bytecode);
put_varint(flag as i32, &mut bytecode);
put_varint(label as i32, &mut bytecode);
}
b'p' => {
iter.next(); // idfk what's that for, in cs+ Credits.tsc it's '2'.
if strict {
expect_char(b':', &mut iter)?;
} else {
iter.next().ok_or_else(|| ParseError("Script unexpectedly ended.".to_owned()))?;
}
let label = read_number(&mut iter)? as u16;
put_varint(CreditOpCode::JumpPlayer2 as i32, &mut bytecode);
put_varint(label as i32, &mut bytecode);
}
_ => (),
}
}
Ok(CreditScript { labels, bytecode })
}
}

View file

@ -1,3 +1,216 @@
pub struct CreditScript {
use std::collections::HashMap;
use std::io;
use std::io::{Cursor, Seek, SeekFrom};
use num_traits::FromPrimitive;
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::scripting::tsc::bytecode_utils::{put_varint, read_cur_varint};
use crate::scripting::tsc::encryption::decrypt_tsc;
use crate::scripting::tsc::opcodes::CreditOpCode;
use crate::shared_game_state::SharedGameState;
pub struct CreditScript {
pub(in crate::scripting::tsc) labels: HashMap<u16, u32>,
pub(in crate::scripting::tsc) bytecode: Vec<u8>,
}
impl Default for CreditScript {
fn default() -> Self {
let mut bytecode = Vec::new();
put_varint(CreditOpCode::StopCredits as i32, &mut bytecode);
CreditScript { labels: HashMap::new(), bytecode }
}
}
impl CreditScript {
/// Loads, decrypts and compiles a credit script from specified stream.
pub fn load_from<R: io::Read>(mut data: R, constants: &EngineConstants) -> GameResult<CreditScript> {
let mut buf = Vec::new();
data.read_to_end(&mut buf)?;
if constants.textscript.encrypted {
decrypt_tsc(&mut buf);
}
CreditScript::compile(&buf, false, constants.textscript.encoding)
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum CreditScriptExecutionState {
Ended,
Running(u32),
WaitTicks(u32, u16),
}
pub struct CreditScriptLine {
pub pos_x: f32,
pub pos_y: f32,
pub cast_id: u16,
pub text: String,
}
pub struct CreditScriptVM {
pub state: CreditScriptExecutionState,
pub lines: Vec<CreditScriptLine>,
pub text_offset: f32,
script: CreditScript,
}
impl CreditScriptVM {
pub fn new() -> CreditScriptVM {
CreditScriptVM {
state: CreditScriptExecutionState::Ended,
lines: Vec::new(),
text_offset: 0.0,
script: CreditScript::default(),
}
}
pub fn set_script(&mut self, script: CreditScript) {
self.reset();
self.script = script;
}
pub fn start(&mut self) {
self.reset();
self.state = CreditScriptExecutionState::Running(0);
}
pub fn reset(&mut self) {
self.lines.clear();
self.text_offset = 0.0;
self.state = CreditScriptExecutionState::Ended;
}
pub fn run(state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
if state.creditscript_vm.state != CreditScriptExecutionState::Ended {
for line in state.creditscript_vm.lines.iter_mut() {
line.pos_y -= 0.5;
}
}
state.creditscript_vm.lines.retain(|l| l.pos_y > -16.0);
loop {
match state.creditscript_vm.state {
CreditScriptExecutionState::Ended => {
break;
}
CreditScriptExecutionState::Running(ip) => {
let mut cursor = Cursor::new(&state.creditscript_vm.script.bytecode);
cursor.seek(SeekFrom::Start(ip as u64))?;
let op: CreditOpCode = if let Some(op) = FromPrimitive::from_i32(
read_cur_varint(&mut cursor).unwrap_or_else(|_| CreditOpCode::StopCredits as i32),
) {
op
} else {
state.creditscript_vm.reset();
return Ok(());
};
match op {
CreditOpCode::_NOP => {
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::StopCredits => {
state.creditscript_vm.state = CreditScriptExecutionState::Ended;
}
CreditOpCode::PushLine => {
let cast_id = read_cur_varint(&mut cursor)? as u16;
let text_len = read_cur_varint(&mut cursor)?;
let mut text = String::new();
text.reserve(text_len as usize);
for _ in 0..text_len {
let chr =
std::char::from_u32(read_cur_varint(&mut cursor)? as u32).unwrap_or('\u{fffd}');
text.push(chr);
}
let line = CreditScriptLine {
pos_x: state.creditscript_vm.text_offset,
pos_y: 256.0,
cast_id,
text,
};
state.creditscript_vm.lines.push(line);
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::Wait => {
let ticks = read_cur_varint(&mut cursor)? as u16;
state.creditscript_vm.state =
CreditScriptExecutionState::WaitTicks(cursor.position() as u32, ticks);
}
CreditOpCode::ChangeXOffset => {
let offset = read_cur_varint(&mut cursor)?;
state.creditscript_vm.text_offset = offset as f32;
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::ChangeMusic => {
let song = read_cur_varint(&mut cursor)? as u16;
state.sound_manager.play_song(song as usize, &state.constants, &state.settings, ctx)?;
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::FadeMusic => {
// todo
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::JumpLabel => {
let label = read_cur_varint(&mut cursor)? as u16;
if let Some(target) = state.creditscript_vm.script.labels.get(&label) {
state.creditscript_vm.state = CreditScriptExecutionState::Running(*target);
continue;
}
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::JumpFlag => {
let flag = read_cur_varint(&mut cursor)? as u16;
let label = read_cur_varint(&mut cursor)? as u16;
if state.get_flag(flag as usize) {
if let Some(target) = state.creditscript_vm.script.labels.get(&label) {
state.creditscript_vm.state = CreditScriptExecutionState::Running(*target);
continue;
}
}
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
CreditOpCode::JumpPlayer2 => {
let _label = read_cur_varint(&mut cursor)? as u16;
// todo
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
}
}
}
CreditScriptExecutionState::WaitTicks(ip, ticks) => {
if ticks == 0 {
state.creditscript_vm.state = CreditScriptExecutionState::Running(ip);
} else if ticks != 9999 {
state.creditscript_vm.state = CreditScriptExecutionState::WaitTicks(ip, ticks - 1);
break;
} else {
break;
}
}
}
}
Ok(())
}
}

View file

@ -5,7 +5,7 @@ use num_traits::FromPrimitive;
use crate::framework::error::GameError::InvalidValue;
use crate::framework::error::GameResult;
use crate::scripting::tsc::bytecode_utils::read_cur_varint;
use crate::scripting::tsc::opcodes::OpCode;
use crate::scripting::tsc::opcodes::TSCOpCode;
use crate::scripting::tsc::text_script::TextScript;
impl TextScript {
@ -15,119 +15,119 @@ impl TextScript {
let mut cursor = Cursor::new(bytecode);
while let Ok(op_num) = read_cur_varint(&mut cursor) {
let op_maybe: Option<OpCode> = FromPrimitive::from_i32(op_num);
let op_maybe: Option<TSCOpCode> = FromPrimitive::from_i32(op_num);
if let Some(op) = op_maybe {
match op {
// Zero operand codes
OpCode::AEp
| OpCode::CAT
| OpCode::CIL
| OpCode::CLO
| OpCode::CLR
| OpCode::CPS
| OpCode::CRE
| OpCode::CSS
| OpCode::END
| OpCode::ESC
| OpCode::FLA
| OpCode::FMU
| 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::HM2
| OpCode::POP
| OpCode::KE2
| OpCode::FR2 => {
TSCOpCode::AEp
| TSCOpCode::CAT
| TSCOpCode::CIL
| TSCOpCode::CLO
| TSCOpCode::CLR
| TSCOpCode::CPS
| TSCOpCode::CRE
| TSCOpCode::CSS
| TSCOpCode::END
| TSCOpCode::ESC
| TSCOpCode::FLA
| TSCOpCode::FMU
| TSCOpCode::FRE
| TSCOpCode::HMC
| TSCOpCode::INI
| TSCOpCode::KEY
| TSCOpCode::LDP
| TSCOpCode::MLP
| TSCOpCode::MM0
| TSCOpCode::MNA
| TSCOpCode::MS2
| TSCOpCode::MS3
| TSCOpCode::MSG
| TSCOpCode::NOD
| TSCOpCode::PRI
| TSCOpCode::RMU
| TSCOpCode::SAT
| TSCOpCode::SLP
| TSCOpCode::SMC
| TSCOpCode::SPS
| TSCOpCode::STC
| TSCOpCode::SVP
| TSCOpCode::TUR
| TSCOpCode::WAS
| TSCOpCode::ZAM
| TSCOpCode::HM2
| TSCOpCode::POP
| TSCOpCode::KE2
| TSCOpCode::FR2 => {
result.push_str(format!("{:?}()\n", op).as_str());
}
// One operand codes
OpCode::BOA
| OpCode::BSL
| OpCode::FOB
| OpCode::FOM
| OpCode::QUA
| OpCode::UNI
| OpCode::MYB
| OpCode::MYD
| OpCode::FAI
| OpCode::FAO
| OpCode::WAI
| OpCode::FAC
| OpCode::GIT
| OpCode::NUM
| OpCode::DNA
| OpCode::DNP
| OpCode::FLm
| OpCode::FLp
| 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::S2MV
| OpCode::S2PJ
| OpCode::PSH => {
TSCOpCode::BOA
| TSCOpCode::BSL
| TSCOpCode::FOB
| TSCOpCode::FOM
| TSCOpCode::QUA
| TSCOpCode::UNI
| TSCOpCode::MYB
| TSCOpCode::MYD
| TSCOpCode::FAI
| TSCOpCode::FAO
| TSCOpCode::WAI
| TSCOpCode::FAC
| TSCOpCode::GIT
| TSCOpCode::NUM
| TSCOpCode::DNA
| TSCOpCode::DNP
| TSCOpCode::FLm
| TSCOpCode::FLp
| TSCOpCode::MPp
| TSCOpCode::SKm
| TSCOpCode::SKp
| TSCOpCode::EQp
| TSCOpCode::EQm
| TSCOpCode::MLp
| TSCOpCode::ITp
| TSCOpCode::ITm
| TSCOpCode::AMm
| TSCOpCode::UNJ
| TSCOpCode::MPJ
| TSCOpCode::YNJ
| TSCOpCode::EVE
| TSCOpCode::XX1
| TSCOpCode::SIL
| TSCOpCode::LIp
| TSCOpCode::SOU
| TSCOpCode::CMU
| TSCOpCode::SSS
| TSCOpCode::ACH
| TSCOpCode::S2MV
| TSCOpCode::S2PJ
| TSCOpCode::PSH => {
let par_a = read_cur_varint(&mut cursor)?;
result.push_str(format!("{:?}({})\n", op, par_a).as_str());
}
// Two operand codes
OpCode::FON
| OpCode::MOV
| OpCode::AMp
| OpCode::NCJ
| OpCode::ECJ
| OpCode::FLJ
| OpCode::ITJ
| OpCode::SKJ
| OpCode::AMJ
| OpCode::SMP
| OpCode::PSp
| OpCode::IpN
| OpCode::FFm => {
TSCOpCode::FON
| TSCOpCode::MOV
| TSCOpCode::AMp
| TSCOpCode::NCJ
| TSCOpCode::ECJ
| TSCOpCode::FLJ
| TSCOpCode::ITJ
| TSCOpCode::SKJ
| TSCOpCode::AMJ
| TSCOpCode::SMP
| TSCOpCode::PSp
| TSCOpCode::IpN
| TSCOpCode::FFm => {
let par_a = read_cur_varint(&mut cursor)?;
let par_b = read_cur_varint(&mut cursor)?;
result.push_str(format!("{:?}({}, {})\n", op, par_a, par_b).as_str());
}
// Three operand codes
OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => {
TSCOpCode::ANP | TSCOpCode::CNP | TSCOpCode::INP | TSCOpCode::TAM | TSCOpCode::CMP | TSCOpCode::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)?;
@ -135,7 +135,7 @@ impl TextScript {
result.push_str(format!("{:?}({}, {}, {})\n", op, par_a, par_b, par_c).as_str());
}
// Four operand codes
OpCode::TRA | OpCode::MNP | OpCode::SNP => {
TSCOpCode::TRA | TSCOpCode::MNP | TSCOpCode::SNP => {
let par_a = read_cur_varint(&mut cursor)?;
let par_b = read_cur_varint(&mut cursor)?;
let par_c = read_cur_varint(&mut cursor)?;
@ -143,7 +143,7 @@ impl TextScript {
result.push_str(format!("{:?}({}, {}, {}, {})\n", op, par_a, par_b, par_c, par_d).as_str());
}
OpCode::_STR => {
TSCOpCode::_STR => {
let len = read_cur_varint(&mut cursor)?;
result.push_str(format!("%string(len = {}, value = \"", len).as_str());
@ -169,9 +169,9 @@ impl TextScript {
}
result.push_str("\")\n");
}
OpCode::_NOP => result.push_str("%no_op()\n"),
OpCode::_UNI => result.push_str("%unimplemented()\n"),
OpCode::_END => result.push_str("%end_marker()\n"),
TSCOpCode::_NOP => result.push_str("%no_op()\n"),
TSCOpCode::_UNI => result.push_str("%unimplemented()\n"),
TSCOpCode::_END => result.push_str("%end_marker()\n"),
}
} else {
break;

View file

@ -0,0 +1,13 @@
pub fn decrypt_tsc(buf: &mut [u8]) {
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;
}
*byte = byte.wrapping_add(key);
}
}

View file

@ -2,6 +2,7 @@ mod bytecode_utils;
mod compiler;
pub mod credit_script;
mod decompiler;
mod encryption;
mod opcodes;
mod parse_utils;
pub mod text_script;

View file

@ -1,9 +1,8 @@
use num_derive::FromPrimitive;
/// Engine's text script VM operation codes.
#[derive(EnumString, Debug, FromPrimitive, PartialEq)]
#[repr(i32)]
pub enum OpCode {
#[derive(EnumString, Debug, FromPrimitive, PartialEq, Copy, Clone)]
pub enum TSCOpCode {
// ---- Internal opcodes (used by bytecode, no TSC representation)
/// internal: no operation
_NOP = 0,
@ -267,3 +266,54 @@ pub enum OpCode {
FR2,
// ---- Custom opcodes, for use by modders ----
}
#[derive(FromPrimitive, PartialEq, Copy, Clone)]
pub enum CreditOpCode {
/// Internal, no operation
_NOP = 0,
/// `/`
///
/// Arguments: `()`
StopCredits,
/// `[{text: string}]{cast_tile: number}`
///
/// Arguments: `(cast_tile: varint, text_len: varint, text: [varint; text_len])`
PushLine,
/// `-{ticks: number}`
///
/// Arguments: `(ticks: varint)`
Wait,
/// `+{offset: number}`
///
/// Arguments: `(offset: varint)`
ChangeXOffset,
/// `!{music_id: number}`
///
/// Arguments: `(music_id: varint)`
ChangeMusic,
/// `~`
///
/// Arguments: `()`
FadeMusic,
/// `j{label: number}`
///
/// Arguments: `(label: varint)`
JumpLabel,
/// `f{flag: number}:{label: number}`
///
/// Arguments: `(flag: varint, label: varint)`
JumpFlag,
// ---- Cave Story+ (Switch) specific opcodes ----
/// `p2:{label: number}`
///
/// Arguments: `(label: varint)`
JumpPlayer2,
}

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@ use crate::scene::title_scene::TitleScene;
use crate::scene::Scene;
#[cfg(feature = "scripting-lua")]
use crate::scripting::lua::LuaScriptingState;
use crate::scripting::tsc::credit_script::CreditScriptVM;
use crate::settings::Settings;
use crate::sound::SoundManager;
use crate::stage::StageData;
@ -137,6 +138,7 @@ pub struct SharedGameState {
pub preferred_viewport_size: (f32, f32),
pub next_scene: Option<Box<dyn Scene>>,
pub textscript_vm: TextScriptVM,
pub creditscript_vm: CreditScriptVM,
pub lightmap_canvas: Option<Box<dyn BackendTexture>>,
pub season: Season,
pub constants: EngineConstants,
@ -237,6 +239,7 @@ impl SharedGameState {
preferred_viewport_size: (320.0, 240.0),
next_scene: None,
textscript_vm: TextScriptVM::new(),
creditscript_vm: CreditScriptVM::new(),
lightmap_canvas: None,
season,
constants,