diff --git a/Cargo.toml b/Cargo.toml index 6a5453b..9c52e90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,9 +57,9 @@ gfx_core = "0.9" gfx_device_gl = {git = "https://github.com/doukutsu-rs/gfx.git", branch = "pre-ll"} ggez = {git = "https://github.com/doukutsu-rs/ggez.git", rev = "aad56b0d173ca9f4aeb28599075b5af49ab9214e"} glutin = {git = "https://github.com/doukutsu-rs/glutin.git", branch = "android-support"} -imgui = {git = "https://github.com/JMS55/imgui-rs.git"} -imgui-gfx-renderer = {git = "https://github.com/JMS55/imgui-rs.git"} -imgui-winit-support = {git = "https://github.com/JMS55/imgui-rs.git", default-features = false, features = ["winit-23"]} +imgui = {git = "https://github.com/Gekkio/imgui-rs.git", rev = "a990a538b66cb67dba3a072bf299b6a51c001447"} +imgui-gfx-renderer = {git = "https://github.com/Gekkio/imgui-rs.git", rev = "a990a538b66cb67dba3a072bf299b6a51c001447"} +imgui-winit-support = {git = "https://github.com/Gekkio/imgui-rs.git", default-features = false, features = ["winit-23"], rev = "a990a538b66cb67dba3a072bf299b6a51c001447"} image = {version = "0.22", default-features = false, features = ["png_codec", "pnm", "bmp"]} itertools = "0.9.0" lazy_static = "1.4.0" diff --git a/src/difficulty_modifier.rs b/src/difficulty_modifier.rs new file mode 100644 index 0000000..b075af0 --- /dev/null +++ b/src/difficulty_modifier.rs @@ -0,0 +1,3 @@ +struct DifficultyModifier { + +} diff --git a/src/lib.rs b/src/lib.rs index e94c6da..56726f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ mod bullet; mod caret; mod common; mod components; +mod difficulty_modifier; mod encoding; mod engine_constants; mod entity; diff --git a/src/live_debugger.rs b/src/live_debugger.rs index f92c8d2..e19c749 100644 --- a/src/live_debugger.rs +++ b/src/live_debugger.rs @@ -1,10 +1,21 @@ -use imgui::{CollapsingHeader, Condition, im_str, ImStr, ImString, Window}; -use itertools::Itertools; +use std::ops::Deref; use ggez::{Context, GameResult}; +use imgui::{CollapsingHeader, Condition, im_str, ImStr, ImString, Slider, Window, WindowFlags}; +use itertools::Itertools; + use crate::scene::game_scene::GameScene; use crate::shared_game_state::SharedGameState; +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum ScriptType { + Scene, + Global, + Inventory, + StageSelect, +} + pub struct LiveDebugger { map_selector_visible: bool, events_visible: bool, @@ -14,8 +25,9 @@ pub struct LiveDebugger { stages: Vec, selected_stage: i32, events: Vec, - event_ids: Vec, + event_ids: Vec<(ScriptType, u16)>, selected_event: i32, + text_windows: Vec<(u32, ImString, ImString)>, error: Option, } @@ -32,6 +44,7 @@ impl LiveDebugger { events: Vec::new(), event_ids: Vec::new(), selected_event: -1, + text_windows: Vec::new(), error: None, } } @@ -44,18 +57,15 @@ impl LiveDebugger { } Window::new(im_str!("Debugger")) + .resizable(false) .collapsed(true, Condition::FirstUseEver) .position([5.0, 5.0], Condition::FirstUseEver) - .size([300.0, 120.0], Condition::FirstUseEver) + .size([380.0, 170.0], Condition::FirstUseEver) .build(ui, || { ui.text(format!( - "Player position: ({:.1},{:.1})", + "Player position: ({:.1},{:.1}), velocity: ({:.1},{:.1})", game_scene.player.x as f32 / 512.0, game_scene.player.y as f32 / 512.0, - )); - - ui.text(format!( - "Player velocity: ({:.1},{:.1})", game_scene.player.vel_x as f32 / 512.0, game_scene.player.vel_y as f32 / 512.0, )); @@ -67,9 +77,24 @@ impl LiveDebugger { )); ui.text(format!( - "Booster fuel: ({})", game_scene.player.booster_fuel + "Booster fuel: {}", game_scene.player.booster_fuel )); + + ui.text(format!("Game speed ({:.1} TPS):", state.current_tps())); + let mut speed = state.settings.speed; + Slider::new(im_str!("")) + .range(0.1..=3.0) + .build(ui, &mut speed); + ui.same_line(0.0); + if ui.button(im_str!("Reset"), [0.0, 0.0]) { + speed = 1.0 + } + + if state.settings.speed != speed { + state.set_speed(speed); + } + if ui.button(im_str!("Map Selector"), [0.0, 0.0]) { self.map_selector_visible = !self.map_selector_visible; } @@ -90,27 +115,11 @@ impl LiveDebugger { } }); - if self.error.is_some() { - Window::new(im_str!("Error!")) - .resizable(false) - .collapsible(false) - .position([((state.screen_size.0 - 300.0) / 2.0).floor(), ((state.screen_size.1 - 100.0) / 2.0).floor()], Condition::Appearing) - .size([300.0, 100.0], Condition::Appearing) - .build(ui, || { - ui.push_item_width(-1.0); - ui.text_wrapped(self.error.as_ref().unwrap()); - - if ui.button(im_str!("OK"), [0.0, 0.0]) { - self.error = None; - } - }); - } - if self.map_selector_visible { Window::new(im_str!("Map selector")) .resizable(false) - .position([80.0, 80.0], Condition::FirstUseEver) - .size([240.0, 280.0], Condition::FirstUseEver) + .position([80.0, 80.0], Condition::Appearing) + .size([240.0, 280.0], Condition::Appearing) .build(ui, || { if self.stages.is_empty() { for s in state.stages.iter() { @@ -153,21 +162,32 @@ impl LiveDebugger { if self.events_visible { Window::new(im_str!("Events")) .resizable(false) - .position([80.0, 80.0], Condition::FirstUseEver) - .size([280.0, 300.0], Condition::FirstUseEver) + .position([80.0, 80.0], Condition::Appearing) + .size([300.0, 300.0], Condition::Appearing) .build(ui, || { if self.events.is_empty() { self.event_ids.clear(); let vm = &state.textscript_vm; - 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.scripts.scene_script.get_event_ids() { self.events.push(ImString::new(format!("Scene: #{:04}", event))); - self.event_ids.push(event); + self.event_ids.push((ScriptType::Scene, event)); + } + + for event in vm.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() { + 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() { + 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(); @@ -180,26 +200,108 @@ impl LiveDebugger { if ui.button(im_str!("Execute"), [0.0, 0.0]) { assert_eq!(self.event_ids.len(), self.events.len()); - if let Some(&event_num) = self.event_ids.get(self.selected_event as usize) { + if let Some((_, event_num)) = self.event_ids.get(self.selected_event as usize) { state.control_flags.set_tick_world(true); state.control_flags.set_interactions_disabled(true); - state.textscript_vm.start_script(event_num); + state.textscript_vm.start_script(*event_num); + } + } + + ui.same_line(0.0); + if ui.button(im_str!("Decompile"), [0.0, 0.0]) { + 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 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, + }; + + match script.decompile_event(*event_num) { + Ok(code) => { + self.text_windows.push(( + id, + ImString::new(format!("Decompiled event: #{:04}", *event_num)), + ImString::new(code) + )); + } + Err(e) => { + self.error = Some(ImString::new(format!("Error decompiling TextScript #{:04}: {}", *event_num, e))); + } + } + } } } }); } - if self.flags_visible { Window::new(im_str!("Flags")) .position([80.0, 80.0], Condition::FirstUseEver) .size([280.0, 300.0], Condition::FirstUseEver) .build(ui, || { - if CollapsingHeader::new(im_str!("Control flags")).default_open(true).build(&ui) - { - ui.checkbox_flags(im_str!("Flag 0x01"), &mut state.control_flags.0, 1); + if CollapsingHeader::new(im_str!("Control flags")).default_open(false).build(&ui) { + ui.checkbox_flags(im_str!("Tick world"), &mut state.control_flags.0, 1); ui.checkbox_flags(im_str!("Control enabled"), &mut state.control_flags.0, 2); ui.checkbox_flags(im_str!("Interactions disabled"), &mut state.control_flags.0, 4); + ui.checkbox_flags(im_str!("Credits running"), &mut state.control_flags.0, 8); + ui.separator(); + ui.checkbox_flags(im_str!("[Internal] Windy level"), &mut state.control_flags.0, 15); + } + + if CollapsingHeader::new(im_str!("Player condition flags")).default_open(false).build(&ui) { + cond_flags(&ui, &mut game_scene.player.cond); + } + + if CollapsingHeader::new(im_str!("Player equipment")).default_open(false).build(&ui) { + ui.checkbox_flags(im_str!("Booster 0.8"), &mut game_scene.player.equip.0, 1); + ui.checkbox_flags(im_str!("Map System"), &mut game_scene.player.equip.0, 2); + ui.checkbox_flags(im_str!("Arms Barrier"), &mut game_scene.player.equip.0, 4); + ui.checkbox_flags(im_str!("Turbocharge"), &mut game_scene.player.equip.0, 8); + ui.checkbox_flags(im_str!("Air Tank"), &mut game_scene.player.equip.0, 16); + ui.checkbox_flags(im_str!("Booster 2.0"), &mut game_scene.player.equip.0, 32); + ui.checkbox_flags(im_str!("Mimiga Mask"), &mut game_scene.player.equip.0, 64); + ui.checkbox_flags(im_str!("Whimsical Star"), &mut game_scene.player.equip.0, 128); + ui.checkbox_flags(im_str!("Nikumaru Counter"), &mut game_scene.player.equip.0, 256); + } + }); + } + + let mut remove = -1; + for (idx, (_, title, contents)) in self.text_windows.iter().enumerate() { + let mut opened = true; + + Window::new(title) + .position([100.0, 100.0], Condition::FirstUseEver) + .size([400.0, 300.0], Condition::FirstUseEver) + .opened(&mut opened) + .build(ui, || { + ui.text_wrapped(contents); + }); + + if !opened { + remove = idx as i32; + } + } + + if remove >= 0 { + self.text_windows.remove(remove as usize); + } + + if self.error.is_some() { + Window::new(im_str!("Error!")) + .resizable(false) + .collapsible(false) + .position([((state.screen_size.0 - 300.0) / 2.0).floor(), ((state.screen_size.1 - 100.0) / 2.0).floor()], Condition::Appearing) + .size([300.0, 100.0], Condition::Appearing) + .build(ui, || { + ui.push_item_width(-1.0); + ui.text_wrapped(self.error.as_ref().unwrap()); + + if ui.button(im_str!("OK"), [0.0, 0.0]) { + self.error = None; } }); } @@ -207,3 +309,14 @@ impl LiveDebugger { Ok(()) } } + +fn cond_flags(ui: &imgui::Ui, cond: &mut crate::common::Condition) { + ui.checkbox_flags(im_str!("Interacted"), &mut cond.0, 1); + ui.checkbox_flags(im_str!("Hidden"), &mut cond.0, 2); + ui.checkbox_flags(im_str!("Fallen"), &mut cond.0, 4); + ui.checkbox_flags(im_str!("Built-in NPC destroy handler"), &mut cond.0, 8); + ui.checkbox_flags(im_str!("Damage first boss NPC"), &mut cond.0, 16); + ui.checkbox_flags(im_str!("Increased acceleration"), &mut cond.0, 32); + ui.checkbox_flags(im_str!("Unknown (0x40)"), &mut cond.0, 64); + ui.checkbox_flags(im_str!("Alive"), &mut cond.0, 128); +} diff --git a/src/npc/chaco.rs b/src/npc/chaco.rs new file mode 100644 index 0000000..c81f768 --- /dev/null +++ b/src/npc/chaco.rs @@ -0,0 +1,76 @@ +use ggez::GameResult; + +use crate::caret::CaretType; +use crate::common::Direction; +use crate::npc::NPC; +use crate::player::Player; +use crate::shared_game_state::SharedGameState; + +impl NPC { + pub(crate) fn tick_n093_chaco(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + self.anim_counter = 0; + } + + if state.game_rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + if (self.x - player.x).abs() < 32 * 0x200 + && self.y - 32 * 0x200 < player.y + && self.y + 16 * 0x200 > player.y { + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + } + } + 3 | 4 => { + if self.action_num == 3 { + self.action_num = 4; + self.anim_num = 2; + self.anim_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 4 { + self.anim_counter = 0; + self.anim_num += 1; + + if self.anim_num > 5 { + self.anim_num = 2; + } + } + + self.x += self.direction.vector_x() * 0x200; + } + 10 => { + self.anim_num = 6; + + self.action_counter += 1; + if self.action_counter > 200 { + self.action_counter = 0; + + state.create_caret(self.x, self.y, CaretType::Zzz, Direction::Left); + } + } + _ => {} + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 7 }; + + self.anim_rect = state.constants.npc.n093_chaco[self.anim_num as usize + dir_offset]; + + Ok(()) + } +} diff --git a/src/npc/mod.rs b/src/npc/mod.rs index ecdf439..65f51b0 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -26,6 +26,7 @@ use crate::str; pub mod balrog; pub mod boss; +pub mod chaco; pub mod characters; pub mod egg_corridor; pub mod first_cave; @@ -241,6 +242,8 @@ impl GameEntity<(&mut Player, &BTreeMap>, &mut Stage)> for NPC 88 => self.tick_n088_igor_boss(state, player), 89 => self.tick_n089_igor_dead(state, player), 91 => self.tick_n091_mimiga_cage(state), + 92 => self.tick_n092_sue_at_pc(state), + 93 => self.tick_n093_chaco(state, player), 94 => self.tick_n094_kulala(state, player), 95 => self.tick_n095_jelly(state), 96 => self.tick_n096_fan_left(state, player), @@ -389,7 +392,6 @@ impl PhysicalEntity for NPC { pub struct NPCMap { ids: HashSet, - /// Do not iterate over this directly outside render pipeline. pub npcs: BTreeMap>, /// NPCMap but for bosses and of static size. pub boss_map: BossNPC, @@ -509,18 +511,6 @@ impl NPCMap { } pub fn garbage_collect(&mut self) { - // let dead_npcs = self.npcs.iter().(|(&id, npc_cell)| { - // if !npc_cell.borrow().cond.alive() { - // Some(id) - // } else { - // None - // } - // }).collect_vec(); - // - // for npc_id in dead_npcs.iter() { - // self.npcs.remove(npc_id); - // } - for npc_cell in self.npcs.values_mut() { let mut npc = npc_cell.borrow(); diff --git a/src/npc/sue.rs b/src/npc/sue.rs index b6e8c4b..135676b 100644 --- a/src/npc/sue.rs +++ b/src/npc/sue.rs @@ -1,12 +1,13 @@ use std::cell::RefCell; use std::collections::BTreeMap; -use crate::common::Direction; use ggez::GameResult; +use num_traits::clamp; + +use crate::common::Direction; use crate::npc::{NPC, NPCMap}; use crate::player::Player; use crate::shared_game_state::SharedGameState; -use num_traits::clamp; impl NPC { pub fn tick_n042_sue(&mut self, state: &mut SharedGameState, player: &Player, map: &BTreeMap>) -> GameResult { @@ -198,7 +199,7 @@ impl NPC { self.anim_counter = 0; self.anim_num += 1; - if self.anim_num > 5{ + if self.anim_num > 5 { self.anim_num = 2; } } @@ -230,4 +231,61 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n092_sue_at_pc(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + self.anim_counter = 0; + + self.x -= 4 * 0x200; + self.y += 16 * 0x200; + } + + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + } + + if state.game_rng.range(0..80) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + if state.game_rng.range(0..120) == 10 { + self.action_num = 3; + self.action_counter = 0; + self.anim_num = 2; + } + } + 2 => { + self.action_counter += 1; + + if self.action_counter > 40 { + self.action_num = 3; + self.action_counter = 0; + self.anim_num = 2; + } + } + 3 => { + self.action_counter += 1; + if self.action_counter > 80 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => {} + } + + self.anim_rect = state.constants.npc.n092_sue_at_pc[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 4b41560..abc177d 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -7,6 +7,7 @@ use gfx::{self, *}; use ggez::{Context, filesystem, GameResult, graphics}; use ggez::filesystem::OpenOptions; use ggez::graphics::{Canvas, Shader}; +use num_traits::clamp; use crate::bmfont_renderer::BMFontRenderer; use crate::caret::{Caret, CaretType}; @@ -47,6 +48,14 @@ impl TimingMode { TimingMode::FrameSynchronized => { 0.0 } } } + + pub fn get_tps(self) -> usize { + match self { + TimingMode::_50Hz => { 50 } + TimingMode::_60Hz => { 60 } + TimingMode::FrameSynchronized => { 0 } + } + } } @@ -361,7 +370,7 @@ impl SharedGameState { } pub fn set_speed(&mut self, value: f64) { - self.settings.speed = value; + self.settings.speed = clamp(value, 0.1, 3.0); self.frame_time = 0.0; if let Err(err) = self.sound_manager.set_speed(value as f32) { @@ -369,6 +378,10 @@ impl SharedGameState { } } + pub fn current_tps(&self) -> f64 { + self.timing_mode.get_tps() as f64 * self.settings.speed + } + pub fn shutdown(&mut self) { self.shutdown = true; } diff --git a/src/text_script.rs b/src/text_script.rs index 6611087..e639a7d 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -9,8 +9,8 @@ use std::ops::Not; use std::str::FromStr; use byteorder::ReadBytesExt; -use ggez::{Context, GameResult}; -use ggez::GameError::ParseError; +use ggez::{Context, GameError, GameResult}; +use ggez::GameError::{InvalidValue, ParseError}; use itertools::Itertools; use num_derive::FromPrimitive; use num_traits::{clamp, FromPrimitive}; @@ -1604,6 +1604,9 @@ impl TextScript { TextScript::compile_code(code.as_ref(), strict, iter, &mut bytecode)?; } + b'\r' => { + iter.next(); + } _ => { char_buf.push(chr); @@ -1745,19 +1748,112 @@ impl TextScript { Ok(()) } + pub fn decompile_event(&self, id: u16) -> GameResult { + if let Some(bytecode) = self.event_map.get(&id) { + let mut result = String::new(); + let mut cursor = Cursor::new(bytecode); + + while let Ok(op_num) = read_cur_varint(&mut cursor) { + let op_maybe: Option = 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 => { + 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::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 => { + 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 => { + let par_a = read_cur_varint(&mut cursor)?; + let par_b = read_cur_varint(&mut cursor)?; + let par_c = read_cur_varint(&mut cursor)?; + + result.push_str(format!("{:?}({}, {}, {})\n", op, par_a, par_b, par_c).as_str()); + } + // Four operand codes + OpCode::TRA | OpCode::MNP | OpCode::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)?; + let par_d = read_cur_varint(&mut cursor)?; + + result.push_str(format!("{:?}({}, {}, {}, {})\n", op, par_a, par_b, par_c, par_d).as_str()); + } + OpCode::_STR => { + let len = read_cur_varint(&mut cursor)?; + + result.push_str(format!("%string(len = {}, value = \"", len).as_str()); + for _ in 0..len { + let chr = std::char::from_u32(read_cur_varint(&mut cursor)? as u32).unwrap_or('?'); + match chr { + '\n' => { + result.push_str("\\n"); + } + '\r' => { + result.push_str("\\r"); + } + '\t' => { + result.push_str("\\t"); + } + '\u{0000}'..='\u{001f}' | '\u{0080}'..='\u{ffff}' => { + result.push_str(chr.escape_unicode().to_string().as_str()); + } + _ => { + result.push(chr); + } + } + } + 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"), + } + } else { + break; + } + } + + Ok(result) + } else { + Err(InvalidValue("Unknown script.".to_string())) + } + } + fn expect_char>(expect: u8, iter: &mut I) -> GameResult { let res = iter.next(); match res { - Some(n) if n == expect => { - Ok(()) - } - Some(n) => { - Err(ParseError(format!("Expected {}, found {}", expect as char, n as char))) - } - None => { - Err(ParseError(str!("Script unexpectedly ended."))) - } + Some(n) if n == expect => Ok(()), + Some(n) => Err(ParseError(format!("Expected {}, found {}", expect as char, n as char))), + None => Err(ParseError(str!("Script unexpectedly ended."))), } }