diff --git a/core/build_playerglobal/asc.jar b/core/build_playerglobal/asc.jar index ed8c939cd..c6bb306b1 100644 Binary files a/core/build_playerglobal/asc.jar and b/core/build_playerglobal/asc.jar differ diff --git a/core/build_playerglobal/src/lib.rs b/core/build_playerglobal/src/lib.rs index 4de0da1f4..6b132961f 100644 --- a/core/build_playerglobal/src/lib.rs +++ b/core/build_playerglobal/src/lib.rs @@ -10,11 +10,19 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; use swf::avm2::types::*; +use swf::avm2::write::Writer; use swf::DoAbc; use swf::Header; use swf::SwfStr; use swf::Tag; +// The metadata name - all metadata in our .as files +// should be of the form `[Ruffle(key1 = value1, key2 = value2)]` +const RUFFLE_METADATA_NAME: &str = "Ruffle"; +// Indicates that we should generate a reference to an instance allocator +// method (used as a metadata key with `Ruffle` metadata) +const METADATA_INSTANCE_ALLOCATOR: &str = "InstanceAllocator"; + /// If successful, returns a list of paths that were used. If this is run /// from a build script, these paths should be printed with /// cargo:rerun-if-changed @@ -49,14 +57,14 @@ pub fn build_playerglobal( } let playerglobal = out_dir.join("playerglobal"); - let bytes = std::fs::read(playerglobal.with_extension("abc"))?; + let mut bytes = std::fs::read(playerglobal.with_extension("abc"))?; // Cleanup the temporary files written out by 'asc.jar' std::fs::remove_file(playerglobal.with_extension("abc"))?; std::fs::remove_file(playerglobal.with_extension("cpp"))?; std::fs::remove_file(playerglobal.with_extension("h"))?; - write_native_table(&bytes, &out_dir)?; + bytes = write_native_table(&bytes, &out_dir)?; let tags = vec![Tag::DoAbc(DoAbc { name: SwfStr::from_utf8_str(""), @@ -95,6 +103,91 @@ fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str } } +fn flash_to_rust_path(path: &str) -> String { + // Convert each component of the path to snake-case. + // This correctly handles sequences of upper-case letters, + // so 'URLLoader' becomes 'url_loader' + let components = path + .split('.') + .map(|component| component.to_case(Case::Snake)) + .collect::>(); + // Form a Rust path from the snake-case components + components.join("::") +} + +fn rust_method_path( + abc: &AbcFile, + trait_: &Trait, + parent: Option>, + prefix: &str, + suffix: &str, +) -> TokenStream { + let mut path = "crate::avm2::globals::".to_string(); + + let trait_name = &abc.constant_pool.multinames[trait_.name.0 as usize - 1]; + + if let Some(parent) = parent { + // This is a method defined inside the class. Append the class namespace + // (the package) and the class name. + // For example, a namespace of "flash.system" and a name of "Security" + // turns into the path "flash::system::security" + let multiname = &abc.constant_pool.multinames[parent.0 as usize - 1]; + path += &flash_to_rust_path(resolve_multiname_ns(&abc, multiname)); + path += "::"; + path += &flash_to_rust_path(resolve_multiname_name(&abc, multiname)); + path += "::"; + } else { + // This is a freestanding function. Append its namespace (the package). + // For example, the freestanding function "flash.utils.getDefinitionByName" + // has a namespace of "flash.utils", which turns into the path + // "flash::utils" + path += &flash_to_rust_path(resolve_multiname_ns(&abc, trait_name)); + path += "::"; + } + + // Append the trait name - this corresponds to the actual method + // name (e.g. `getDefinitionByName`) + path += prefix; + + path += &flash_to_rust_path(resolve_multiname_name(&abc, trait_name)); + + path += suffix; + + // Now that we've built up the path, convert it into a `TokenStream`. + // This gives us something like + // `crate::avm2::globals::flash::system::Security::allowDomain` + // + // The resulting `TokenStream` is suitable for usage with `quote!` to + // generate a reference to the function pointer that should exist + // at that path in Rust code. + let path_tokens = TokenStream::from_str(&path).unwrap(); + quote! { Some(#path_tokens) } +} + +fn strip_metadata(abc: &mut AbcFile) { + abc.metadata.clear(); + for instance in &mut abc.instances { + for trait_ in &mut instance.traits { + trait_.metadata.clear(); + } + } + for class in &mut abc.classes { + for trait_ in &mut class.traits { + trait_.metadata.clear(); + } + } + for script in &mut abc.scripts { + for trait_ in &mut script.traits { + trait_.metadata.clear(); + } + } + for body in &mut abc.method_bodies { + for trait_ in &mut body.traits { + trait_.metadata.clear(); + } + } +} + /// Handles native functons defined in our `playerglobal` /// /// The high-level idea is to generate code (specifically, a `TokenStream`) @@ -109,12 +202,16 @@ fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str /// /// See `flash.system.Security.allowDomain` for an example of defining /// and using a native method. -fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box> { +/// +/// Returns a modified version of 'data' that should be saved to disk +/// in our generated SWF +fn write_native_table(data: &[u8], out_dir: &Path) -> Result, Box> { let mut reader = swf::avm2::read::Reader::new(data); - let abc = reader.read()?; + let mut abc = reader.read()?; let none_tokens = quote! { None }; - let mut rust_paths = vec![none_tokens; abc.methods.len()]; + let mut rust_paths = vec![none_tokens.clone(); abc.methods.len()]; + let mut rust_instance_allocators = vec![none_tokens; abc.classes.len()]; let mut check_trait = |trait_: &Trait, parent: Option>| { let method_id = match trait_.kind { @@ -145,55 +242,55 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box "", }; - let flash_to_rust_path = |path: &str| { - // Convert each component of the path to snake-case. - // This correctly handles sequences of upper-case letters, - // so 'URLLoader' becomes 'url_loader' - let components = path - .split('.') - .map(|component| component.to_case(Case::Snake)) - .collect::>(); - // Form a Rust path from the snake-case components - components.join("::") + rust_paths[method_id.0 as usize] = + rust_method_path(&abc, trait_, parent, method_prefix, ""); + }; + + // Look for `[Ruffle(InstanceAllocator)]` metadata - if present, + // generate a reference to an allocator function in the native instance + // allocators table. + let mut check_instance_allocator = |trait_: &Trait| { + let class_id = if let TraitKind::Class { slot_id, .. } = trait_.kind { + slot_id + } else { + return; }; - let mut path = "crate::avm2::globals::".to_string(); + let class_name_idx = abc.instances[class_id as usize - 1].name.0; + let class_name = resolve_multiname_name( + &abc, + &abc.constant_pool.multinames[class_name_idx as usize - 1], + ); - let trait_name = &abc.constant_pool.multinames[trait_.name.0 as usize - 1]; + let method_name = "::".to_string() + &flash_to_rust_path(class_name) + "_allocator"; - if let Some(parent) = parent { - // This is a method defined inside the class. Append the class namespace - // (the package) and the class name. - // For example, a namespace of "flash.system" and a name of "Security" - // turns into the path "flash::system::security" - let multiname = &abc.constant_pool.multinames[parent.0 as usize - 1]; - path += &flash_to_rust_path(resolve_multiname_ns(&abc, multiname)); - path += "::"; - path += &flash_to_rust_path(resolve_multiname_name(&abc, multiname)); - path += "::"; - } else { - // This is a freestanding function. Append its namespace (the package). - // For example, the freestanding function "flash.utils.getDefinitionByName" - // has a namespace of "flash.utils", which turns into the path - // "flash::utils" - path += &flash_to_rust_path(resolve_multiname_ns(&abc, trait_name)); - path += "::"; + for metadata_idx in &trait_.metadata { + let metadata = &abc.metadata[metadata_idx.0 as usize]; + let name = &abc.constant_pool.strings[metadata.name.0 as usize - 1]; + match name.as_str() { + RUFFLE_METADATA_NAME => {} + _ => panic!("Unexpected class metadata {:?}", name), + } + + for item in &metadata.items { + let key = if item.key.0 != 0 { + Some(abc.constant_pool.strings[item.key.0 as usize - 1].as_str()) + } else { + None + }; + let value = &abc.constant_pool.strings[item.value.0 as usize - 1]; + match (key, value.as_str()) { + // Match `[Ruffle(InstanceAllocator)]` + (None, METADATA_INSTANCE_ALLOCATOR) => { + // This results in a path of the form + // `crate::avm2::globals::::` + rust_instance_allocators[class_id as usize - 1] = + rust_method_path(&abc, trait_, None, "", &method_name); + } + _ => panic!("Unexpected metadata pair ({:?}, {})", key, value), + } + } } - - // Append the trait name - this corresponds to the actual method - // name (e.g. `getDefinitionByName`) - path += method_prefix; - path += &flash_to_rust_path(resolve_multiname_name(&abc, trait_name)); - - // Now that we've built up the path, convert it into a `TokenStream`. - // This gives us something like - // `crate::avm2::globals::flash::system::Security::allowDomain` - // - // The resulting `TokenStream` is suitable for usage with `quote!` to - // generate a reference to the function pointer that should exist - // at that path in Rust code. - let path_tokens = TokenStream::from_str(&path).unwrap(); - rust_paths[method_id.0 as usize] = quote! { Some(#path_tokens) }; }; // We support three kinds of native methods: @@ -216,29 +313,37 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box] = &[ + // This is a Rust array - + // the entry at index `i` is a Rust function pointer for the native + // method with id `i`. Not all methods in playerglobal will be native + // methods, so we store `None` in the entries corresponding to non-native + // functions. We expect the majority of the methods in playerglobal to be + // native, so this should only waste a small amount of memory. + // + // If a function pointer doesn't exist at the expected path, + // then Ruffle compilation will fail + // with an error message that mentions the non-existent path. + // + // When we initially load a method from an ABC file, we check if it's from our playerglobal, + // and if its ID exists in this table. + // If so, we replace it with a `NativeMethod` constructed + // from the function pointer we looked up in the table. + pub const NATIVE_METHOD_TABLE: &[Option] = &[ #(#rust_paths,)* ]; + + // This is very similar to `NATIVE_METHOD_TABLE`, but we have one entry per + // class, rather than per method. When an entry is `Some(fn_ptr)`, we use + // `fn_ptr` as the instance allocator for the corresponding class when we + // load it into Ruffle. + pub const NATIVE_INSTANCE_ALLOCATOR_TABLE: &[Option] = &[ + #(#rust_instance_allocators,)* + ]; } .to_string(); @@ -249,5 +354,13 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box { system_classes: Option>, #[collect(require_static)] - native_table: &'static [Option], + native_method_table: &'static [Option], + + #[collect(require_static)] + native_instance_allocator_table: &'static [Option], /// A list of objects which are capable of recieving broadcasts. /// @@ -101,7 +105,8 @@ impl<'gc> Avm2<'gc> { stack: Vec::new(), globals, system_classes: None, - native_table: Default::default(), + native_method_table: Default::default(), + native_instance_allocator_table: Default::default(), broadcast_list: Default::default(), #[cfg(feature = "avm_debug")] diff --git a/core/src/avm2/class.rs b/core/src/avm2/class.rs index 920ca45c8..9a7a6d64e 100644 --- a/core/src/avm2/class.rs +++ b/core/src/avm2/class.rs @@ -354,6 +354,16 @@ impl<'gc> Class<'gc> { attributes.set(ClassAttributes::FINAL, abc_instance.is_final); attributes.set(ClassAttributes::INTERFACE, abc_instance.is_interface); + let mut instance_allocator = None; + + // When loading a class from our playerglobal, grab the corresponding native + // allocator function from the table (which may be `None`) + if unit.domain().is_avm2_global_domain(activation) { + instance_allocator = activation.avm2().native_instance_allocator_table + [class_index as usize] + .map(Allocator); + } + Ok(GcCell::allocate( activation.context.gc_context, Self { @@ -363,7 +373,7 @@ impl<'gc> Class<'gc> { attributes, protected_namespace, interfaces, - instance_allocator: None, + instance_allocator, instance_init, native_instance_init, instance_traits: Vec::new(), diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 9bb6c7e4f..11b4475d3 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -210,9 +210,10 @@ fn class<'gc>( let class_read = class_def.read(); let super_class = if let Some(sc_name) = class_read.super_class_name() { - let super_class: Result, Error> = global - .get_property(sc_name, activation) + let super_class: Result, Error> = activation + .resolve_definition(sc_name) .ok() + .and_then(|v| v) .and_then(|v| v.as_object()) .ok_or_else(|| { format!( @@ -441,13 +442,6 @@ pub fn load_player_globals<'gc>( )?; class(activation, flash::system::system::create_class(mc), script)?; - // package `flash.events` - avm2_system_class!( - event, - activation, - flash::events::event::create_class(mc), - script - ); class( activation, flash::events::ieventdispatcher::create_interface(mc), @@ -458,62 +452,6 @@ pub fn load_player_globals<'gc>( flash::events::eventdispatcher::create_class(mc), script, )?; - avm2_system_class!( - mouseevent, - activation, - flash::events::mouseevent::create_class(mc), - script - ); - avm2_system_class!( - textevent, - activation, - flash::events::textevent::create_class(mc), - script - ); - avm2_system_class!( - errorevent, - activation, - flash::events::errorevent::create_class(mc), - script - ); - avm2_system_class!( - securityerrorevent, - activation, - flash::events::securityerrorevent::create_class(mc), - script - ); - avm2_system_class!( - ioerrorevent, - activation, - flash::events::ioerrorevent::create_class(mc), - script - ); - class( - activation, - flash::events::contextmenuevent::create_class(mc), - script, - )?; - class( - activation, - flash::events::keyboardevent::create_class(mc), - script, - )?; - class( - activation, - flash::events::progressevent::create_class(mc), - script, - )?; - class( - activation, - flash::events::activityevent::create_class(mc), - script, - )?; - avm2_system_class!( - fullscreenevent, - activation, - flash::events::fullscreenevent::create_class(mc), - script - ); class( activation, flash::events::eventphase::create_class(mc), @@ -750,10 +688,64 @@ pub fn load_player_globals<'gc>( )?; // Inside this call, the macro `avm2_system_classes_playerglobal` - // triggers classloading. Therefore, we run `load_playerglobal` last, - // so that all of our classes have been defined. + // triggers classloading. Therefore, we run `load_playerglobal` + // relative late, so that it can access classes defined before + // this call. load_playerglobal(activation, domain)?; + // These are event definitions, which need to be able to + // load "flash.events.Event", which is defined in our playerglobal. + // Therefore, they need to come after "load_playerglobal" + // FIXME: Convert all of these event classes to ActionScript, + // which will allow us to remove all of these calls. + + avm2_system_class!( + mouseevent, + activation, + flash::events::mouseevent::create_class(mc), + script + ); + avm2_system_class!( + textevent, + activation, + flash::events::textevent::create_class(mc), + script + ); + avm2_system_class!( + errorevent, + activation, + flash::events::errorevent::create_class(mc), + script + ); + avm2_system_class!( + securityerrorevent, + activation, + flash::events::securityerrorevent::create_class(mc), + script + ); + avm2_system_class!( + ioerrorevent, + activation, + flash::events::ioerrorevent::create_class(mc), + script + ); + class( + activation, + flash::events::keyboardevent::create_class(mc), + script, + )?; + class( + activation, + flash::events::progressevent::create_class(mc), + script, + )?; + avm2_system_class!( + fullscreenevent, + activation, + flash::events::fullscreenevent::create_class(mc), + script + ); + Ok(()) } @@ -761,8 +753,9 @@ pub fn load_player_globals<'gc>( /// See that tool, and 'core/src/avm2/globals/README.md', for more details const PLAYERGLOBAL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/playerglobal.swf")); -// This defines a const named `NATIVE_TABLE` -include!(concat!(env!("OUT_DIR"), "/native_table.rs")); +mod native { + include!(concat!(env!("OUT_DIR"), "/native_table.rs")); +} /// Loads classes from our custom 'playerglobal' (which are written in ActionScript) /// into the environment. See 'core/src/avm2/globals/README.md' for more information @@ -770,7 +763,8 @@ fn load_playerglobal<'gc>( activation: &mut Activation<'_, 'gc, '_>, domain: Domain<'gc>, ) -> Result<(), Error> { - activation.avm2().native_table = NATIVE_TABLE; + activation.avm2().native_method_table = native::NATIVE_METHOD_TABLE; + activation.avm2().native_instance_allocator_table = native::NATIVE_INSTANCE_ALLOCATOR_TABLE; let movie = Arc::new(SwfMovie::from_data(PLAYERGLOBAL, None, None)?); @@ -805,7 +799,14 @@ fn load_playerglobal<'gc>( // This acts the same way as 'avm2_system_class', but for classes // declared in 'playerglobal'. Classes are declared as ("package", "class", field_name), // and are stored in 'avm2().system_classes' - avm2_system_classes_playerglobal!(activation, script, [("flash.display", "Scene", scene)]); + avm2_system_classes_playerglobal!( + activation, + script, + [ + ("flash.display", "Scene", scene), + ("flash.events", "Event", event), + ] + ); Ok(()) } diff --git a/core/src/avm2/globals/README.md b/core/src/avm2/globals/README.md index b2806610f..04bd3faeb 100644 --- a/core/src/avm2/globals/README.md +++ b/core/src/avm2/globals/README.md @@ -52,6 +52,17 @@ The ActionScript method and the Rust function are automatically linked together, and the Rust function will be invoked when the corresponding function is called from ActionScript. +## Custom instance allocator + +You can use a custom instance allocator method by applying the metadata +`[Ruffle(InstanceAllocator)]` +to your class definition. A reference to a function named `_allocator` +will be generated - this should be an `AllocatorFn`, just like when defining +a class in Rust. This allocator will automatically be registered when the corresponding +class is loaded. + +See `flash/events/Event.as` for an example + ## Compiling Java must be installed for the build process to complete. diff --git a/core/src/avm2/globals/flash/display/InteractiveObject.as b/core/src/avm2/globals/flash/display/InteractiveObject.as new file mode 100644 index 000000000..e9569fa94 --- /dev/null +++ b/core/src/avm2/globals/flash/display/InteractiveObject.as @@ -0,0 +1,5 @@ +// This is a stub - the actual class is defined in `interactiveobject.rs` +package flash.display { + public class InteractiveObject { + } +} diff --git a/core/src/avm2/globals/flash/events.rs b/core/src/avm2/globals/flash/events.rs index 23cf5bd85..67a34ec51 100644 --- a/core/src/avm2/globals/flash/events.rs +++ b/core/src/avm2/globals/flash/events.rs @@ -1,7 +1,5 @@ //! `flash.events` namespace -pub mod activityevent; -pub mod contextmenuevent; pub mod errorevent; pub mod event; pub mod eventdispatcher; diff --git a/core/src/avm2/globals/flash/events/ActivityEvent.as b/core/src/avm2/globals/flash/events/ActivityEvent.as new file mode 100644 index 000000000..31ecec567 --- /dev/null +++ b/core/src/avm2/globals/flash/events/ActivityEvent.as @@ -0,0 +1,19 @@ +package flash.events { + import flash.events.Event; + public class ActivityEvent extends Event { + public var activating:Boolean; + + public function ActivityEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, activating:Boolean = false) { + super(type, bubbles, cancelable); + this.activating = activating; + } + + override public function clone() : Event { + return new ActivityEvent(this.type, this.bubbles, this.cancelable, this.activating); + } + + override public function toString(): String { + return formatToString("ActivityEvent","type","bubbles","cancelable","eventPhase","activating"); + } + } +} diff --git a/core/src/avm2/globals/flash/events/ContextMenuEvent.as b/core/src/avm2/globals/flash/events/ContextMenuEvent.as new file mode 100644 index 000000000..ef051eead --- /dev/null +++ b/core/src/avm2/globals/flash/events/ContextMenuEvent.as @@ -0,0 +1,49 @@ +package flash.events { + import flash.display.InteractiveObject; + public class ContextMenuEvent extends Event { + public static const MENU_ITEM_SELECT:String = "menuItemSelect"; + public static const MENU_SELECT:String = "menuSelect"; + + private var _mouseTarget:InteractiveObject; + private var _contextMenuOwner:InteractiveObject; + private var _isMouseTargetInaccessible:Boolean; + + public function ContextMenuEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, mouseTarget:InteractiveObject = null, contextMenuOwner:InteractiveObject = null) { + super(type,bubbles,cancelable); + this._mouseTarget = mouseTarget; + this._contextMenuOwner = contextMenuOwner; + } + + override public function clone() : Event { + return new ContextMenuEvent(this.type, this.bubbles, this.cancelable, this._mouseTarget, this._contextMenuOwner); + } + + override public function toString() : String { + return this.formatToString("ContextMenuEvent","type","bubbles","cancelable","eventPhase","mouseTarget","contextMenuOwner"); + } + + public function get mouseTarget() : InteractiveObject { + return this._mouseTarget; + } + + public function set mouseTarget(value:InteractiveObject) : void { + this._mouseTarget = value; + } + + public function get contextMenuOwner() : InteractiveObject { + return this._contextMenuOwner; + } + + public function set contextMenuOwner(value:InteractiveObject) : void { + this._contextMenuOwner = value; + } + + public function get isMouseTargetInaccessible() : Boolean { + return this._isMouseTargetInaccessible; + } + + public function set isMouseTargetInaccessible(value:Boolean) : void { + this._isMouseTargetInaccessible = value; + } + } +} diff --git a/core/src/avm2/globals/flash/events/Event.as b/core/src/avm2/globals/flash/events/Event.as new file mode 100644 index 000000000..672b98fbb --- /dev/null +++ b/core/src/avm2/globals/flash/events/Event.as @@ -0,0 +1,129 @@ +package flash.events { + [Ruffle(InstanceAllocator)] + public class Event { + public static const ACTIVATE:String = "activate"; + + public static const ADDED:String = "added"; + + public static const ADDED_TO_STAGE:String = "addedToStage"; + + public static const BROWSER_ZOOM_CHANGE:String = "browserZoomChange"; + + public static const CANCEL:String = "cancel"; + + public static const CHANGE:String = "change"; + + public static const CLEAR:String = "clear"; + + public static const CLOSE:String = "close"; + + public static const COMPLETE:String = "complete"; + + public static const CONNECT:String = "connect"; + + public static const COPY:String = "copy"; + + public static const CUT:String = "cut"; + + public static const DEACTIVATE:String = "deactivate"; + + public static const ENTER_FRAME:String = "enterFrame"; + + public static const FRAME_CONSTRUCTED:String = "frameConstructed"; + + public static const EXIT_FRAME:String = "exitFrame"; + + public static const FRAME_LABEL:String = "frameLabel"; + + public static const ID3:String = "id3"; + + public static const INIT:String = "init"; + + public static const MOUSE_LEAVE:String = "mouseLeave"; + + public static const OPEN:String = "open"; + + public static const PASTE:String = "paste"; + + public static const REMOVED:String = "removed"; + + public static const REMOVED_FROM_STAGE:String = "removedFromStage"; + + public static const RENDER:String = "render"; + + public static const RESIZE:String = "resize"; + + public static const SCROLL:String = "scroll"; + + public static const TEXT_INTERACTION_MODE_CHANGE:String = "textInteractionModeChange"; + + public static const SELECT:String = "select"; + + public static const SELECT_ALL:String = "selectAll"; + + public static const SOUND_COMPLETE:String = "soundComplete"; + + public static const TAB_CHILDREN_CHANGE:String = "tabChildrenChange"; + + public static const TAB_ENABLED_CHANGE:String = "tabEnabledChange"; + + public static const TAB_INDEX_CHANGE:String = "tabIndexChange"; + + public static const UNLOAD:String = "unload"; + + public static const FULLSCREEN:String = "fullScreen"; + + public static const CONTEXT3D_CREATE:String = "context3DCreate"; + + public static const TEXTURE_READY:String = "textureReady"; + + public static const VIDEO_FRAME:String = "videoFrame"; + + public static const SUSPEND:String = "suspend"; + + public static const CHANNEL_MESSAGE:String = "channelMessage"; + + public static const CHANNEL_STATE:String = "channelState"; + + public static const WORKER_STATE:String = "workerState"; + + public function Event(type:String, bubbles:Boolean = false, cancelable:Boolean = false) { + this.init(type, bubbles, cancelable); + } + + private native function init(type:String, bubbles:Boolean = false, cancelable:Boolean = false):void; + + public native function get bubbles():Boolean; + public native function get cancelable():Boolean; + public native function get currentTarget():Object; + public native function get eventPhase():uint; + public native function get target():Object; + public native function get type():String; + + public function clone():Event { + return new Event(this.type, this.bubbles, this.cancelable); + } + + public function toString(): String { + return this.formatToString("Event","type","bubbles","cancelable","eventPhase"); + } + + public function formatToString(className:String, ... arguments):String { + var fmt = "[" + className; + for each (var key: String in arguments) { + var val = this[key]; + if(val is String) { + fmt += " " + key + "=\"" + val + "\""; + } else { + fmt += " " + key + "=" + val; + } + } + return fmt += "]"; + } + + public native function isDefaultPrevented(): Boolean; + public native function preventDefault():void; + public native function stopPropagation():void; + public native function stopImmediatePropagation():void; + } +} diff --git a/core/src/avm2/globals/flash/events/activityevent.rs b/core/src/avm2/globals/flash/events/activityevent.rs deleted file mode 100644 index 3489bf970..000000000 --- a/core/src/avm2/globals/flash/events/activityevent.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::avm2::activation::Activation; -use crate::avm2::class::{Class, ClassAttributes}; -use crate::avm2::method::{Method, NativeMethodImpl}; -use crate::avm2::names::{Namespace, QName}; -use crate::avm2::object::Object; -use crate::avm2::value::Value; -use crate::avm2::Error; -use gc_arena::{GcCell, MutationContext}; - -/// Implements `flash.events.ActivityEvent`'s instance constructor. -pub fn instance_init<'gc>( - activation: &mut Activation<'_, 'gc, '_>, - this: Option>, - args: &[Value<'gc>], -) -> Result, Error> { - if let Some(this) = this { - activation.super_init(this, args)?; // Event uses the first three parameters - } - Ok(Value::Undefined) -} - -/// Implements `flash.events.ActivityEvent`'s class constructor. -pub fn class_init<'gc>( - _activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - Ok(Value::Undefined) -} - -pub fn activating<'gc>( - _activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - log::warn!("ActivityEvent.activating - not implemented"); - - Ok(Value::Undefined) -} - -pub fn set_activating<'gc>( - _activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - log::warn!("ActivityEvent.set_activating - not implemented"); - - Ok(Value::Undefined) -} - -/// Construct `ActivityEvent`'s class. -pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { - let class = Class::new( - QName::new(Namespace::package("flash.events"), "ActivityEvent"), - Some(QName::new(Namespace::package("flash.events"), "Event").into()), - Method::from_builtin(instance_init, "", mc), - Method::from_builtin(class_init, "", mc), - mc, - ); - - let mut write = class.write(mc); - - const PUBLIC_INSTANCE_PROPERTIES: &[( - &str, - Option, - Option, - )] = &[("activating", Some(activating), Some(set_activating))]; - write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES); - - write.set_attributes(ClassAttributes::SEALED); - - const CONSTANTS: &[(&str, &str)] = &[("ACTIVITY", "activity")]; - write.define_public_constant_string_class_traits(CONSTANTS); - - class -} diff --git a/core/src/avm2/globals/flash/events/contextmenuevent.rs b/core/src/avm2/globals/flash/events/contextmenuevent.rs deleted file mode 100644 index 5b49e7818..000000000 --- a/core/src/avm2/globals/flash/events/contextmenuevent.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::avm2::activation::Activation; -use crate::avm2::class::{Class, ClassAttributes}; -use crate::avm2::method::Method; -use crate::avm2::names::{Namespace, QName}; -use crate::avm2::object::Object; -use crate::avm2::value::Value; -use crate::avm2::Error; -use gc_arena::{GcCell, MutationContext}; - -/// Implements `flash.events.ContextMenuEvent`'s instance constructor. -pub fn instance_init<'gc>( - activation: &mut Activation<'_, 'gc, '_>, - this: Option>, - args: &[Value<'gc>], -) -> Result, Error> { - if let Some(this) = this { - activation.super_init(this, args)?; // ErrorEvent, Event use these - } - Ok(Value::Undefined) -} - -/// Implements `flash.events.ContextMenuEvent`'s class constructor. -pub fn class_init<'gc>( - _activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - Ok(Value::Undefined) -} - -/// Construct `ContextMenuEvent`'s class. -pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { - let class = Class::new( - QName::new(Namespace::package("flash.events"), "ContextMenuEvent"), - Some(QName::new(Namespace::package("flash.events"), "Event").into()), - Method::from_builtin(instance_init, "", mc), - Method::from_builtin(class_init, "", mc), - mc, - ); - - let mut write = class.write(mc); - - write.set_attributes(ClassAttributes::SEALED); - - const CONSTANTS: &[(&str, &str)] = &[ - ("MENU_ITEM_SELECT", "menuItemSelect"), - ("MENU_SELECT", "menuSelect"), - ]; - - write.define_public_constant_string_class_traits(CONSTANTS); - - class -} diff --git a/core/src/avm2/globals/flash/events/event.rs b/core/src/avm2/globals/flash/events/event.rs index 161724b6e..8c028c6a0 100644 --- a/core/src/avm2/globals/flash/events/event.rs +++ b/core/src/avm2/globals/flash/events/event.rs @@ -1,60 +1,42 @@ //! `flash.events.Event` builtin/prototype use crate::avm2::activation::Activation; -use crate::avm2::class::{Class, ClassAttributes}; -use crate::avm2::method::{Method, NativeMethodImpl}; -use crate::avm2::names::{Namespace, QName}; -use crate::avm2::object::{event_allocator, EventObject, Object, TObject}; +use crate::avm2::object::{Object, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; -use crate::string::AvmString; -use gc_arena::{GcCell, MutationContext}; -/// Implements `flash.events.Event`'s instance constructor. -pub fn instance_init<'gc>( +pub use crate::avm2::object::event_allocator; + +pub fn init<'gc>( activation: &mut Activation<'_, 'gc, '_>, this: Option>, args: &[Value<'gc>], ) -> Result, Error> { - if let Some(this) = this { - activation.super_init(this, &[])?; - - if let Some(mut evt) = this.as_event_mut(activation.context.gc_context) { - evt.set_event_type( - args.get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(activation)?, - ); - evt.set_bubbles( - args.get(1) - .cloned() - .unwrap_or(Value::Bool(false)) - .coerce_to_boolean(), - ); - evt.set_cancelable( - args.get(2) - .cloned() - .unwrap_or(Value::Bool(false)) - .coerce_to_boolean(), - ); - } - } - - Ok(Value::Undefined) -} - -/// Implements `flash.events.Event`'s class constructor. -pub fn class_init<'gc>( - _activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { + let this = this.unwrap(); + let mut evt = this.as_event_mut(activation.context.gc_context).unwrap(); + evt.set_event_type( + args.get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(activation)?, + ); + evt.set_bubbles( + args.get(1) + .cloned() + .unwrap_or(Value::Bool(false)) + .coerce_to_boolean(), + ); + evt.set_cancelable( + args.get(2) + .cloned() + .unwrap_or(Value::Bool(false)) + .coerce_to_boolean(), + ); Ok(Value::Undefined) } /// Implements `bubbles` property's getter -pub fn bubbles<'gc>( +pub fn get_bubbles<'gc>( _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], @@ -67,7 +49,7 @@ pub fn bubbles<'gc>( } /// Implements `cancelable` property's getter -pub fn cancelable<'gc>( +pub fn get_cancelable<'gc>( _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], @@ -93,7 +75,7 @@ pub fn get_type<'gc>( } /// Implements `target` property's getter -pub fn target<'gc>( +pub fn get_target<'gc>( _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], @@ -106,7 +88,7 @@ pub fn target<'gc>( } /// Implements `currentTarget` property's getter -pub fn current_target<'gc>( +pub fn get_current_target<'gc>( _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], @@ -122,7 +104,7 @@ pub fn current_target<'gc>( } /// Implements `eventPhase` property's getter -pub fn event_phase<'gc>( +pub fn get_event_phase<'gc>( _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], @@ -135,66 +117,6 @@ pub fn event_phase<'gc>( Ok(Value::Undefined) } -/// Implements `clone` -pub fn clone<'gc>( - activation: &mut Activation<'_, 'gc, '_>, - this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - if let Some(evt) = this.unwrap().as_event() { - return Ok(EventObject::from_event(activation, evt.clone())?.into()); - } - - Ok(Value::Undefined) -} - -/// Implements `formatToString` -pub fn format_to_string<'gc>( - activation: &mut Activation<'_, 'gc, '_>, - this: Option>, - args: &[Value<'gc>], -) -> Result, Error> { - use std::fmt::Write; - - if let Some(this) = this { - let class_name = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(activation)?; - - let mut stringified_params = String::new(); - if let Some(params) = args.get(1..) { - for param_name in params { - let param_name = QName::dynamic_name(match param_name { - Value::Undefined | Value::Null => "null".into(), - _ => param_name.coerce_to_string(activation)?, - }) - .into(); - - let param_value = this - .get_property(¶m_name, activation)? - .coerce_to_debug_string(activation)?; - write!( - stringified_params, - " {}={}", - param_name.local_name().unwrap(), - param_value - ) - .unwrap(); - } - } - - return Ok(AvmString::new_utf8( - activation.context.gc_context, - format!("[{}{}]", class_name, stringified_params), - ) - .into()); - } - - Ok(Value::Undefined) -} - /// Implements `isDefaultPrevented` pub fn is_default_prevented<'gc>( _activation: &mut Activation<'_, 'gc, '_>, @@ -246,134 +168,3 @@ pub fn stop_immediate_propagation<'gc>( Ok(Value::Undefined) } - -/// Implements `toString` -pub fn to_string<'gc>( - activation: &mut Activation<'_, 'gc, '_>, - this: Option>, - _args: &[Value<'gc>], -) -> Result, Error> { - if let Some(this) = this { - if let Some(event) = this.as_event() { - let event_type = event.event_type(); - let bubbles = event.is_bubbling(); - let cancelable = event.is_cancelable(); - let phase = event.phase() as u32; - - return Ok(AvmString::new_utf8( - activation.context.gc_context, - format!( - "[Event type=\"{}\" bubbles={} cancelable={} eventPhase={}]", - event_type, bubbles, cancelable, phase - ), - ) - .into()); - } - } - - Ok(Value::Undefined) -} - -/// Construct `Event`'s class. -pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { - let class = Class::new( - QName::new(Namespace::package("flash.events"), "Event"), - Some(QName::new(Namespace::public(), "Object").into()), - Method::from_builtin(instance_init, "", mc), - Method::from_builtin(class_init, "", mc), - mc, - ); - - let mut write = class.write(mc); - - write.set_attributes(ClassAttributes::SEALED); - write.set_instance_allocator(event_allocator); - - const PUBLIC_INSTANCE_PROPERTIES: &[( - &str, - Option, - Option, - )] = &[ - ("bubbles", Some(bubbles), None), - ("cancelable", Some(cancelable), None), - ("type", Some(get_type), None), - ("target", Some(target), None), - ("currentTarget", Some(current_target), None), - ("eventPhase", Some(event_phase), None), - ]; - write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES); - - const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[ - ("clone", clone), - ("formatToString", format_to_string), - ("isDefaultPrevented", is_default_prevented), - ("preventDefault", prevent_default), - ("stopPropagation", stop_propagation), - ("stopImmediatePropagation", stop_immediate_propagation), - ("toString", to_string), - ]; - write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS); - - const CONSTANTS: &[(&str, &str)] = &[ - ("ACTIVATE", "activate"), - ("ADDED", "added"), - ("ADDED_TO_STAGE", "addedToStage"), - ("BROWSER_ZOOM_CHANGE", "browserZoomChange"), - ("CANCEL", "cancel"), - ("CHANGE", "change"), - ("CHANNEL_MESSAGE", "channelMessage"), - ("CHANNEL_STATE", "channelState"), - ("CLEAR", "clear"), - ("CLOSE", "close"), - ("CLOSING", "closing"), - ("COMPLETE", "complete"), - ("CONNECT", "connect"), - ("CONTEXT3D_CREATE", "context3DCreate"), - ("COPY", "copy"), - ("CUT", "cut"), - ("DEACTIVATE", "deactivate"), - ("DISPLAYING", "displaying"), - ("ENTER_FRAME", "enterFrame"), - ("EXIT_FRAME", "exitFrame"), - ("EXITING", "exiting"), - ("FRAME_CONSTRUCTED", "frameConstructed"), - ("FRAME_LABEL", "frameLabel"), - ("FULLSCREEN", "fullScreen"), - ("HTML_BOUNDS_CHANGE", "htmlBoundsChange"), - ("HTML_DOM_INITIALIZE", "htmlDOMInitialize"), - ("HTML_RENDER", "htmlRender"), - ("ID3", "id3"), - ("INIT", "init"), - ("LOCATION_CHANGE", "locationChange"), - ("MOUSE_LEAVE", "mouseLeave"), - ("NETWORK_CHANGE", "networkChange"), - ("OPEN", "open"), - ("PASTE", "paste"), - ("PREPARING", "preparing"), - ("REMOVED", "removed"), - ("REMOVED_FROM_STAGE", "removedFromStage"), - ("RENDER", "render"), - ("RESIZE", "resize"), - ("SCROLL", "scroll"), - ("SELECT", "select"), - ("SELECT_ALL", "selectAll"), - ("SOUND_COMPLETE", "soundComplete"), - ("STANDARD_ERROR_CLOSE", "standardErrorClose"), - ("STANDARD_INPUT_CLOSE", "standardInputClose"), - ("STANDARD_OUTPUT_CLOSE", "standardOutputClose"), - ("SUSPEND", "suspend"), - ("TAB_CHILDREN_CHANGE", "tabChildrenChange"), - ("TAB_ENABLED_CHANGE", "tabEnabledChange"), - ("TAB_INDEX_CHANGE", "tabIndexChange"), - ("TEXT_INTERACTION_MODE_CHANGE", "textInteractionModeChange"), - ("TEXTURE_READY", "textureReady"), - ("UNLOAD", "unload"), - ("USER_IDLE", "userIdle"), - ("USER_PRESENT", "userPresent"), - ("VIDEO_FRAME", "videoFrame"), - ("WORKER_STATE", "workerState"), - ]; - write.define_public_constant_string_class_traits(CONSTANTS); - - class -} diff --git a/core/src/avm2/globals/flash/events/mouseevent.rs b/core/src/avm2/globals/flash/events/mouseevent.rs index caba47448..2ba6a5807 100644 --- a/core/src/avm2/globals/flash/events/mouseevent.rs +++ b/core/src/avm2/globals/flash/events/mouseevent.rs @@ -17,7 +17,9 @@ pub fn instance_init<'gc>( args: &[Value<'gc>], ) -> Result, Error> { if let Some(this) = this { - activation.super_init(this, args)?; + // Get up to three arguments + let event_args = &args[..(std::cmp::min(args.len(), 2))]; + activation.super_init(this, event_args)?; if let Some(mut evt) = this.as_event_mut(activation.context.gc_context) { // This is technically duplicative of `Event`'s initializer, but // we have different default parameters. diff --git a/core/src/avm2/globals/globals.as b/core/src/avm2/globals/globals.as index 9bf8cbcf9..f0031dead 100644 --- a/core/src/avm2/globals/globals.as +++ b/core/src/avm2/globals/globals.as @@ -1,4 +1,6 @@ -// List is ordered alphabetically. +// List is ordered alphabetically, except where superclasses +// need to come before subclasses. + include "flash/accessibility/AccessibilityProperties.as" include "flash/display/ActionScriptVersion.as" include "flash/display/BitmapDataChannel.as" @@ -30,6 +32,12 @@ include "flash/display/StageQuality.as" include "flash/display/StageScaleMode.as" include "flash/display/SWFVersion.as" include "flash/display/TriangleCulling.as" + +// Event needs to come before its subclasses +include "flash/events/Event.as" +include "flash/events/ActivityEvent.as" +include "flash/events/ContextMenuEvent.as" + include "flash/geom/ColorTransform.as" include "flash/geom/Orientation3D.as" include "flash/geom/Matrix.as" diff --git a/core/src/avm2/globals/stubs.as b/core/src/avm2/globals/stubs.as index 903f37082..9df649775 100644 --- a/core/src/avm2/globals/stubs.as +++ b/core/src/avm2/globals/stubs.as @@ -6,6 +6,7 @@ include "Object.as" // List is ordered alphabetically. include "Array.as" include "Boolean.as" +include "flash/display/InteractiveObject.as" include "flash/events/EventDispatcher.as" include "Number.as" include "String.as" diff --git a/core/src/avm2/script.rs b/core/src/avm2/script.rs index 780b2b502..1c6bc91cc 100644 --- a/core/src/avm2/script.rs +++ b/core/src/avm2/script.rs @@ -140,7 +140,8 @@ impl<'gc> TranslationUnit<'gc> { // allowing us to use 'bc_method' later on without a borrow-checker error. let method = (|| { if is_global { - if let Some(native) = activation.avm2().native_table[method_index.0 as usize] { + if let Some(native) = activation.avm2().native_method_table[method_index.0 as usize] + { let variadic = bc_method.is_variadic(); return Method::from_builtin_and_params( native,