diff --git a/Cargo.lock b/Cargo.lock index e225e13a08423..d49bb1d0d2734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4231,6 +4231,7 @@ dependencies = [ "url", "wasm-bindgen-futures", "weak-table", + "web-sys", "web-time", ] @@ -5007,6 +5008,7 @@ dependencies = [ "lzma-rs", "num-derive", "num-traits", + "serde", "simple_asn1", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index af9eb9a1022dc..23d140d906672 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,6 +66,7 @@ ttf-parser = "0.20" num-bigint = "0.4" unic-segment = "0.9.0" id3 = "1.13.1" +web-sys = { version="0.3.69", features=[ "console" ] } [target.'cfg(not(target_family = "wasm"))'.dependencies.futures] version = "0.3.30" diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 311e8ef4492d7..01b948903fc08 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -1,4 +1,5 @@ use crate::avm1::callable_value::CallableValue; +use crate::avm1::debug::VariableDumperJson; use crate::avm1::error::Error; use crate::avm1::function::{Avm1Function, ExecutionReason, FunctionObject}; use crate::avm1::object::{Object, TObject}; @@ -24,9 +25,11 @@ use smallvec::SmallVec; use std::borrow::Cow; use std::cmp::min; use std::fmt; +use std::io; use swf::avm1::read::Reader; use swf::avm1::types::*; use url::form_urlencoded; +use web_sys::console; use web_time::Instant; use super::object_reference::MovieClipReference; @@ -464,6 +467,36 @@ impl<'a, 'gc> Activation<'a, 'gc> { "({}) Action: {action:?}", self.id.depth(), ); + if self.context.avm1.output_json != -1 { + if self.context.avm1.output_json_stdin { + println!("{}", serde_json::to_string(&action).unwrap()); + + let mut buffer = String::new(); + match io::stdin().read_line(&mut buffer).unwrap() { + #[cfg(not(windows))] + 1 => (), //\n , \r //continue + #[cfg(windows)] + 2 => (), //\r\n //continue + + _ => { + //breakpoint + let mut dumper = VariableDumperJson::new(); + dumper.print_activation(self); + println!("{}", dumper.output()); + let _ = io::stdin().read_line(&mut buffer); //block again to aknowledge the breakpoint at the other side + } + } + } else { + if self.context.avm1.output_json == 1 { + console::log_1(&serde_json::to_string(&action).unwrap().into()); + } + if action.discriminant() == self.context.avm1.output_json_code { + let mut dumper = VariableDumperJson::new(); + dumper.print_activation(self); + console::log_1(&dumper.output().into()); + } + } + } match action { Action::Add => self.action_add(), diff --git a/core/src/avm1/debug.rs b/core/src/avm1/debug.rs index c1cde3cf7326c..fc541e82320d8 100644 --- a/core/src/avm1/debug.rs +++ b/core/src/avm1/debug.rs @@ -1,8 +1,33 @@ use crate::avm1::activation::Activation; use crate::avm1::{Object, ObjectPtr, TObject, Value}; +use crate::display_object::{TDisplayObject, TDisplayObjectContainer}; use crate::string::AvmString; use std::fmt::Write; +macro_rules! print_string { + ($self:expr,$string:expr) => { + $self.output.push('\"'); + for c in $string.chars() { + let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); + let escape = match u8::try_from(c as u32) { + Ok(b'"') => "\\\"", + Ok(b'\\') => "\\\\", + Ok(b'\n') => "\\n", + Ok(b'\r') => "\\r", + Ok(b'\t') => "\\t", + Ok(0x08) => "\\b", + Ok(0x0C) => "\\f", + _ => { + $self.output.push(c); + continue; + } + }; + $self.output.push_str(escape); + } + $self.output.push('\"'); + }; +} + #[allow(dead_code)] pub struct VariableDumper<'a> { objects: Vec<*const ObjectPtr>, @@ -57,28 +82,7 @@ impl<'a> VariableDumper<'a> { } pub fn print_string(&mut self, string: AvmString<'_>) { - self.output.push('\"'); - - for c in string.chars() { - let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); - let escape = match u8::try_from(c as u32) { - Ok(b'"') => "\\\"", - Ok(b'\\') => "\\\\", - Ok(b'\n') => "\\n", - Ok(b'\r') => "\\r", - Ok(b'\t') => "\\t", - Ok(0x08) => "\\b", - Ok(0x0C) => "\\f", - _ => { - self.output.push(c); - continue; - } - }; - - self.output.push_str(escape); - } - - self.output.push('\"'); + print_string!(self, string); } pub fn print_object<'gc>( @@ -189,6 +193,136 @@ impl<'a> VariableDumper<'a> { } } +pub struct VariableDumperJson { + output: String, +} + +impl<'a> VariableDumperJson { + pub fn new() -> Self { + Self { + output: String::new(), + } + } + + pub fn output(&mut self) -> &str { + self.output.push('}'); + &self.output + } + + fn print_property<'gc>( + &mut self, + object: &Object<'gc>, + key: AvmString<'gc>, + activation: &mut Activation<'_, 'gc>, + ) { + match object.get(key, activation) { + Ok(value) => { + self.print_value(&value, activation); + } + Err(e) => { + self.output.push_str("\"Error\": \""); + self.output.push_str(&e.to_string()); + self.output.push('\"'); + } + } + } + + fn print_properties<'gc>( + &mut self, + object: &Object<'gc>, + activation: &mut Activation<'_, 'gc>, + ) { + let keys = object.get_keys(activation, false); + if keys.is_empty() { + self.output.push_str("{}"); + } else { + self.output.push('{'); + + let mut b = false; + for key in keys.into_iter() { + if b { + self.output.push(','); + } else { + b = true; + } + self.output.push('\"'); + self.output.push_str(&key.to_utf8_lossy()); + self.output.push_str("\":"); + self.print_property(object, key, activation); + } + + self.output.push('}'); + } + } + + fn print_value<'gc>(&mut self, value: &Value<'gc>, activation: &mut Activation<'_, 'gc>) { + match value { + Value::Undefined => self.output.push_str("[]"), // "undefined" is not valid json, {} is for empty objects, [] ? (is not conflicting with "NewObject" for "Array") + Value::Null => self.output.push_str("null"), + Value::Bool(value) => self.output.push_str(&value.to_string()), + Value::Number(value) => self.output.push_str(&value.to_string()), + Value::String(value) => { + print_string!(self, *value); + } + Value::Object(object) => { + self.print_properties(object, activation); + } + Value::MovieClip(_) => { + let obj = value.coerce_to_object(activation); + self.print_properties(&obj, activation); //object's properties + } + } + } + + fn print_variables<'gc>( + &mut self, + name: &str, + object: &Object<'gc>, + activation: &mut Activation<'_, 'gc>, + ) { + let keys = object.get_keys(activation, false); + if keys.is_empty() { + return; + } + + if self.output != "" { + self.output.push(','); + } else { + self.output.push('{'); + } + self.output.push('\"'); + self.output.push_str(name); + self.output.push_str("\":{"); + + let mut b = false; + for key in keys.into_iter() { + if b { + self.output.push(','); + } else { + b = true; + } + + let _ = write!(self.output, "\"{key}\":"); + self.print_property(object, key, activation); + } + + self.output.push('}'); + } + + pub fn print_activation<'gc>(&mut self, activation: &mut Activation<'_, 'gc>) { + self.print_variables( + "_global", + &activation.context.avm1.global_object(), + activation, + ); + for display_object in activation.context.stage.iter_render_list() { + let level = display_object.depth(); + let object = display_object.object().coerce_to_object(activation); + self.print_variables(&format!("_level{level}"), &object, activation); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/avm1/runtime.rs b/core/src/avm1/runtime.rs index b83d3d6b6db72..e6fd287be12ab 100644 --- a/core/src/avm1/runtime.rs +++ b/core/src/avm1/runtime.rs @@ -16,6 +16,7 @@ use gc_arena::{Collect, Gc, Mutation}; use std::borrow::Cow; use swf::avm1::read::Reader; use tracing::instrument; +use web_sys::console; #[derive(Collect)] #[collect(no_drop)] @@ -87,6 +88,10 @@ pub struct Avm1<'gc> { #[cfg(feature = "avm_debug")] pub debug_output: bool, + + pub output_json: i8, + pub output_json_stdin: bool, + pub output_json_code: u8, } impl<'gc> Avm1<'gc> { @@ -118,6 +123,10 @@ impl<'gc> Avm1<'gc> { #[cfg(feature = "avm_debug")] debug_output: false, use_new_invalid_bounds_value: false, + + output_json: -1, + output_json_stdin: false, + output_json_code: 0xFF, } } @@ -350,6 +359,18 @@ impl<'gc> Avm1<'gc> { }); avm_debug!(self, "Stack pop {}: {value:?}", self.stack.len()); + if self.output_json == 1 { + match value { + Value::Bool(b) => { + if self.output_json_stdin { + println!("{}", b); + } else { + console::log_1(&b.into()); + } + } + _ => (), + } + } value } diff --git a/core/src/player.rs b/core/src/player.rs index fb9ce8f17e0b0..bafe0f67c9072 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1245,6 +1245,18 @@ impl Player { } } + pub fn avm_output_json(&mut self, switch: i8) { + self.mutate_with_update_context(|context| { + context.avm1.output_json = switch; + }); + } + + pub fn avm_output_json_code(&mut self, opcode: u8) { + self.mutate_with_update_context(|context| { + context.avm1.output_json_code = opcode; + }); + } + /// Updates the hover state of buttons. fn update_mouse_state(&mut self, is_mouse_button_changed: bool, is_mouse_moved: bool) -> bool { let mut new_cursor = self.mouse_cursor; diff --git a/desktop/src/cli.rs b/desktop/src/cli.rs index d7c2366e838ff..92bd35798bea7 100644 --- a/desktop/src/cli.rs +++ b/desktop/src/cli.rs @@ -195,6 +195,9 @@ pub struct Opt { value_name = "GAMEPAD BUTTON>=, + + #[clap(long, action)] + pub avm_output_json: bool, } fn parse_movie_file_or_url(path: &str) -> Result { diff --git a/desktop/src/player.rs b/desktop/src/player.rs index 92d15830d84a7..616249757975a 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -57,6 +57,7 @@ pub struct PlayerOptions { pub open_url_mode: OpenURLMode, pub dummy_external_interface: bool, pub gamepad_button_mapping: HashMap, + pub avm_output_json: bool, } impl From<&GlobalPreferences> for PlayerOptions { @@ -85,6 +86,7 @@ impl From<&GlobalPreferences> for PlayerOptions { socket_allowed: HashSet::from_iter(value.cli.socket_allow.iter().cloned()), tcp_connections: value.cli.tcp_connections, gamepad_button_mapping: HashMap::from_iter(value.cli.gamepad_button.iter().cloned()), + avm_output_json: value.cli.avm_output_json, } } } @@ -269,6 +271,13 @@ impl ActivePlayer { "Arial Unicode MS".into(), // Mac fallback ], ); + + player_lock.mutate_with_update_context(|context| { + if opt.avm_output_json { + context.avm1.output_json = 1; + context.avm1.output_json_stdin = true; + } + }); } Self { player, executor } diff --git a/swf/Cargo.toml b/swf/Cargo.toml index a81d7940b005f..a6240444c9620 100644 --- a/swf/Cargo.toml +++ b/swf/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true workspace = true [dependencies] -bitflags = "2.4.2" +bitflags = { version = "2.4.2", features = ["serde"] } bitstream-io = "2.2.0" byteorder = "1.5" encoding_rs = "0.8.33" @@ -24,6 +24,7 @@ flate2 = {version = "1.0", optional = true} lzma-rs = {version = "0.3.0", optional = true } enum-map = "2.7.3" simple_asn1 = "0.6.2" +serde = { version = "1.0.197", features = ["derive"] } [features] default = ["flate2", "lzma"] diff --git a/swf/src/avm1/types.rs b/swf/src/avm1/types.rs index d6f9d669a7d79..709c65597d3db 100644 --- a/swf/src/avm1/types.rs +++ b/swf/src/avm1/types.rs @@ -1,8 +1,9 @@ use crate::string::SwfStr; use bitflags::bitflags; +use serde::Serialize; use std::num::NonZeroU8; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub enum Action<'a> { Add, Add2, @@ -107,19 +108,19 @@ pub enum Action<'a> { Unknown(Unknown<'a>), } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct ConstantPool<'a> { pub strings: Vec<&'a SwfStr>, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct DefineFunction<'a> { pub name: &'a SwfStr, pub params: Vec<&'a SwfStr>, pub actions: &'a [u8], } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct DefineFunction2<'a> { pub name: &'a SwfStr, pub register_count: u8, @@ -149,14 +150,14 @@ impl<'a> From> for DefineFunction2<'a> { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct FunctionParam<'a> { pub name: &'a SwfStr, pub register_index: Option, } bitflags! { - #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] pub struct FunctionFlags: u16 { const PRELOAD_THIS = 1 << 0; const SUPPRESS_THIS = 1 << 1; @@ -170,13 +171,13 @@ bitflags! { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct GetUrl<'a> { pub url: &'a SwfStr, pub target: &'a SwfStr, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct GetUrl2(pub(crate) GetUrlFlags); impl GetUrl2 { @@ -237,7 +238,7 @@ impl GetUrl2 { bitflags! { // NOTE: The GetURL2 flag layout is listed backwards in the SWF19 specs. - #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] pub(crate) struct GetUrlFlags: u8 { const METHOD_NONE = 0; const METHOD_GET = 1; @@ -257,38 +258,38 @@ pub enum SendVarsMethod { Post = 2, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct GotoFrame { pub frame: u16, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct GotoFrame2 { pub set_playing: bool, pub scene_offset: u16, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct GotoLabel<'a> { pub label: &'a SwfStr, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct If { pub offset: i16, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct Jump { pub offset: i16, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct Push<'a> { pub values: Vec>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub enum Value<'a> { Undefined, Null, @@ -301,24 +302,24 @@ pub enum Value<'a> { ConstantPool(u16), } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct SetTarget<'a> { pub target: &'a SwfStr, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] pub struct StoreRegister { pub register: u8, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct Try<'a> { pub try_body: &'a [u8], pub catch_body: Option<(CatchVar<'a>, &'a [u8])>, pub finally_body: Option<&'a [u8]>, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub enum CatchVar<'a> { Var(&'a SwfStr), Register(u8), @@ -332,24 +333,30 @@ bitflags! { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct WaitForFrame { pub frame: u16, pub num_actions_to_skip: u8, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct WaitForFrame2 { pub num_actions_to_skip: u8, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct With<'a> { pub actions: &'a [u8], } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct Unknown<'a> { pub opcode: u8, pub data: &'a [u8], } + +impl Action<'_> { + pub fn discriminant(&self) -> u8 { + unsafe { *<*const _>::from(self).cast::() } + } +} diff --git a/swf/src/string.rs b/swf/src/string.rs index a8caddeab4967..cbeb66c96f801 100644 --- a/swf/src/string.rs +++ b/swf/src/string.rs @@ -1,5 +1,7 @@ //! String type used by SWF files. +use serde::Serialize; + pub use encoding_rs::{Encoding, SHIFT_JIS, UTF_8, WINDOWS_1252}; use std::{borrow::Cow, fmt}; @@ -12,7 +14,7 @@ use std::{borrow::Cow, fmt}; /// any conversions to std::String will be lossy for invalid data. /// /// To convert this to a standard Rust string, use [`SwfStr::to_str_lossy`]. -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Serialize)] #[repr(transparent)] pub struct SwfStr { /// The string bytes. diff --git a/web/packages/core/src/ruffle-player.ts b/web/packages/core/src/ruffle-player.ts index 93720419aeb78..a189222b72251 100644 --- a/web/packages/core/src/ruffle-player.ts +++ b/web/packages/core/src/ruffle-player.ts @@ -2375,6 +2375,18 @@ export class RufflePlayer extends HTMLElement { // TODO: Move this to whatever function changes the ReadyState to Loaded when we have streaming support. this.dispatchEvent(new CustomEvent(RufflePlayer.LOADED_DATA)); } + + avmOutputJson(switcher: number): void { + if (this.instance) { + this.instance.avm_output_json(switcher); + } + } + + avmOutputJsonCode(code: number): void { + if (this.instance) { + this.instance.avm_output_json_code(code); + } + } } /** diff --git a/web/src/lib.rs b/web/src/lib.rs index 42d28983b5add..855b6d4f256dd 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -589,6 +589,18 @@ impl Ruffle { pub fn is_wasm_simd_used() -> bool { cfg!(target_feature = "simd128") } + + pub fn avm_output_json(&mut self, switch: i8) { + let _ = self.with_core_mut(|core| { + core.avm_output_json(switch); + }); + } + + pub fn avm_output_json_code(&mut self, opcode: u8) { + let _ = self.with_core_mut(|core| { + core.avm_output_json_code(opcode); + }); + } } impl Ruffle {