avm2: Allow implementing classes in ActionScript in custom `playerglobal`

This PR adds support for building a custom `playerglobal.swf`, which can be used
to implement builtin Flash classes in ActionScript. This file is embedded into Ruffle
using `include_bytes!`, and loaded during initialization.
As an example, the `Point` class is reimplemented
in ActionScript, and `flash.text.AntiAliasType` is added.

The ActionScript compilation process is performed by `core/build.rs`.
See that file, along with `core/src/avm2/globals/README.md`, for
more details.
This commit is contained in:
Aaron Hill 2022-06-15 14:00:17 -05:00 committed by GitHub
parent b8cdd59877
commit f629a91e53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 402 additions and 412 deletions

9
Cargo.lock generated
View File

@ -251,6 +251,14 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
[[package]]
name = "build_playerglobal"
version = "0.1.0"
dependencies = [
"swf",
"walkdir",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.9.1" version = "3.9.1"
@ -2992,6 +3000,7 @@ dependencies = [
"approx", "approx",
"bitflags", "bitflags",
"bitstream-io", "bitstream-io",
"build_playerglobal",
"byteorder", "byteorder",
"chrono", "chrono",
"dasp", "dasp",

View File

@ -2,6 +2,7 @@
members = [ members = [
"core", "core",
"core/macros", "core/macros",
"core/build_playerglobal",
"desktop", "desktop",
"swf", "swf",
"web", "web",

View File

@ -39,6 +39,8 @@ For more detailed instructions, see our [wiki page](https://github.com/ruffle-rs
[Follow the official guide](https://www.rust-lang.org/tools/install) to install Rust for your platform. [Follow the official guide](https://www.rust-lang.org/tools/install) to install Rust for your platform.
You must also have Java installed, and available on your PATH as `java`.
### Desktop ### Desktop
If you are building for a Linux platform, make sure that the GTK 3 development packages are If you are building for a Linux platform, make sure that the GTK 3 development packages are

View File

@ -72,3 +72,6 @@ lzma = ["lzma-rs", "swf/lzma"]
wasm-bindgen = [ "instant/wasm-bindgen" ] wasm-bindgen = [ "instant/wasm-bindgen" ]
avm_debug = [] avm_debug = []
deterministic = [] deterministic = []
[build-dependencies]
build_playerglobal = { path = "build_playerglobal" }

10
core/build.rs Normal file
View File

@ -0,0 +1,10 @@
fn main() {
build_playerglobal::build_playerglobal("../".into(), std::env::var("OUT_DIR").unwrap().into())
.expect("Failed to build playerglobal");
// This is overly conservative - it will cause us to rebuild playerglobal.swf
// if *any* files in this directory change, not just .as files.
// However, this script is fast to run, so it shouldn't matter in practice.
// If Cargo ever adds glob support to 'rerun-if-changed', we should use it.
println!("cargo:rerun-if-changed=src/avm2/globals/");
}

View File

@ -0,0 +1,10 @@
[package]
name = "build_playerglobal"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
swf = { path = "../../swf" }
walkdir = "2.3.2"

Binary file not shown.

View File

@ -0,0 +1,91 @@
//! An internal Ruffle utility to build our playerglobal
//! `library.swf`
use std::collections::HashSet;
use std::fs::File;
use std::path::PathBuf;
use std::process::Command;
use swf::DoAbc;
use swf::Header;
use swf::SwfStr;
use swf::Tag;
use walkdir::WalkDir;
/// 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
pub fn build_playerglobal(
repo_root: PathBuf,
out_dir: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
let classes_dir = repo_root.join("core/src/avm2/globals/");
let asc_path = repo_root.join("core/build_playerglobal/asc.jar");
let out_path = out_dir.join("playerglobal.swf");
// These classes are currently stubs - they're referenced by
// other classes that we need to compile, but the real definition
// is in Ruffle itself (in Rust code).
// As a result, we don't emit them into the final SWF (but we do
// provide them to asc.jar with '-import' to link against).
let stub_classes: HashSet<_> = ["Object", "Number", "Boolean", "String"].into();
// This will create 'playerglobal.abc', 'playerglobal.cpp', and 'playerglobal.h'
// in `out_dir`
let mut cmd = Command::new("java");
cmd.args(&[
"-classpath",
&asc_path.to_string_lossy(),
"macromedia.asc.embedding.ScriptCompiler",
"-outdir",
&out_dir.to_string_lossy(),
"-out",
"playerglobal",
]);
for entry in WalkDir::new(&classes_dir) {
let entry = entry?;
if entry.path().extension().and_then(|e| e.to_str()) != Some("as") {
continue;
}
let class = entry.into_path();
let class_name: String = class
.strip_prefix(&classes_dir)?
.with_extension("")
.iter()
.map(|c| c.to_string_lossy())
.collect::<Vec<_>>()
.join("/");
if stub_classes.contains(class_name.as_str()) {
cmd.arg("-import");
}
cmd.arg(class);
}
println!("Compiling: {:?}", cmd);
let code = cmd.status()?;
if !code.success() {
return Err(format!("Compiling failed with code {:?}", code).into());
}
let playerglobal = out_dir.join("playerglobal");
let 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"))?;
let tags = vec![Tag::DoAbc(DoAbc {
name: SwfStr::from_utf8_str(""),
is_lazy_initialize: true,
data: &bytes,
})];
let header = Header::default_with_swf_version(19);
let out_file = File::create(out_path).unwrap();
swf::write_swf(&header, &tags, out_file)?;
Ok(())
}

View File

@ -0,0 +1,14 @@
//! Manually builds `playerglobal.swf` without building the `core` crate.
//! This binary is invoked as:
//! `cargo run --package=build_playerglobal <repo_root> <out_dir>`
//! where `<repo_root>` is the location of the Ruffle repository,
//! and `out_dir` is the directory where `playerglobal.swf` should
//! be written
fn main() {
build_playerglobal::build_playerglobal(
std::env::args().nth(1).unwrap().into(),
std::env::args().nth(2).unwrap().into(),
)
.unwrap();
}

View File

@ -6,11 +6,12 @@ use crate::avm2::object::EventObject;
use crate::avm2::script::{Script, TranslationUnit}; use crate::avm2::script::{Script, TranslationUnit};
use crate::context::UpdateContext; use crate::context::UpdateContext;
use crate::string::AvmString; use crate::string::AvmString;
use crate::tag_utils::SwfSlice; use crate::tag_utils::{SwfSlice, SwfStream};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use gc_arena::{Collect, MutationContext}; use gc_arena::{Collect, MutationContext};
use std::rc::Rc; use std::rc::Rc;
use swf::avm2::read::Reader; use swf::avm2::read::Reader;
use swf::extensions::ReadSwfExt;
#[macro_export] #[macro_export]
macro_rules! avm_debug { macro_rules! avm_debug {
@ -261,6 +262,37 @@ impl<'gc> Avm2<'gc> {
Ok(()) Ok(())
} }
pub fn load_abc_from_do_abc(
context: &mut UpdateContext<'_, 'gc, '_>,
swf: &SwfSlice,
domain: Domain<'gc>,
reader: &mut SwfStream<'_>,
tag_len: usize,
) -> Result<(), Error> {
let start = reader.as_slice();
// Queue the actions.
// TODO: The tag reader parses the entire ABC file, instead of just
// giving us a `SwfSlice` for later parsing, so we have to replcate the
// *entire* parsing code here. This sucks.
let flags = reader.read_u32()?;
let name = reader.read_str()?.to_string_lossy(reader.encoding());
let is_lazy_initialize = flags & 1 != 0;
let num_read = reader.pos(start);
// The rest of the tag is an ABC file so we can take our SwfSlice now.
let slice = swf
.resize_to_reader(reader, tag_len - num_read)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Invalid source or tag length when running init action",
)
})?;
Avm2::load_abc(slice, &name, is_lazy_initialize, context, domain)?;
Ok(())
}
/// Load an ABC file embedded in a `SwfSlice`. /// Load an ABC file embedded in a `SwfSlice`.
/// ///
/// The `SwfSlice` must resolve to the contents of an ABC file. /// The `SwfSlice` must resolve to the contents of an ABC file.

View File

@ -9,9 +9,13 @@ use crate::avm2::object::{
use crate::avm2::scope::{Scope, ScopeChain}; use crate::avm2::scope::{Scope, ScopeChain};
use crate::avm2::script::Script; use crate::avm2::script::Script;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::Avm2;
use crate::avm2::Error; use crate::avm2::Error;
use crate::string::AvmString; use crate::string::AvmString;
use crate::tag_utils::{self, SwfMovie, SwfSlice, SwfStream};
use gc_arena::{Collect, GcCell, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
use std::sync::Arc;
use swf::TagCode;
mod array; mod array;
mod boolean; mod boolean;
@ -522,6 +526,8 @@ pub fn load_player_globals<'gc>(
// After this point, it is safe to initialize any other classes. // After this point, it is safe to initialize any other classes.
// Make sure to initialize superclasses *before* their subclasses! // Make sure to initialize superclasses *before* their subclasses!
load_playerglobal(activation, domain)?;
avm2_system_class!(string, activation, string::create_class(mc), script); avm2_system_class!(string, activation, string::create_class(mc), script);
avm2_system_class!(boolean, activation, boolean::create_class(mc), script); avm2_system_class!(boolean, activation, boolean::create_class(mc), script);
avm2_system_class!(number, activation, number::create_class(mc), script); avm2_system_class!(number, activation, number::create_class(mc), script);
@ -900,12 +906,6 @@ pub fn load_player_globals<'gc>(
// package `flash.geom` // package `flash.geom`
class(activation, flash::geom::matrix::create_class(mc), script)?; class(activation, flash::geom::matrix::create_class(mc), script)?;
avm2_system_class!(
point,
activation,
flash::geom::point::create_class(mc),
script
);
avm2_system_class!( avm2_system_class!(
rectangle, rectangle,
activation, activation,
@ -1034,3 +1034,54 @@ pub fn load_player_globals<'gc>(
Ok(()) Ok(())
} }
/// This file is built by 'core/build_playerglobal/'
/// See that tool, and 'core/src/avm2/globals/README.md', for more details
const PLAYERGLOBAL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/playerglobal.swf"));
/// Loads classes from our custom 'playerglobal' (which are written in ActionScript)
/// into the environment. See 'core/src/avm2/globals/README.md' for more information
fn load_playerglobal<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
domain: Domain<'gc>,
) -> Result<(), Error> {
let movie = Arc::new(SwfMovie::from_data(PLAYERGLOBAL, None, None)?);
let slice = SwfSlice::from(movie);
let mut reader = slice.read_from(0);
let tag_callback = |reader: &mut SwfStream<'_>, tag_code, tag_len| {
if tag_code == TagCode::DoAbc {
Avm2::load_abc_from_do_abc(&mut activation.context, &slice, domain, reader, tag_len)?;
} else if tag_code != TagCode::End {
panic!(
"playerglobal should only contain `DoAbc` tag - found tag {:?}",
tag_code
)
}
Ok(())
};
let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::End);
macro_rules! avm2_system_classes_playerglobal {
($activation:expr, $script:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => {
$(
let qname = QName::new(Namespace::package($package), $class_name);
let class_object = activation.resolve_class(&qname.into())?;
let sc = $activation.avm2().system_classes.as_mut().unwrap();
sc.$field = class_object;
let sp = $activation.avm2().system_prototypes.as_mut().unwrap();
sp.$field = class_object.prototype();
)*
}
}
// 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' and 'avm2().system_prototypes'
avm2_system_classes_playerglobal!(activation, script, [("flash.geom", "Point", point),]);
Ok(())
}

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is defined in `boolean.rs`
package {
public class Boolean {
}
}

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is defined in `number.rs`
package {
public class Number {
}
}

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is defined in `object.rs`
package {
public dynamic class Object {
}
}

View File

@ -0,0 +1,56 @@
# Ruffle AVM2 globals
This directory contains implementations of Flash global
definitions (e.g. `Object`, `flash.geom.Point`, `flash.utils.getDefinitionByName`)
WARNING: Do *not* implement classes by copying their (decompiled) ActionScript
from the Adobe Flash `playerglobal.swf`. This would violate copyright by making
Ruffle re-distribute Adobe's code (and will not even work in general, since
Adobe's `playerglobal.swf` uses native methods that Ruffle doesn't implement).
Currently, globals are implemented in one of two ways:
1) As pure-Rust code in files like `system.rs`. These are normal Rust
modules, and are used from `core/src/avm2/global.rs`
2) As ActionScript class definitions like `flash/geom/Point.as`.
These files are automatically compiled into a `playerglobal.swf`
file at build time, which is included into the final Ruffle binary
and loaded during player initialization.
In many cases, defining a class in ActionScript results in
code that's much simpler and more readable than if were
defined in Rust.
Flash's `playerglobal.swc` (specifically, its `library.swf`)
cannot be used as a drop-in replacement for our `playerglobal.swf`.
In addition to potential copyright issues around redistributing Flash's `playerglobal.swc`,
many of its classes rely on specific 'native' methods being provided
by the Flash VM, which Ruffle does not implement.
## Compiling
Java must be installed for the build process to complete.
ActionScript classes are processed by the `core/build\_playerglobal`
The tool first uses 'asc.jar'
from the Flex SDK to compile these ActionScript files into
ABC (bytecode). This tool is entirely self-contained, so we can
include in our repository without requiring the entire Flex SDK
to be installed.
The produced ABC files are then combined into the final
`playerglobal.swf` (which is written out into the build directory,
and not checked in to Git).
The `core/build\_playerglobal` tool is automatically run by `core`'s build script
when any of our ActionScript classes are changed.
## Limitations
* Only pure ActionScript classes are currently supported. Classes with
'native' methods are not yet supported.
* 'Special' classes which are loaded early during Ruffle initialization
(e.g. 'Object', 'Function', 'Class') cannot currently
be implemented in 'playerglobal', since Ruffle initializes them in a special
way. However, virtually all classes in the 'flash' package are initialized
in a 'normal' way, and are eligible for implementation in 'playerglobal'

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is defined in `string.rs`
package {
public class String {
}
}

View File

@ -1,5 +1,4 @@
//! `flash.geom` namespace //! `flash.geom` namespace
pub mod matrix; pub mod matrix;
pub mod point;
pub mod rectangle; pub mod rectangle;

View File

@ -0,0 +1,71 @@
package flash.geom {
public class Point {
public var x:Number;
public var y:Number;
public function Point(x:Number = 0, y:Number = 0) {
this.x = x;
this.y = y;
}
public function get length():Number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
public function toString():String {
return "(x=" + this.x + ", y=" + this.y + ")";
}
public function add(v:Point):Point {
return new Point(this.x + v.x, this.y + v.y);
}
public function subtract(v:Point):Point {
return new Point(this.x - v.x, this.y - v.y);
}
public function clone():Point {
return new Point(this.x, this.y);
}
public function copyFrom(sourcePoint:Point):void {
this.x = sourcePoint.x;
this.y = sourcePoint.y;
}
public function equals(toCompare:Point):Boolean {
return this.x == toCompare.x && this.y == toCompare.y;
}
public function normalize(thickness:Number):void {
var len:Number = this.length;
if (len > 0) {
var inv_d:Number = thickness / len;
this.x *= inv_d;
this.y *= inv_d;
}
}
public function offset(dx:Number, dy:Number):void {
this.x += dx;
this.y += dy;
}
public function setTo(xa:Number, ya:Number):void {
this.x = xa;
this.y = ya;
}
public static function distance(pt1:Point, pt2:Point):Number {
return pt2.subtract(pt1).length;
}
public static function interpolate(pt1:Point, pt2:Point, f:Number):Point {
return new Point(pt2.x - (pt2.x - pt1.x) * f, pt2.y - (pt2.y - pt1.y) * f);
}
public static function polar(len:Number, angle:Number):Point {
return new Point(len* Math.cos(angle), len * Math.sin(angle));
}
}
}

View File

@ -1,379 +0,0 @@
//! `flash.geom.Point` builtin/prototype
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::{Activation, Error, Namespace, Object, QName, TObject, Value};
use crate::string::AvmString;
use gc_arena::{GcCell, MutationContext};
pub fn create_point<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
coords: (f64, f64),
) -> Result<Value<'gc>, Error> {
let point_class = activation.context.avm2.classes().point;
let args = [Value::Number(coords.0), Value::Number(coords.1)];
let new_point = point_class.construct(activation, &args)?;
Ok(new_point.into())
}
/// Implements `flash.geom.Point`'s instance constructor.
pub fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let _ = set_to(activation, this, args)?;
Ok(Value::Undefined)
}
fn coords<'gc>(
this: &mut Object<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<(f64, f64), Error> {
let x = this
.get_property(&QName::new(Namespace::public(), "x").into(), activation)?
.coerce_to_number(activation)?;
let y = this
.get_property(&QName::new(Namespace::public(), "y").into(), activation)?
.coerce_to_number(activation)?;
Ok((x, y))
}
fn set_coords<'gc>(
this: &mut Object<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
value: (f64, f64),
) -> Result<(), Error> {
this.set_property(
&QName::new(Namespace::public(), "x").into(),
value.0.into(),
activation,
)?;
this.set_property(
&QName::new(Namespace::public(), "y").into(),
value.1.into(),
activation,
)?;
Ok(())
}
/// Implements `flash.geom.Point`'s class initializer.
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
Ok(Value::Undefined)
}
/// Implements the `length` property
pub fn length<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
let (x, y) = coords(&mut this, activation)?;
return Ok((x * x + y * y).sqrt().into());
}
Ok(Value::Undefined)
}
/// Implements `add`
pub fn add<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
if let Some(other) = args.get(0) {
let mut other_obj = other.coerce_to_object(activation)?;
let (our_x, our_y) = coords(&mut this, activation)?;
let (their_x, their_y) = coords(&mut other_obj, activation)?;
return create_point(activation, (our_x + their_x, our_y + their_y));
}
}
Ok(Value::Undefined)
}
/// Implements `clone`
pub fn clone<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
let (our_x, our_y) = coords(&mut this, activation)?;
return create_point(activation, (our_x, our_y));
}
Ok(Value::Undefined)
}
/// Implements `copyFrom`
pub fn copy_from<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
if let Some(other) = args.get(0) {
let mut other_obj = other.coerce_to_object(activation)?;
let (their_x, their_y) = coords(&mut other_obj, activation)?;
set_coords(&mut this, activation, (their_x, their_y))?;
}
}
Ok(Value::Undefined)
}
/// Implements `distance`
pub fn distance<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(first) = args.get(0) {
let mut first_object = first.coerce_to_object(activation)?;
if let Some(second) = args.get(1) {
let mut second_obj = second.coerce_to_object(activation)?;
let (our_x, our_y) = coords(&mut first_object, activation)?;
let (their_x, their_y) = coords(&mut second_obj, activation)?;
return Ok(((our_x - their_x).powf(2.0) + (our_y - their_y).powf(2.0))
.sqrt()
.into());
}
}
Ok(Value::Undefined)
}
/// Implements `equals`
#[allow(clippy::float_cmp)]
pub fn equals<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
if let Some(other) = args.get(0) {
let mut other_obj = other.coerce_to_object(activation)?;
let (our_x, our_y) = coords(&mut this, activation)?;
let (their_x, their_y) = coords(&mut other_obj, activation)?;
return Ok((our_x == their_x && our_y == their_y).into());
}
}
Ok(Value::Undefined)
}
/// Implements `interpolate`
pub fn interpolate<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if args.len() < 3 {
return create_point(activation, (f64::NAN, f64::NAN));
}
let (a_x, a_y) = coords(
&mut args.get(0).unwrap().coerce_to_object(activation)?,
activation,
)?;
let (b_x, b_y) = coords(
&mut args.get(1).unwrap().coerce_to_object(activation)?,
activation,
)?;
let f = args.get(2).unwrap().coerce_to_number(activation)?;
let result = (b_x - (b_x - a_x) * f, b_y - (b_y - a_y) * f);
create_point(activation, result)
}
/// Implements `normalize`
pub fn normalize<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
let thickness = args
.get(0)
.unwrap_or(&0.into())
.coerce_to_number(activation)?;
let length = length(activation, Some(this), args)?.coerce_to_number(activation)?;
if length > 0.0 {
let inv_d = thickness / length;
let (old_x, old_y) = coords(&mut this, activation)?;
set_coords(&mut this, activation, (old_x * inv_d, old_y * inv_d))?;
}
}
Ok(Value::Undefined)
}
/// Implements `offset`
pub fn offset<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
let (x, y) = coords(&mut this, activation)?;
let dx = args
.get(0)
.unwrap_or(&0.into())
.coerce_to_number(activation)?;
let dy = args
.get(1)
.unwrap_or(&0.into())
.coerce_to_number(activation)?;
set_coords(&mut this, activation, (x + dx, y + dy))?;
}
Ok(Value::Undefined)
}
/// Implements `polar`
pub fn polar<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let length = args
.get(0)
.unwrap_or(&Value::Undefined)
.coerce_to_number(activation)?;
let angle = args
.get(1)
.unwrap_or(&Value::Undefined)
.coerce_to_number(activation)?;
create_point(activation, (length * angle.cos(), length * angle.sin()))
}
/// Implements `setTo`
pub fn set_to<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
let x = args
.get(0)
.unwrap_or(&0.into())
.coerce_to_number(activation)?;
let y = args
.get(1)
.unwrap_or(&0.into())
.coerce_to_number(activation)?;
set_coords(&mut this, activation, (x, y))?;
}
Ok(Value::Undefined)
}
/// Implements `subtract`
pub fn subtract<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mut this) = this {
if let Some(other) = args.get(0) {
let mut other_obj = other.coerce_to_object(activation)?;
let (our_x, our_y) = coords(&mut this, activation)?;
let (their_x, their_y) = coords(&mut other_obj, activation)?;
return create_point(activation, (our_x - their_x, our_y - their_y));
}
}
Ok(Value::Undefined)
}
/// Implements `toString`
pub fn to_string<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
let x = this
.get_property(&QName::new(Namespace::public(), "x").into(), activation)?
.coerce_to_string(activation)?;
let y = this
.get_property(&QName::new(Namespace::public(), "y").into(), activation)?
.coerce_to_string(activation)?;
return Ok(AvmString::new_utf8(
activation.context.gc_context,
format!("(x={}, y={})", x, y),
)
.into());
}
Ok(Value::Undefined)
}
/// Construct `Point`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.geom"), "Point"),
Some(QName::new(Namespace::public(), "Object").into()),
Method::from_builtin(instance_init, "<Point instance initializer>", mc),
Method::from_builtin(class_init, "<Point class initializer>", mc),
mc,
);
let mut write = class.write(mc);
write.set_attributes(ClassAttributes::SEALED);
const PUBLIC_INSTANCE_PROPERTIES: &[(
&str,
Option<NativeMethodImpl>,
Option<NativeMethodImpl>,
)] = &[("length", Some(length), None)];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
const PUBLIC_INSTANCE_NUMBER_SLOTS: &[(&str, Option<f64>)] = &[("x", None), ("y", None)];
write.define_public_slot_number_instance_traits(PUBLIC_INSTANCE_NUMBER_SLOTS);
const PUBLIC_CLASS_METHODS: &[(&str, NativeMethodImpl)] = &[
("distance", distance),
("interpolate", interpolate),
("polar", polar),
];
write.define_public_builtin_class_methods(mc, PUBLIC_CLASS_METHODS);
const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[
("add", add),
("clone", clone),
("copyFrom", copy_from),
("equals", equals),
("normalize", normalize),
("offset", offset),
("setTo", set_to),
("subtract", subtract),
("toString", to_string),
];
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);
class
}

