1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-11-25 21:55:50 +00:00

Improve TextScript VM to make it execute ingame-conversations

This commit is contained in:
Alula 2020-08-27 07:12:14 +02:00
parent 677f928467
commit 8f908b306d
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
7 changed files with 234 additions and 53 deletions

View file

@ -88,12 +88,18 @@ impl Clone for WorldConsts {
#[derive(Debug)]
pub struct TextScriptConsts {
pub encoding: TextScriptEncoding,
pub textbox_rect_top: Rect<usize>,
pub textbox_rect_middle: Rect<usize>,
pub textbox_rect_bottom: Rect<usize>,
}
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 },
},
}
}

View file

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

View file

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

View file

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

View file

@ -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::<usize>::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(())
}

View file

@ -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<char>,
line_2: Vec<char>,
pub flags: TextScriptFlags,
pub face: u16,
pub current_line: TextScriptLine,
pub line_1: Vec<char>,
pub line_2: Vec<char>,
}
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<u8>> {
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<u8>>) -> GameResult<i32> {
let mut result = 0u32;
@ -209,9 +244,9 @@ fn read_cur_varint(cursor: &mut Cursor<&Vec<u8>>) -> GameResult<i32> {
/// 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<u8>>, max_bytes: usize) -> (u8, char) {
fn read_cur_wtf8(cursor: &mut Cursor<&Vec<u8>>, 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<u8>>, 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<u8>> {
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<TextScriptExecutionState> {
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<OpCode> = 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 => {

View file

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