diff --git a/src/engine_constants.rs b/src/engine_constants.rs index ba6e157..f09a10b 100644 --- a/src/engine_constants.rs +++ b/src/engine_constants.rs @@ -88,12 +88,18 @@ impl Clone for WorldConsts { #[derive(Debug)] pub struct TextScriptConsts { pub encoding: TextScriptEncoding, + pub textbox_rect_top: Rect, + pub textbox_rect_middle: Rect, + pub textbox_rect_bottom: Rect, } impl Clone for TextScriptConsts { fn clone(&self) -> Self { Self { encoding: self.encoding, + textbox_rect_top: self.textbox_rect_top, + textbox_rect_middle: self.textbox_rect_middle, + textbox_rect_bottom: self.textbox_rect_bottom, } } } @@ -364,7 +370,10 @@ impl EngineConstants { }, textscript: TextScriptConsts { encoding: TextScriptEncoding::UTF8, - } + 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 }, + }, } } diff --git a/src/live_debugger.rs b/src/live_debugger.rs index badc425..142f4a6 100644 --- a/src/live_debugger.rs +++ b/src/live_debugger.rs @@ -86,7 +86,7 @@ impl LiveDebugger { .size([300.0, 100.0], Condition::Appearing) .build(ui, || { ui.push_item_width(-1.0); - ui.text(self.error.as_ref().unwrap()); + ui.text_wrapped(self.error.as_ref().unwrap()); if ui.button(im_str!("OK"), [0.0, 0.0]) { self.error = None; @@ -135,25 +135,29 @@ impl LiveDebugger { Window::new(im_str!("Events")) .resizable(false) .position([80.0, 80.0], Condition::FirstUseEver) - .size([250.0, 300.0], Condition::FirstUseEver) + .size([280.0, 300.0], Condition::FirstUseEver) .build(ui, || { if self.events.is_empty() { self.event_ids.clear(); let vm = &state.textscript_vm; - for event in vm.global_script.get_event_ids() { + for event in vm.scripts.global_script.get_event_ids() { self.events.push(ImString::new(format!("Global: #{:04}", event))); self.event_ids.push(event); } - for event in vm.scene_script.get_event_ids() { + for event in vm.scripts.scene_script.get_event_ids() { self.events.push(ImString::new(format!("Scene: #{:04}", event))); self.event_ids.push(event); } } let events: Vec<&ImStr> = self.events.iter().map(|e| e.as_ref()).collect(); - ui.text(format!("Execution state: {:?}", state.textscript_vm.state)); + ui.text_wrapped(&ImString::new(format!("Execution state: {:?}", state.textscript_vm.state))); + let line1: String = state.textscript_vm.line_1.iter().collect(); + let line2: String = state.textscript_vm.line_2.iter().collect(); + ui.text_wrapped(&ImString::new(&line1)); + ui.text_wrapped(&ImString::new(&line2)); ui.push_item_width(-1.0); ui.list_box(im_str!(""), &mut self.selected_event, &events, 10); diff --git a/src/main.rs b/src/main.rs index 865cb56..140965e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,7 @@ bitfield! { } bitfield! { - pub struct ControlFlags(u32); + pub struct ControlFlags(u16); impl Debug; pub flag_x01, set_flag_x01: 0; pub control_enabled, set_control_enabled: 1; diff --git a/src/player.rs b/src/player.rs index ebc1fc3..323a1a8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -87,12 +87,12 @@ pub struct Player { pub unit: u8, pub question: bool, pub booster_fuel: usize, + pub up: bool, + pub down: bool, + pub shock_counter: u8, index_x: isize, index_y: isize, sprash: bool, - up: bool, - down: bool, - shock_counter: u8, booster_switch: u8, star: u8, bubble: u8, diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index e84e0c8..b3d6c4d 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -5,14 +5,13 @@ use crate::entity::GameEntity; use crate::frame::Frame; use crate::ggez::{Context, GameResult, timer}; use crate::ggez::nalgebra::clamp; -use crate::live_debugger::LiveDebugger; use crate::player::Player; use crate::scene::Scene; use crate::SharedGameState; use crate::stage::{BackgroundType, Stage}; use crate::str; -use crate::ui::{UI, Components}; use crate::text_script::{TextScript, TextScriptVM}; +use crate::ui::Components; pub struct GameScene { pub tick: usize, @@ -22,6 +21,7 @@ pub struct GameScene { pub stage_id: usize, tex_background_name: String, tex_caret_name: String, + tex_face_name: String, tex_hud_name: String, tex_npcsym_name: String, tex_tileset_name: String, @@ -51,6 +51,7 @@ impl GameScene { let tex_background_name = stage.data.background.filename(); let tex_caret_name = str!("Caret"); + let tex_face_name = str!("Face"); let tex_hud_name = str!("TextBox"); let tex_npcsym_name = str!("Npc/NpcSym"); let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join(""); @@ -67,6 +68,7 @@ impl GameScene { stage_id: id, tex_background_name, tex_caret_name, + tex_face_name, tex_hud_name, tex_npcsym_name, tex_tileset_name, @@ -208,6 +210,37 @@ impl GameScene { } fn draw_black_bars(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + Ok(()) + } + + fn draw_text_boxes(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if !state.textscript_vm.flags.render() { return Ok(()); } + + let top_pos = if state.textscript_vm.flags.position_top() { 32.0 } else { state.canvas_size.1 as f32 - 64.0 }; + let left_pos = (state.canvas_size.0 / 2.0 - 122.0).floor(); + + if state.textscript_vm.flags.background_visible() { + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_hud_name)?; + + batch.add_rect(left_pos, top_pos, &state.constants.textscript.textbox_rect_top); + for i in 1..7 { + batch.add_rect(left_pos, top_pos + i as f32 * 8.0, &state.constants.textscript.textbox_rect_middle); + } + batch.add_rect(left_pos, top_pos + 64.0, &state.constants.textscript.textbox_rect_bottom); + + batch.draw(ctx)?; + } + + if state.textscript_vm.face != 0 { + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_face_name)?; + batch.add_rect(left_pos + 14.0, top_pos + 8.0, &Rect::::new_size( + (state.textscript_vm.face as usize % 6) * 48, + (state.textscript_vm.face as usize / 6) * 48, + 48, 48, + )); + + batch.draw(ctx)?; + } Ok(()) } @@ -320,7 +353,7 @@ impl Scene for GameScene { } } - TextScriptVM::run(state, self); + TextScriptVM::run(state, self)?; self.tick = self.tick.wrapping_add(1); Ok(()) } @@ -337,6 +370,8 @@ impl Scene for GameScene { self.draw_hud(state, ctx)?; self.draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?; + self.draw_text_boxes(state, ctx)?; + Ok(()) } diff --git a/src/text_script.rs b/src/text_script.rs index 5449220..de8177c 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -12,7 +12,7 @@ use num_derive::FromPrimitive; use num_traits::FromPrimitive; use crate::{SharedGameState, str}; -use crate::common::CursorIterator; +use crate::bitfield; use crate::ggez::GameError::ParseError; use crate::ggez::GameResult; use crate::scene::game_scene::GameScene; @@ -161,29 +161,47 @@ pub enum OpCode { // ---- Custom opcodes, for use by modders ---- } -#[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)] +bitfield! { + pub struct TextScriptFlags(u16); + impl Debug; + pub render, set_render: 0; + pub background_visible, set_background_visible: 1; + pub flag_x10, set_flag_x10: 4; + pub position_top, set_position_top: 5; + pub flag_x40, set_flag_x40: 6; +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] pub enum TextScriptEncoding { - UTF8, + UTF8 = 0, ShiftJIS, } +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum TextScriptLine { + Line1 = 0, + Line2, +} + #[derive(Debug, PartialEq, Copy, Clone)] pub enum TextScriptExecutionState { Ended, Running(u16, u32), - Msg(u16, u32, u32), + Msg(u16, u32, u32, u8), WaitTicks(u16, u32, u32), WaitInput(u16, u32), } pub struct TextScriptVM { - pub global_script: TextScript, - pub scene_script: TextScript, + pub scripts: TextScriptVMScripts, pub state: TextScriptExecutionState, - msg_timer: u8, - face: u16, - line_1: Vec, - line_2: Vec, + pub flags: TextScriptFlags, + pub face: u16, + pub current_line: TextScriptLine, + pub line_1: Vec, + pub line_2: Vec, } impl Default for TextScriptVM { @@ -192,6 +210,23 @@ impl Default for TextScriptVM { } } +pub struct TextScriptVMScripts { + pub global_script: TextScript, + pub scene_script: TextScript, +} + +impl TextScriptVMScripts { + pub fn find_script(&self, event_num: u16) -> Option<&Vec> { + if let Some(tsc) = self.scene_script.event_map.get(&event_num) { + return Some(tsc); + } else if let Some(tsc) = self.global_script.event_map.get(&event_num) { + return Some(tsc); + } + + None + } +} + fn read_cur_varint(cursor: &mut Cursor<&Vec>) -> GameResult { let mut result = 0u32; @@ -209,9 +244,9 @@ fn read_cur_varint(cursor: &mut Cursor<&Vec>) -> GameResult { /// Decodes UTF-8 character in a less strict way. /// http://simonsapin.github.io/wtf-8/#decoding-wtf-8 -fn read_cur_wtf8(cursor: &mut Cursor<&Vec>, max_bytes: usize) -> (u8, char) { +fn read_cur_wtf8(cursor: &mut Cursor<&Vec>, max_bytes: u32) -> (u32, char) { let mut result = 0u32; - let mut consumed = 0u8; + let mut consumed = 0u32; if max_bytes == 0 { return (0, '\u{fffd}'); @@ -256,39 +291,38 @@ fn read_cur_wtf8(cursor: &mut Cursor<&Vec>, max_bytes: usize) -> (u8, char) impl TextScriptVM { pub fn new() -> Self { Self { - global_script: TextScript::new(), - scene_script: TextScript::new(), + scripts: TextScriptVMScripts { + global_script: TextScript::new(), + scene_script: TextScript::new(), + }, state: TextScriptExecutionState::Ended, - msg_timer: 0, + flags: TextScriptFlags(0), face: 0, + current_line: TextScriptLine::Line1, line_1: Vec::with_capacity(24), line_2: Vec::with_capacity(24), } } pub fn set_global_script(&mut self, script: TextScript) { - self.global_script = script; + self.scripts.global_script = script; self.reset(); } pub fn set_scene_script(&mut self, script: TextScript) { - self.scene_script = script; + self.scripts.scene_script = script; self.reset(); } - pub fn find_script(&self, event_num: u16) -> Option<&Vec> { - if let Some(tsc) = self.scene_script.event_map.get(&event_num) { - return Some(tsc); - } else if let Some(tsc) = self.global_script.event_map.get(&event_num) { - return Some(tsc); - } - - None - } - pub fn reset(&mut self) { self.state = TextScriptExecutionState::Ended; + self.clear_text_box(); + } + + pub fn clear_text_box(&mut self) { + self.flags.0 = 0; self.face = 0; + self.current_line = TextScriptLine::Line1; self.line_1.clear(); self.line_2.clear(); } @@ -303,14 +337,52 @@ impl TextScriptVM { loop { match state.textscript_vm.state { TextScriptExecutionState::Ended => { + state.control_flags.set_flag_x04(false); break; } TextScriptExecutionState::Running(event, ip) => { + state.control_flags.set_flag_x04(true); state.textscript_vm.state = TextScriptVM::execute(event, ip, state, game_scene)?; - println!("new vm state: {:?}", state.textscript_vm.state); + + if state.textscript_vm.state == TextScriptExecutionState::Ended { + state.textscript_vm.reset(); + } } - TextScriptExecutionState::Msg(event, ip, remaining) => { - if let Some(bytecode) = state.textscript_vm.find_script(event) {} else { + TextScriptExecutionState::Msg(event, ip, remaining, timer) => { + if timer > 0 { + state.textscript_vm.state = TextScriptExecutionState::Msg(event, ip, remaining, timer - 1); + break; + } + + if let Some(bytecode) = state.textscript_vm.scripts.find_script(event) { + let mut cursor = Cursor::new(bytecode); + cursor.seek(SeekFrom::Start(ip as u64))?; + + let (consumed, chr) = read_cur_wtf8(&mut cursor, remaining); + println!("char: {} {} {}", chr, remaining, consumed); + + match chr { + '\n' => { + state.textscript_vm.line_1.clear(); + state.textscript_vm.line_2.append(&mut state.textscript_vm.line_1); + } + '\r' => {} + _ if state.textscript_vm.current_line == TextScriptLine::Line1 => { + state.textscript_vm.line_1.push(chr); + } + _ if state.textscript_vm.current_line == TextScriptLine::Line2 => { + state.textscript_vm.line_2.push(chr); + } + _ => {} + } + + if (remaining - consumed) > 0 { + let ticks = if state.key_state.jump() || state.key_state.fire() { 1 } else { 4 }; + state.textscript_vm.state = TextScriptExecutionState::Msg(event, ip + consumed, remaining - consumed, ticks); + } else { + state.textscript_vm.state = TextScriptExecutionState::Running(event, ip + consumed); + } + } else { state.textscript_vm.reset(); } } @@ -337,28 +409,61 @@ impl TextScriptVM { pub fn execute(event: u16, ip: u32, state: &mut SharedGameState, game_scene: &mut GameScene) -> GameResult { let mut exec_state = state.textscript_vm.state; - if let Some(bytecode) = state.textscript_vm.find_script(event) { + if let Some(bytecode) = state.textscript_vm.scripts.find_script(event) { let mut cursor = Cursor::new(bytecode); cursor.seek(SeekFrom::Start(ip as u64))?; let op_maybe: Option = FromPrimitive::from_i32(read_cur_varint(&mut cursor)?); if let Some(op) = op_maybe { + println!("opcode: {:?}", op); match op { OpCode::_NOP => { - println!("opcode: {:?}", op); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::_UNI => {} OpCode::_STR => { - // simply skip the text if we aren't in message mode. let len = read_cur_varint(&mut cursor)? as u32; - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32 + len); + if state.textscript_vm.flags.render() { + exec_state = TextScriptExecutionState::Msg(event, cursor.position() as u32, len, 4); + } else { + // simply skip the text if we aren't in message mode. + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32 + len); + } } OpCode::_END | OpCode::END => { + state.control_flags.set_flag_x01(true); + state.control_flags.set_control_enabled(true); + exec_state = TextScriptExecutionState::Ended; } + OpCode::PRI => { + state.control_flags.set_flag_x01(false); + state.control_flags.set_control_enabled(false); + game_scene.player.shock_counter = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + OpCode::KEY => { + state.control_flags.set_flag_x01(true); + state.control_flags.set_control_enabled(false); + + game_scene.player.up = false; + game_scene.player.down = false; + game_scene.player.shock_counter = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + OpCode::FRE => { + state.control_flags.set_flag_x01(true); + state.control_flags.set_control_enabled(true); + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + OpCode::WAI => { + let ticks = read_cur_varint(&mut cursor)? as u32; + exec_state = TextScriptExecutionState::WaitTicks(event, cursor.position() as u32, ticks); + } OpCode::NOD => { exec_state = TextScriptExecutionState::WaitInput(event, cursor.position() as u32); } @@ -372,6 +477,8 @@ impl TextScriptVM { let event_num = read_cur_varint(&mut cursor)? as u16; if let Some(true) = state.game_flags.get(flag_num) { exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } } OpCode::EVE => { @@ -380,6 +487,7 @@ impl TextScriptVM { } OpCode::MM0 => { game_scene.player.vel_x = 0; + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::CMP => { let pos_x = read_cur_varint(&mut cursor)? as usize; @@ -397,23 +505,41 @@ impl TextScriptVM { game_scene.player.max_life += life; exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } + OpCode::FAC => { + let face = read_cur_varint(&mut cursor)? as u16; + state.textscript_vm.face = face; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + OpCode::MSG => { + state.textscript_vm.face = 0; + state.textscript_vm.current_line = TextScriptLine::Line1; + state.textscript_vm.line_1.clear(); + state.textscript_vm.line_2.clear(); + state.textscript_vm.flags.set_render(true); + state.textscript_vm.flags.set_background_visible(true); + state.textscript_vm.flags.set_flag_x10(state.textscript_vm.flags.flag_x40()); + state.textscript_vm.flags.set_position_top(false); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } // unimplemented opcodes // Zero operands OpCode::AEp | OpCode::CAT | OpCode::CIL | OpCode::CLO | OpCode::CLR | OpCode::CPS | OpCode::CRE | OpCode::CSS | OpCode::ESC | OpCode::FLA | OpCode::FMU | - OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP | - OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | - OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS | + OpCode::HMC | OpCode::INI | OpCode::LDP | OpCode::MLP | + OpCode::MNA | OpCode::MS2 | OpCode::MS3 | + OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS | OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM => { println!("unimplemented opcode: {:?}", op); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // 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::MYB | OpCode::MYD | OpCode::FAI | OpCode::FAO | OpCode::FAC | OpCode::GIT | OpCode::NUM | OpCode::DNA | OpCode::DNP | - OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp | + OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU | OpCode::SSS | OpCode::ACH => { @@ -447,6 +573,8 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } } + } else { + exec_state = TextScriptExecutionState::Ended; } } else { return Ok(TextScriptExecutionState::Ended); @@ -531,7 +659,7 @@ impl TextScript { println!("{:x?}", &bytecode); event_map.insert(event_num, bytecode); } - b'\r' | b'\n' => { + b'\r' | b'\n' | b' ' => { iter.next(); } n => { diff --git a/src/texture_set.rs b/src/texture_set.rs index c0334ae..19ad683 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -159,4 +159,9 @@ impl TextureSet { Ok(self.tex_map.get_mut(name).unwrap()) } + + pub fn draw_text(&mut self, ctx: &mut Context, text: &str) -> GameResult { + + Ok(()) + } }