View File

@ -1,12 +1,23 @@
//! `flash.geom.Rectangle` builtin/prototype //! `flash.geom.Rectangle` builtin/prototype
use crate::avm2::class::{Class, ClassAttributes}; use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::globals::flash::geom::point::create_point;
use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::{Activation, Error, Namespace, Object, QName, TObject, Value}; use crate::avm2::{Activation, Error, Namespace, Object, QName, TObject, Value};
use crate::string::AvmString; use crate::string::AvmString;
use gc_arena::{GcCell, MutationContext}; use gc_arena::{GcCell, MutationContext};
pub fn create_point<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
coords: (f64, f64),
) -> Result<Value<'gc>, Error> {
let point_class = activation.context.avm2.classes().point;
let args = [Value::Number(coords.0), Value::Number(coords.1)];
let new_point = point_class.construct(activation, &args)?;
Ok(new_point.into())
}
pub fn create_rectangle<'gc>( pub fn create_rectangle<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
properties: (f64, f64, f64, f64), properties: (f64, f64, f64, f64),

View File

@ -0,0 +1,6 @@
package flash.text {
public final class AntiAliasType {
public static const ADVANCED:String = "advanced";
public static const NORMAL:String = "normal";
}
}

View File

@ -502,32 +502,15 @@ impl<'gc> MovieClip<'gc> {
return Ok(()); return Ok(());
} }
let start = reader.as_slice();
// Queue the actions.
// TODO: The tag reader parses the entire ABC file, instead of just
// giving us a `SwfSlice` for later parsing, so we have to replcate the
// *entire* parsing code here. This sucks.
let flags = reader.read_u32()?;
let name = reader.read_str()?.to_string_lossy(reader.encoding());
let is_lazy_initialize = flags & 1 != 0;
let domain = context.library.library_for_movie_mut(movie).avm2_domain(); let domain = context.library.library_for_movie_mut(movie).avm2_domain();
let num_read = reader.pos(start);
// The rest of the tag is an ABC file so we can take our SwfSlice now. if let Err(e) = Avm2::load_abc_from_do_abc(
let slice = self context,
.0 &self.0.read().static_data.swf,
.read() domain,
.static_data reader,
.swf tag_len,
.resize_to_reader(reader, tag_len - num_read) ) {
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Invalid source or tag length when running init action",
)
})?;
if let Err(e) = Avm2::load_abc(slice, &name, is_lazy_initialize, context, domain) {
log::warn!("Error loading ABC file: {}", e); log::warn!("Error loading ABC file: {}", e);
} }