Repository: ruffle-rs/ruffle (language: rust)

Issue:
RegExp valueOf returns a string instead of object
RegExp.prototype.valueOf currently returns a string (similar to toString) instead of returning the RegExp object. Actually I think we need to add an actual RegExp.prototype.valueOf method, instead of just implementing the [`value_of`](https://github.com/ruffle-rs/ruffle/blob/0466605b62609f47f69a2a97b25e2a9b3a37cb64/core/src/avm2/object/regexp_object.rs#L90) method of the TObject trait.

Testcase: `typeof /abc/.valueOf()`



Discussion / hints:

Fix patch:
diff --git a/core/src/avm2.rs b/core/src/avm2.rs
index 446f334965b16..ad35cdea5df3f 100644
--- a/core/src/avm2.rs
+++ b/core/src/avm2.rs
@@ -3,9 +3,11 @@
 use std::rc::Rc;
 
 use crate::avm2::class::{AllocatorFn, CustomConstructorFn};
+use crate::avm2::e4x::XmlSettings;
 use crate::avm2::error::{make_error_1014, make_error_1107, type_error, Error1014Type};
 use crate::avm2::globals::{
-    init_builtin_system_classes, init_native_system_classes, SystemClassDefs, SystemClasses,
+    init_builtin_system_class_defs, init_builtin_system_classes, init_native_system_classes,
+    SystemClassDefs, SystemClasses,
 };
 use crate::avm2::method::{Method, NativeMethodImpl};
 use crate::avm2::scope::ScopeChain;
@@ -175,6 +177,9 @@ pub struct Avm2<'gc> {
     alias_to_class_map: FnvHashMap<AvmString<'gc>, ClassObject<'gc>>,
     class_to_alias_map: FnvHashMap<Class<'gc>, AvmString<'gc>>,
 
+    #[collect(require_static)]
+    pub xml_settings: XmlSettings,
+
     /// The api version of our root movie clip. Note - this is used as the
     /// api version for swfs loaded via `Loader`, overriding the api version
     /// specified in the loaded SWF. This is only used for API versioning (hiding
@@ -230,6 +235,8 @@ impl<'gc> Avm2<'gc> {
             alias_to_class_map: Default::default(),
             class_to_alias_map: Default::default(),
 
+            xml_settings: XmlSettings::new_default(),
+
             // Set the lowest version for now - this will be overridden when we set our movie
             root_api_version: ApiVersion::AllVersions,
 
@@ -410,7 +417,7 @@ impl<'gc> Avm2<'gc> {
     /// Returns `true` if the event has been handled.
     pub fn dispatch_event(
         context: &mut UpdateContext<'gc>,
-        event: Object<'gc>,
+        event: EventObject<'gc>,
         target: Object<'gc>,
     ) -> bool {
         Self::dispatch_event_internal(context, event, target, false)
@@ -424,7 +431,7 @@ impl<'gc> Avm2<'gc> {
     /// Returns `true` when the event would have been handled if not simulated.
     pub fn simulate_event_dispatch(
         context: &mut UpdateContext<'gc>,
-        event: Object<'gc>,
+        event: EventObject<'gc>,
         target: Object<'gc>,
     ) -> bool {
         Self::dispatch_event_internal(context, event, target, true)
@@ -432,18 +439,15 @@ impl<'gc> Avm2<'gc> {
 
     fn dispatch_event_internal(
         context: &mut UpdateContext<'gc>,
-        event: Object<'gc>,
+        event: EventObject<'gc>,
         target: Object<'gc>,
         simulate_dispatch: bool,
     ) -> bool {
-        let event_name = event
-            .as_event()
-            .map(|e| e.event_type())
-            .unwrap_or_else(|| panic!("cannot dispatch non-event object: {:?}", event));
-
         let mut activation = Activation::from_nothing(context);
         match events::dispatch_event(&mut activation, target, event, simulate_dispatch) {
             Err(err) => {
+                let event_name = event.event().event_type();
+
                 tracing::error!(
                     "Encountered AVM2 error when dispatching `{}` event: {:?}",
                     event_name,
@@ -503,13 +507,10 @@ impl<'gc> Avm2<'gc> {
     /// Attempts to broadcast a non-event object will panic.
     pub fn broadcast_event(
         context: &mut UpdateContext<'gc>,
-        event: Object<'gc>,
+        event: EventObject<'gc>,
         on_type: ClassObject<'gc>,
     ) {
-        let event_name = event
-            .as_event()
-            .map(|e| e.event_type())
-            .unwrap_or_else(|| panic!("cannot broadcast non-event object: {:?}", event));
+        let event_name = event.event().event_type();
 
         if !BROADCAST_WHITELIST
             .iter()
@@ -697,6 +698,10 @@ impl<'gc> Avm2<'gc> {
             .load_classes(&mut activation)
             .expect("Classes should load");
 
+        // These Classes are absolutely critical to the runtime, so make sure
+        // we've registered them before anything else.
+        init_builtin_system_class_defs(&mut activation);
+
         // The second script (script #1) is Toplevel.as, and includes important
         // builtin classes such as Namespace, QName, and XML.
         tunit
@@ -753,25 +758,7 @@ impl<'gc> Avm2<'gc> {
 
     /// Push a value onto the operand stack.
     #[inline(always)]
-    fn push(&mut self, mut value: Value<'gc>) {
-        if let Value::Object(o) = value {
-            // this is hot, so let's avoid a non-inlined call here
-            if let Object::PrimitiveObject(_) = o {
-                if let Some(prim) = o.as_primitive() {
-                    value = *prim;
-                }
-            }
-        }
-
-        avm_debug!(self, "Stack push {}: {value:?}", self.stack.len());
-
-        self.push_internal(value);
-    }
-
-    /// Push a value onto the operand stack.
-    /// This is like `push`, but does not handle `PrimitiveObject`.
-    #[inline(always)]
-    fn push_raw(&mut self, value: Value<'gc>) {
+    fn push(&mut self, value: Value<'gc>) {
         avm_debug!(self, "Stack push {}: {value:?}", self.stack.len());
 
         self.push_internal(value);
diff --git a/core/src/avm2/activation.rs b/core/src/avm2/activation.rs
index 89b1d4f322288..7fa2c0c96aca6 100644
--- a/core/src/avm2/activation.rs
+++ b/core/src/avm2/activation.rs
@@ -279,15 +279,18 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     pub fn find_definition(
         &mut self,
         name: &Multiname<'gc>,
-    ) -> Result<Option<Object<'gc>>, Error<'gc>> {
+    ) -> Result<Option<Value<'gc>>, Error<'gc>> {
         let outer_scope = self.outer;
 
-        if let Some(obj) = search_scope_stack(self.scope_frame(), name, outer_scope.is_empty())? {
+        if let Some(obj) = search_scope_stack(self, name, outer_scope.is_empty())? {
             Ok(Some(obj))
         } else if let Some(obj) = outer_scope.find(name, self)? {
             Ok(Some(obj))
         } else if let Some(global) = self.global_scope() {
-            if global.base().has_own_dynamic_property(name) {
+            if global
+                .as_object()
+                .is_some_and(|o| o.base().has_own_dynamic_property(name))
+            {
                 Ok(Some(global))
             } else {
                 Ok(None)
@@ -392,11 +395,11 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         &mut self,
         method: Gc<'gc, BytecodeMethod<'gc>>,
         outer: ScopeChain<'gc>,
-        this: Object<'gc>,
+        this: Value<'gc>,
         user_arguments: &[Value<'gc>],
         bound_superclass_object: Option<ClassObject<'gc>>,
         bound_class: Option<Class<'gc>>,
-        callee: Object<'gc>,
+        callee: Value<'gc>,
     ) -> Result<(), Error<'gc>> {
         let body: Result<_, Error<'gc>> = method
             .body()
@@ -406,7 +409,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let has_rest_or_args = method.is_variadic();
 
         let mut local_registers = RegisterSet::new(num_locals + 1);
-        *local_registers.get_unchecked_mut(0) = this.into();
+        *local_registers.get_unchecked_mut(0) = this;
 
         let activation_class =
             BytecodeMethod::get_or_init_activation_class(method, self.gc(), || {
@@ -494,7 +497,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
                 .flags
                 .contains(AbcMethodFlags::NEED_ARGUMENTS)
             {
-                args_object.set_string_property_local("callee", callee.into(), self)?;
+                args_object.set_string_property_local("callee", callee, self)?;
                 args_object.set_local_property_is_enumerable(self.gc(), "callee".into(), false);
             }
 
@@ -539,16 +542,18 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     }
 
     /// Call the superclass's instance initializer.
+    ///
+    /// This method may panic if called with a Null or Undefined receiver.
     pub fn super_init(
         &mut self,
-        receiver: Object<'gc>,
+        receiver: Value<'gc>,
         args: &[Value<'gc>],
     ) -> Result<Value<'gc>, Error<'gc>> {
         let bound_superclass_object = self
             .bound_superclass_object
             .expect("Superclass object is required to run super_init");
 
-        bound_superclass_object.call_init(receiver.into(), args, self)
+        bound_superclass_object.call_init(receiver, args, self)
     }
 
     /// Retrieve a local register.
@@ -605,7 +610,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ///
     /// A return value of `None` implies that both the outer scope, and
     /// the current scope stack were both empty.
-    pub fn global_scope(&self) -> Option<Object<'gc>> {
+    pub fn global_scope(&self) -> Option<Value<'gc>> {
         let outer_scope = self.outer;
         outer_scope
             .get(0)
@@ -627,12 +632,6 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         self.avm2().push(value.into());
     }
 
-    /// Pushes a value onto the operand stack, without running some checks.
-    #[inline]
-    pub fn push_raw(&mut self, value: impl Into<Value<'gc>>) {
-        self.avm2().push_raw(value.into());
-    }
-
     /// Pops a value off the operand stack.
     #[inline]
     #[must_use]
@@ -1138,7 +1137,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         // However, the optimizer can still generate it.
 
         let args = self.pop_stack_args(arg_count);
-        let receiver = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
+        let receiver = self.pop_stack().null_check(self, None)?;
 
         let value = receiver.call_method(index, &args, self)?;
 
@@ -1156,9 +1155,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         let args = self.pop_stack_args(arg_count);
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let receiver = self
-            .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let receiver = self.pop_stack().null_check(self, Some(&multiname))?;
 
         let value = receiver.call_property(&multiname, &args, self)?;
 
@@ -1174,9 +1171,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         let args = self.pop_stack_args(arg_count);
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let receiver = self
-            .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let receiver = self.pop_stack().null_check(self, Some(&multiname))?;
         let function = receiver.get_property(&multiname, self)?;
         let value = function.call(self, Value::Null, &args)?;
 
@@ -1192,9 +1187,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         let args = self.pop_stack_args(arg_count);
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let receiver = self
-            .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let receiver = self.pop_stack().null_check(self, Some(&multiname))?;
 
         receiver.call_property(&multiname, &args, self)?;
 
@@ -1229,7 +1222,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let multiname = multiname.fill_with_runtime_params(self)?;
         let receiver = self
             .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            .null_check(self, Some(&multiname))?
+            .as_object()
+            .expect("Super ops should not appear in primitive functions");
 
         let bound_superclass_object = self.bound_superclass_object(&multiname);
 
@@ -1249,7 +1244,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let multiname = multiname.fill_with_runtime_params(self)?;
         let receiver = self
             .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            .null_check(self, Some(&multiname))?
+            .as_object()
+            .expect("Super ops should not appear in primitive functions");
 
         let bound_superclass_object = self.bound_superclass_object(&multiname);
 
@@ -1290,10 +1287,11 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         // default path for static names
         if !multiname.has_lazy_component() {
-            let object = self.pop_stack();
-            let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
             let value = object.get_property(&multiname, self)?;
             self.push_stack(value);
+
             return Ok(FrameControl::Continue);
         }
 
@@ -1336,8 +1334,8 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         // main path for dynamic names
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let object = self.pop_stack();
-        let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
         let value = object.get_property(&multiname, self)?;
         self.push_stack(value);
 
@@ -1352,8 +1350,8 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         // default path for static names
         if !multiname.has_lazy_component() {
-            let object = self.pop_stack();
-            let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
             object.set_property(&multiname, value, self)?;
             return Ok(FrameControl::Continue);
         }
@@ -1396,8 +1394,8 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         // main path for dynamic names
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let object = self.pop_stack();
-        let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
         object.set_property(&multiname, value, self)?;
 
         Ok(FrameControl::Continue)
@@ -1411,9 +1409,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let multiname = multiname.fill_with_runtime_params(self)?;
 
-        let object = self
-            .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let object = self.pop_stack().null_check(self, Some(&multiname))?;
 
         object.init_property(&multiname, value, self)?;
 
@@ -1426,10 +1422,11 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         // default path for static names
         if !multiname.has_lazy_component() {
-            let object = self.pop_stack();
-            let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
             let did_delete = object.delete_property(self, &multiname)?;
-            self.push_raw(did_delete);
+            self.push_stack(did_delete);
+
             return Ok(FrameControl::Continue);
         }
 
@@ -1442,13 +1439,13 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             let name_value = self.context.avm2.peek(0);
             let object = self.context.avm2.peek(1);
             if let Some(name_object) = name_value.as_object() {
-                let object = object.coerce_to_object_or_typeerror(self, None)?;
-                if let Some(dictionary) = object.as_dictionary_object() {
+                if let Some(dictionary) = object.as_object().and_then(|o| o.as_dictionary_object())
+                {
                     let _ = self.pop_stack();
                     let _ = self.pop_stack();
                     dictionary.delete_property_by_object(name_object, self.gc());
 
-                    self.push_raw(true);
+                    self.push_stack(true);
                     return Ok(FrameControl::Continue);
                 }
             }
@@ -1468,11 +1465,11 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             }
         }
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let object = self.pop_stack();
-        let object = object.coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let object = self.pop_stack().null_check(self, Some(&multiname))?;
+
         let did_delete = object.delete_property(self, &multiname)?;
 
-        self.push_raw(did_delete);
+        self.push_stack(did_delete);
 
         Ok(FrameControl::Continue)
     }
@@ -1484,7 +1481,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let multiname = multiname.fill_with_runtime_params(self)?;
         let object = self
             .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            .null_check(self, Some(&multiname))?
+            .as_object()
+            .expect("Super ops should not appear in primitive functions");
 
         let bound_superclass_object = self.bound_superclass_object(&multiname);
 
@@ -1503,7 +1502,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let multiname = multiname.fill_with_runtime_params(self)?;
         let object = self
             .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+            .null_check(self, Some(&multiname))?
+            .as_object()
+            .expect("Super ops should not appear in primitive functions");
 
         let bound_superclass_object = self.bound_superclass_object(&multiname);
 
@@ -1513,22 +1514,41 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     }
 
     fn op_in(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
-        let obj = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
+        let value = self.pop_stack().null_check(self, None)?;
         let name_value = self.pop_stack();
 
-        if let Some(dictionary) = obj.as_dictionary_object() {
-            if let Some(name_object) = name_value.as_object() {
-                self.push_raw(dictionary.has_property_by_object(name_object));
+        let has_prop = match value {
+            Value::Object(obj) => {
+                if let Some(dictionary) = obj.as_dictionary_object() {
+                    if let Some(name_object) = name_value.as_object() {
+                        self.push_stack(dictionary.has_property_by_object(name_object));
+
+                        return Ok(FrameControl::Continue);
+                    }
+                }
 
-                return Ok(FrameControl::Continue);
+                let name = name_value.coerce_to_string(self)?;
+                let multiname = Multiname::new(self.avm2().find_public_namespace(), name);
+
+                obj.has_property_via_in(self, &multiname)?
             }
-        }
+            _ => {
+                let name = name_value.coerce_to_string(self)?;
+                let multiname = Multiname::new(self.avm2().find_public_namespace(), name);
 
-        let name = name_value.coerce_to_string(self)?;
-        let multiname = Multiname::new(self.avm2().find_public_namespace(), name);
-        let has_prop = obj.has_property_via_in(self, &multiname)?;
+                if value.has_trait(self, &multiname) {
+                    true
+                } else if let Some(proto) = value.proto(self) {
+                    proto.has_property(&multiname)
+                } else {
+                    // This condition can't be met, since `Value::proto` always
+                    // returns `Some` for primitives)
+                    unreachable!()
+                }
+            }
+        };
 
-        self.push_raw(has_prop);
+        self.push_stack(has_prop);
 
         Ok(FrameControl::Continue)
     }
@@ -1557,14 +1577,14 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     }
 
     fn op_push_scope(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
-        let object = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
+        let object = self.pop_stack().null_check(self, None)?;
         self.push_scope(Scope::new(object));
 
         Ok(FrameControl::Continue)
     }
 
     fn op_push_with(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
-        let object = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
+        let object = self.pop_stack().null_check(self, None)?;
         self.push_scope(Scope::new_with(object));
 
         Ok(FrameControl::Continue)
@@ -1599,11 +1619,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     }
 
     fn op_get_global_scope(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
-        self.push_stack(
-            self.global_scope()
-                .map(|gs| gs.into())
-                .unwrap_or(Value::Null),
-        );
+        self.push_stack(self.global_scope().unwrap_or(Value::Null));
 
         Ok(FrameControl::Continue)
     }
@@ -1632,7 +1648,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             .find_definition(&multiname)?
             .or_else(|| self.global_scope());
 
-        self.push_stack(result.map(|o| o.into()).unwrap_or(Value::Undefined));
+        self.push_stack(result.unwrap_or(Value::Undefined));
 
         Ok(FrameControl::Continue)
     }
@@ -1644,10 +1660,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         avm_debug!(self.context.avm2, "Resolving {:?}", *multiname);
 
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let found: Result<Object<'gc>, Error<'gc>> = self
+        let result = self
             .find_definition(&multiname)?
-            .ok_or_else(|| make_error_1065(self, &multiname));
-        let result: Value<'gc> = found?.into();
+            .ok_or_else(|| make_error_1065(self, &multiname))?;
 
         self.push_stack(result);
 
@@ -1670,15 +1685,20 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         multiname: Gc<'gc, Multiname<'gc>>,
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let object = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
-        if let Some(descendants) = object.xml_descendants(self, &multiname) {
+        let object = self.pop_stack().null_check(self, None)?;
+
+        if let Some(descendants) = object
+            .as_object()
+            .and_then(|o| o.xml_descendants(self, &multiname))
+        {
             self.push_stack(descendants);
         } else {
             // Even if it's an object with the "descendants" property, we won't support it.
             let class_name = object
-                .instance_class()
+                .instance_class(self)
                 .name()
                 .to_qualified_name_err_message(self.gc());
+
             return Err(Error::AvmError(type_error(
                 self,
                 &format!(
@@ -1734,7 +1754,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_get_global_slot(&mut self, index: u32) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self
             .global_scope()
-            .map(|global| global.get_slot(index))
+            .map(|global| global.as_object().unwrap().get_slot(index))
             .unwrap_or(Value::Undefined);
 
         self.push_stack(value);
@@ -1746,7 +1766,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack();
 
         self.global_scope()
-            .map(|global| global.set_slot(index, value, self))
+            .map(|global| global.as_object().unwrap().set_slot(index, value, self))
             .transpose()?;
 
         Ok(FrameControl::Continue)
@@ -1770,20 +1790,19 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     ) -> Result<FrameControl<'gc>, Error<'gc>> {
         let args = self.pop_stack_args(arg_count);
         let multiname = multiname.fill_with_runtime_params(self)?;
-        let source = self
-            .pop_stack()
-            .coerce_to_object_or_typeerror(self, Some(&multiname))?;
+        let source = self.pop_stack().null_check(self, Some(&multiname))?;
 
-        let object = source.construct_prop(&multiname, &args, self)?;
+        let ctor = source.get_property(&multiname, self)?;
+        let constructed_object = ctor.construct(self, &args)?;
 
-        self.push_stack(object);
+        self.push_stack(constructed_object);
 
         Ok(FrameControl::Continue)
     }
 
     fn op_construct_super(&mut self, arg_count: u32) -> Result<FrameControl<'gc>, Error<'gc>> {
         let args = self.pop_stack_args(arg_count);
-        let receiver = self.pop_stack().coerce_to_object_or_typeerror(self, None)?;
+        let receiver = self.pop_stack().null_check(self, None)?;
 
         self.super_init(receiver, &args)?;
 
@@ -1844,7 +1863,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let new_class = ClassObject::from_class(self, class, base_class)?;
 
-        self.push_raw(new_class);
+        self.push_stack(new_class);
 
         Ok(FrameControl::Continue)
     }
@@ -1876,7 +1895,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_coerce_b(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_boolean();
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1884,7 +1903,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_coerce_d(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1893,7 +1912,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack().coerce_to_number(self)?;
         let _ = self.pop_stack();
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1901,7 +1920,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_coerce_i(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1910,7 +1929,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack().coerce_to_i32(self)?;
         let _ = self.pop_stack();
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1937,7 +1956,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             _ => value.coerce_to_string(self)?.into(),
         };
 
-        self.push_raw(coerced);
+        self.push_stack(coerced);
 
         Ok(FrameControl::Continue)
     }
@@ -1945,7 +1964,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_coerce_u(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_u32(self)?;
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -1954,16 +1973,14 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack().coerce_to_u32(self)?;
         let _ = self.pop_stack();
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
 
     fn op_convert_o(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
-        let value = self.pop_stack();
-        if matches!(value, Value::Null | Value::Undefined) {
-            return Err(make_null_or_undefined_error(self, value, None));
-        }
+        let value = self.pop_stack().null_check(self, None)?;
+
         self.push_stack(value);
 
         Ok(FrameControl::Continue)
@@ -1972,7 +1989,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_convert_s(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_string(self)?;
 
-        self.push_raw(value);
+        self.push_stack(value);
 
         Ok(FrameControl::Continue)
     }
@@ -2059,7 +2076,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1.wrapping_add(value2));
+        self.push_stack(value1.wrapping_add(value2));
 
         Ok(FrameControl::Continue)
     }
@@ -2068,7 +2085,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1 & value2);
+        self.push_stack(value1 & value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2076,7 +2093,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_bitnot(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(!value1);
+        self.push_stack(!value1);
 
         Ok(FrameControl::Continue)
     }
@@ -2085,7 +2102,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1 | value2);
+        self.push_stack(value1 | value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2094,7 +2111,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1 ^ value2);
+        self.push_stack(value1 ^ value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2118,7 +2135,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_decrement(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value - 1.0);
+        self.push_stack(value - 1.0);
 
         Ok(FrameControl::Continue)
     }
@@ -2126,7 +2143,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_decrement_i(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value.wrapping_sub(1));
+        self.push_stack(value.wrapping_sub(1));
 
         Ok(FrameControl::Continue)
     }
@@ -2135,7 +2152,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_number(self)?;
         let value1 = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value1 / value2);
+        self.push_stack(value1 / value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2159,7 +2176,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_increment(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value + 1.0);
+        self.push_stack(value + 1.0);
 
         Ok(FrameControl::Continue)
     }
@@ -2167,7 +2184,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_increment_i(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value.wrapping_add(1));
+        self.push_stack(value.wrapping_add(1));
 
         Ok(FrameControl::Continue)
     }
@@ -2176,7 +2193,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_u32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1 << (value2 & 0x1F));
+        self.push_stack(value1 << (value2 & 0x1F));
 
         Ok(FrameControl::Continue)
     }
@@ -2185,7 +2202,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_number(self)?;
         let value1 = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value1 % value2);
+        self.push_stack(value1 % value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2194,7 +2211,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_number(self)?;
         let value1 = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(value1 * value2);
+        self.push_stack(value1 * value2);
 
         Ok(FrameControl::Continue)
     }
@@ -2203,7 +2220,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1.wrapping_mul(value2));
+        self.push_stack(value1.wrapping_mul(value2));
 
         Ok(FrameControl::Continue)
     }
@@ -2211,7 +2228,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_negate(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value1 = self.pop_stack().coerce_to_number(self)?;
 
-        self.push_raw(-value1);
+        self.push_stack(-value1);
 
         Ok(FrameControl::Continue)
     }
@@ -2219,7 +2236,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_negate_i(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1.wrapping_neg());
+        self.push_stack(value1.wrapping_neg());
 
         Ok(FrameControl::Continue)
     }
@@ -2228,7 +2245,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_u32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1 >> (value2 & 0x1F));
+        self.push_stack(value1 >> (value2 & 0x1F));
 
         Ok(FrameControl::Continue)
     }
@@ -2257,7 +2274,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_i32(self)?;
         let value1 = self.pop_stack().coerce_to_i32(self)?;
 
-        self.push_raw(value1.wrapping_sub(value2));
+        self.push_stack(value1.wrapping_sub(value2));
 
         Ok(FrameControl::Continue)
     }
@@ -2276,7 +2293,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value2 = self.pop_stack().coerce_to_u32(self)?;
         let value1 = self.pop_stack().coerce_to_u32(self)?;
 
-        self.push_raw(value1 >> (value2 & 0x1F));
+        self.push_stack(value1 >> (value2 & 0x1F));
 
         Ok(FrameControl::Continue)
     }
@@ -2442,7 +2459,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_strict_equals(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value2 = self.pop_stack();
         let value1 = self.pop_stack();
-        self.push_raw(value1.strict_eq(&value2));
+        self.push_stack(value1.strict_eq(&value2));
 
         Ok(FrameControl::Continue)
     }
@@ -2453,7 +2470,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let result = value1.abstract_eq(&value2, self)?;
 
-        self.push_raw(result);
+        self.push_stack(result);
 
         Ok(FrameControl::Continue)
     }
@@ -2464,7 +2481,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let result = !value1.abstract_lt(&value2, self)?.unwrap_or(true);
 
-        self.push_raw(result);
+        self.push_stack(result);
 
         Ok(FrameControl::Continue)
     }
@@ -2475,7 +2492,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let result = value2.abstract_lt(&value1, self)?.unwrap_or(false);
 
-        self.push_raw(result);
+        self.push_stack(result);
 
         Ok(FrameControl::Continue)
     }
@@ -2486,7 +2503,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let result = !value2.abstract_lt(&value1, self)?.unwrap_or(true);
 
-        self.push_raw(result);
+        self.push_stack(result);
 
         Ok(FrameControl::Continue)
     }
@@ -2497,7 +2514,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let result = value1.abstract_lt(&value2, self)?.unwrap_or(false);
 
-        self.push_raw(result);
+        self.push_stack(result);
 
         Ok(FrameControl::Continue)
     }
@@ -2505,7 +2522,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
     fn op_not(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
         let value = self.pop_stack().coerce_to_boolean();
 
-        self.push_raw(!value);
+        self.push_stack(!value);
 
         Ok(FrameControl::Continue)
     }
@@ -2514,7 +2531,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let cur_index = self.pop_stack().coerce_to_i32(self)?;
 
         if cur_index < 0 {
-            self.push_raw(false);
+            self.push_stack(false);
 
             return Ok(FrameControl::Continue);
         }
@@ -2530,9 +2547,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         if let Some(object) = object {
             let next_index = object.get_next_enumerant(cur_index as u32, self)?;
 
-            self.push_raw(next_index);
+            self.push_stack(next_index);
         } else {
-            self.push_raw(0);
+            self.push_stack(0);
         }
 
         Ok(FrameControl::Continue)
@@ -2545,7 +2562,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let cur_index = self.local_register(index_register).coerce_to_i32(self)?;
 
         if cur_index < 0 {
-            self.push_raw(false);
+            self.push_stack(false);
 
             return Ok(FrameControl::Continue);
         }
@@ -2584,7 +2601,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             result_value = Value::Null;
         }
 
-        self.push_raw(cur_index != 0);
+        self.push_stack(cur_index != 0);
         self.set_local_register(index_register, cur_index);
         self.set_local_register(object_register, result_value);
 
@@ -2647,7 +2664,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack();
 
         let is_instance_of = value.is_of_type(self, class);
-        self.push_raw(is_instance_of);
+        self.push_stack(is_instance_of);
 
         Ok(FrameControl::Continue)
     }
@@ -2667,7 +2684,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let value = self.pop_stack();
 
         let is_instance_of = value.is_of_type(self, type_object.inner_class_definition());
-        self.push_raw(is_instance_of);
+        self.push_stack(is_instance_of);
 
         Ok(FrameControl::Continue)
     }
@@ -2678,7 +2695,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         if value.is_of_type(self, class) {
             self.push_stack(value);
         } else {
-            self.push_raw(Value::Null);
+            self.push_stack(Value::Null);
         }
 
         Ok(FrameControl::Continue)
@@ -2704,7 +2721,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             if value.is_of_type(self, class.inner_class_definition()) {
                 self.push_stack(value);
             } else {
-                self.push_raw(Value::Null);
+                self.push_stack(Value::Null);
             }
 
             Ok(FrameControl::Continue)
@@ -2735,11 +2752,11 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         match value {
             Value::Undefined => return Err(make_null_or_undefined_error(self, value, None)),
-            Value::Null => self.push_raw(false),
+            Value::Null => self.push_stack(false),
             value => {
                 let is_instance_of = value.is_instance_of(self, type_object);
 
-                self.push_raw(is_instance_of);
+                self.push_stack(is_instance_of);
             }
         }
 
@@ -2782,7 +2799,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             Value::String(_) => "string",
         };
 
-        self.push_raw(Value::String(type_name.into()));
+        self.push_stack(Value::String(type_name.into()));
 
         Ok(FrameControl::Continue)
     }
@@ -2793,7 +2810,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         // Implementation of `EscapeAttributeValue` from ECMA-357(10.2.1.2)
         let r = escape_attribute_value(s);
-        self.push_raw(AvmString::new(self.gc(), r));
+        self.push_stack(AvmString::new(self.gc(), r));
 
         Ok(FrameControl::Continue)
     }
@@ -2808,7 +2825,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
             x => AvmString::new(self.gc(), escape_element_value(x.coerce_to_string(self)?)),
         };
 
-        self.push_raw(r);
+        self.push_stack(r);
 
         Ok(FrameControl::Continue)
     }
@@ -2982,7 +2999,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         let val = dm.get(address);
 
         if let Some(val) = val {
-            self.push_raw(val);
+            self.push_stack(val);
         } else {
             return Err(make_error_1506(self));
         }
@@ -3002,7 +3019,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         }
 
         let val = dm.read_at(2, address).map_err(|e| e.to_avm(self))?;
-        self.push_raw(u16::from_le_bytes(val.try_into().unwrap()));
+        self.push_stack(u16::from_le_bytes(val.try_into().unwrap()));
 
         Ok(FrameControl::Continue)
     }
@@ -3019,7 +3036,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         }
 
         let val = dm.read_at(4, address).map_err(|e| e.to_avm(self))?;
-        self.push_raw(i32::from_le_bytes(val.try_into().unwrap()));
+        self.push_stack(i32::from_le_bytes(val.try_into().unwrap()));
         Ok(FrameControl::Continue)
     }
 
@@ -3035,7 +3052,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         }
 
         let val = dm.read_at(4, address).map_err(|e| e.to_avm(self))?;
-        self.push_raw(f32::from_le_bytes(val.try_into().unwrap()));
+        self.push_stack(f32::from_le_bytes(val.try_into().unwrap()));
 
         Ok(FrameControl::Continue)
     }
@@ -3052,7 +3069,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
         }
 
         let val = dm.read_at(8, address).map_err(|e| e.to_avm(self))?;
-        self.push_raw(f64::from_le_bytes(val.try_into().unwrap()));
+        self.push_stack(f64::from_le_bytes(val.try_into().unwrap()));
         Ok(FrameControl::Continue)
     }
 
@@ -3062,7 +3079,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let val = val.wrapping_shl(31).wrapping_shr(31);
 
-        self.push_raw(Value::Integer(val));
+        self.push_stack(Value::Integer(val));
 
         Ok(FrameControl::Continue)
     }
@@ -3073,7 +3090,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let val = (val.wrapping_shl(23).wrapping_shr(23) & 0xFF) as i8 as i32;
 
-        self.push_raw(Value::Integer(val));
+        self.push_stack(Value::Integer(val));
 
         Ok(FrameControl::Continue)
     }
@@ -3084,7 +3101,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
 
         let val = (val.wrapping_shl(15).wrapping_shr(15) & 0xFFFF) as i16 as i32;
 
-        self.push_raw(Value::Integer(val));
+        self.push_stack(Value::Integer(val));
 
         Ok(FrameControl::Continue)
     }
diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs
index 46f271e56352f..fd1b3fadd5b3f 100644
--- a/core/src/avm2/amf.rs
+++ b/core/src/avm2/amf.rs
@@ -200,7 +200,7 @@ pub fn recursive_serialize<'gc>(
                     continue;
                 }
             }
-            let value = obj.get_public_property(name, activation)?;
+            let value = Value::from(obj).get_public_property(name, activation)?;
             let name = name.to_utf8_lossy().to_string();
             if let Some(elem) =
                 get_or_create_element(activation, name.clone(), value, object_table, amf_version)
@@ -344,7 +344,7 @@ pub fn deserialize_value_impl<'gc>(
                 activation.avm2().classes().object
             };
             let obj = target_class.construct(activation, &[])?;
-            object_map.insert(*id, obj);
+            object_map.insert(*id, obj.as_object().unwrap());
 
             for entry in elements {
                 let name = entry.name();
@@ -370,23 +370,17 @@ pub fn deserialize_value_impl<'gc>(
                 }
             }
 
-            obj.into()
+            obj
         }
         AmfValue::Date(time, _) => activation
             .avm2()
             .classes()
             .date
-            .construct(activation, &[(*time).into()])?
-            .into(),
-        AmfValue::XML(content, _) => activation
-            .avm2()
-            .classes()
-            .xml
-            .construct(
-                activation,
-                &[Value::String(AvmString::new_utf8(activation.gc(), content))],
-            )?
-            .into(),
+            .construct(activation, &[(*time).into()])?,
+        AmfValue::XML(content, _) => activation.avm2().classes().xml.construct(
+            activation,
+            &[Value::String(AvmString::new_utf8(activation.gc(), content))],
+        )?,
         AmfValue::VectorDouble(vec, is_fixed) => {
             let storage = VectorStorage::from_values(
                 vec.iter().map(|v| (*v).into()).collect(),
@@ -452,7 +446,10 @@ pub fn deserialize_value_impl<'gc>(
                 .avm2()
                 .classes()
                 .dictionary
-                .construct(activation, &[(*has_weak_keys).into()])?;
+                .construct(activation, &[(*has_weak_keys).into()])?
+                .as_object()
+                .unwrap();
+
             object_map.insert(*id, obj);
             let dict_obj = obj
                 .as_dictionary_object()
diff --git a/core/src/avm2/class.rs b/core/src/avm2/class.rs
index e559127da1681..5d71af3e38f72 100644
--- a/core/src/avm2/class.rs
+++ b/core/src/avm2/class.rs
@@ -74,7 +74,7 @@ pub struct Allocator(pub AllocatorFn);
 /// This function should be passed an Activation, and the arguments passed to the
 /// constructor, and will return an Object.
 pub type CustomConstructorFn =
-    for<'gc> fn(&mut Activation<'_, 'gc>, &[Value<'gc>]) -> Result<Object<'gc>, Error<'gc>>;
+    for<'gc> fn(&mut Activation<'_, 'gc>, &[Value<'gc>]) -> Result<Value<'gc>, Error<'gc>>;
 
 #[derive(Clone, Copy)]
 pub struct CustomConstructor(pub CustomConstructorFn);
diff --git a/core/src/avm2/domain.rs b/core/src/avm2/domain.rs
index e340940496b86..ca6a4f12dd9c2 100644
--- a/core/src/avm2/domain.rs
+++ b/core/src/avm2/domain.rs
@@ -266,7 +266,7 @@ impl<'gc> Domain<'gc> {
         let (name, script) = self.find_defining_script(activation, &name.into())?;
         let globals = script.globals(activation.context)?;
 
-        globals.get_property(&name.into(), activation)
+        Value::from(globals).get_property(&name.into(), activation)
     }
 
     /// Retrieve a value from this domain, with special handling for 'Vector.<SomeType>'.
@@ -395,7 +395,10 @@ impl<'gc> Domain<'gc> {
     ) -> Result<(), Error<'gc>> {
         let bytearray_class = activation.avm2().classes().bytearray;
 
-        let domain_memory = bytearray_class.construct(activation, &[])?;
+        let domain_memory = bytearray_class
+            .construct(activation, &[])?
+            .as_object()
+            .unwrap();
         domain_memory
             .as_bytearray_mut()
             .unwrap()
diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs
index d1526b1351102..c80b1b319c24d 100644
--- a/core/src/avm2/e4x.rs
+++ b/core/src/avm2/e4x.rs
@@ -1,5 +1,4 @@
 use crate::avm2::error::{make_error_1010, make_error_1085, make_error_1118, type_error};
-use crate::avm2::globals::slots::xml as xml_class_slots;
 use crate::avm2::object::{E4XOrXml, FunctionObject, NamespaceObject};
 use crate::avm2::{Activation, Error, Multiname, TObject, Value};
 use crate::string::{AvmString, WStr, WString};
@@ -132,7 +131,10 @@ impl<'gc> E4XNamespace<'gc> {
             .classes()
             .namespace
             .construct(activation, &args)?;
+
         Ok(obj
+            .as_object()
+            .unwrap()
             .as_namespace_object()
             .expect("just constructed a namespace"))
     }
@@ -758,7 +760,7 @@ impl<'gc> E4XNode<'gc> {
             val => {
                 if let Some(obj) = val.as_object() {
                     if obj.as_xml_object().is_some() || obj.as_xml_list_object().is_some() {
-                        value = obj.call_public_property("toXMLString", &[], activation)?;
+                        value = val.call_public_property("toXMLString", &[], activation)?;
                     }
                 }
                 value.coerce_to_string(activation)?
@@ -1649,21 +1651,10 @@ pub fn to_xml_string<'gc>(
     xml: E4XOrXml<'gc>,
     activation: &mut Activation<'_, 'gc>,
 ) -> AvmString<'gc> {
-    let pretty_printing = activation
-        .avm2()
-        .classes()
-        .xml
-        .get_slot(xml_class_slots::PRETTY_PRINTING)
-        .coerce_to_boolean();
+    let pretty_printing = activation.avm2().xml_settings.pretty_printing;
 
     let pretty = if pretty_printing {
-        let pretty_indent = activation
-            .avm2()
-            .classes()
-            .xml
-            .get_slot(xml_class_slots::PRETTY_INDENT)
-            .coerce_to_i32(activation)
-            .expect("shouldn't error");
+        let pretty_indent = activation.avm2().xml_settings.pretty_indent;
 
         // NOTE: Negative values are invalid and are ignored.
         if pretty_indent < 0 {
@@ -1754,7 +1745,7 @@ pub fn maybe_escape_child<'gc>(
                 .classes()
                 .xml
                 .construct(activation, &[string.into()])?;
-            return Ok(xml.into());
+            return Ok(xml);
         }
     }
 
@@ -1775,3 +1766,23 @@ pub fn maybe_escape_child<'gc>(
 
     Ok(child)
 }
+
+pub struct XmlSettings {
+    pub ignore_comments: bool,
+    pub ignore_processing_instructions: bool,
+    pub ignore_whitespace: bool,
+    pub pretty_printing: bool,
+    pub pretty_indent: i32,
+}
+
+impl XmlSettings {
+    pub fn new_default() -> Self {
+        XmlSettings {
+            ignore_comments: true,
+            ignore_processing_instructions: true,
+            ignore_whitespace: true,
+            pretty_printing: true,
+            pretty_indent: 2,
+        }
+    }
+}
diff --git a/core/src/avm2/error.rs b/core/src/avm2/error.rs
index 85b32d06881e8..95f12aeba37a2 100644
--- a/core/src/avm2/error.rs
+++ b/core/src/avm2/error.rs
@@ -835,9 +835,8 @@ fn error_constructor<'gc>(
     code: u32,
 ) -> Result<Value<'gc>, Error<'gc>> {
     let message = AvmString::new_utf8(activation.gc(), message);
-    Ok(class
-        .construct(activation, &[message.into(), code.into()])?
-        .into())
+
+    class.construct(activation, &[message.into(), code.into()])
 }
 
 impl std::fmt::Display for Error<'_> {
diff --git a/core/src/avm2/events.rs b/core/src/avm2/events.rs
index 1343f6e756282..4ee90391398f9 100644
--- a/core/src/avm2/events.rs
+++ b/core/src/avm2/events.rs
@@ -3,7 +3,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::error::make_error_2007;
 use crate::avm2::globals::slots::flash_events_event_dispatcher as slots;
-use crate::avm2::object::{Object, TObject};
+use crate::avm2::object::{EventObject, Object, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::Error;
 use crate::display_object::TDisplayObject;
@@ -374,13 +374,13 @@ fn dispatch_event_to_target<'gc>(
     activation: &mut Activation<'_, 'gc>,
     dispatcher: Object<'gc>,
     target: Object<'gc>,
-    event: Object<'gc>,
+    event: EventObject<'gc>,
     simulate_dispatch: bool,
 ) -> Result<(), Error<'gc>> {
     avm_debug!(
         activation.context.avm2,
         "Event dispatch: {} to {target:?}",
-        event.as_event().unwrap().event_type(),
+        event.event().event_type(),
     );
 
     let dispatch_list = dispatcher.get_slot(slots::DISPATCH_LIST).as_object();
@@ -392,7 +392,7 @@ fn dispatch_event_to_target<'gc>(
 
     let dispatch_list = dispatch_list.unwrap();
 
-    let mut evtmut = event.as_event_mut(activation.gc()).unwrap();
+    let mut evtmut = event.event_mut(activation.gc());
     let name = evtmut.event_type();
     let use_capture = evtmut.phase() == EventPhase::Capturing;
 
@@ -415,11 +415,7 @@ fn dispatch_event_to_target<'gc>(
     }
 
     for handler in handlers.iter() {
-        if event
-            .as_event()
-            .unwrap()
-            .is_propagation_stopped_immediately()
-        {
+        if event.event().is_propagation_stopped_immediately() {
             break;
         }
 
@@ -441,7 +437,7 @@ fn dispatch_event_to_target<'gc>(
 pub fn dispatch_event<'gc>(
     activation: &mut Activation<'_, 'gc>,
     this: Object<'gc>,
-    event: Object<'gc>,
+    event_object: EventObject<'gc>,
     simulate_dispatch: bool,
 ) -> Result<bool, Error<'gc>> {
     let target = this.get_slot(slots::TARGET).as_object().unwrap_or(this);
@@ -459,18 +455,20 @@ pub fn dispatch_event<'gc>(
         parent = parent_dobj.parent();
     }
 
-    let dispatched = event.as_event().unwrap().dispatched;
+    let dispatched = event_object.event().dispatched;
 
     let event = if dispatched {
-        event
+        Value::from(event_object)
             .call_public_property("clone", &[], activation)?
             .as_object()
             .ok_or_else(|| make_error_2007(activation, "event"))?
+            .as_event_object()
+            .expect("Event.clone should return an Event")
     } else {
-        event
+        event_object
     };
 
-    let mut evtmut = event.as_event_mut(activation.gc()).unwrap();
+    let mut evtmut = event.event_mut(activation.gc());
 
     evtmut.set_phase(EventPhase::Capturing);
     evtmut.set_target(target);
@@ -478,7 +476,7 @@ pub fn dispatch_event<'gc>(
     drop(evtmut);
 
     for ancestor in ancestor_list.iter().rev() {
-        if event.as_event().unwrap().is_propagation_stopped() {
+        if event.event().is_propagation_stopped() {
             break;
         }
 
@@ -486,22 +484,20 @@ pub fn dispatch_event<'gc>(
     }
 
     event
-        .as_event_mut(activation.gc())
-        .unwrap()
+        .event_mut(activation.gc())
         .set_phase(EventPhase::AtTarget);
 
-    if !event.as_event().unwrap().is_propagation_stopped() {
+    if !event.event().is_propagation_stopped() {
         dispatch_event_to_target(activation, this, target, event, simulate_dispatch)?;
     }
 
     event
-        .as_event_mut(activation.gc())
-        .unwrap()
+        .event_mut(activation.context.gc_context)
         .set_phase(EventPhase::Bubbling);
 
-    if event.as_event().unwrap().is_bubbling() {
+    if event.event().is_bubbling() {
         for ancestor in ancestor_list.iter() {
-            if event.as_event().unwrap().is_propagation_stopped() {
+            if event.event().is_propagation_stopped() {
                 break;
             }
 
@@ -509,6 +505,6 @@ pub fn dispatch_event<'gc>(
         }
     }
 
-    let handled = event.as_event().unwrap().dispatched;
+    let handled = event.event().dispatched;
     Ok(handled)
 }
diff --git a/core/src/avm2/filters.rs b/core/src/avm2/filters.rs
index 7175ea830b71d..8af5ea5b962d0 100644
--- a/core/src/avm2/filters.rs
+++ b/core/src/avm2/filters.rs
@@ -34,7 +34,7 @@ pub trait FilterAvm2Ext {
     fn as_avm2_object<'gc>(
         &self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Object<'gc>, Error<'gc>>;
+    ) -> Result<Value<'gc>, Error<'gc>>;
 }
 
 #[derive(Clone, Collect)]
@@ -178,7 +178,7 @@ impl FilterAvm2Ext for Filter {
     fn as_avm2_object<'gc>(
         &self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Object<'gc>, Error<'gc>> {
+    ) -> Result<Value<'gc>, Error<'gc>> {
         match self {
             Filter::BevelFilter(filter) => bevel_filter_to_avm2(activation, filter),
             Filter::BlurFilter(filter) => blur_filter_to_avm2(activation, filter),
@@ -265,7 +265,7 @@ fn avm2_to_bevel_filter<'gc>(
 fn bevel_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &BevelFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     activation.avm2().classes().bevelfilter.construct(
         activation,
         &[
@@ -315,7 +315,7 @@ fn avm2_to_blur_filter<'gc>(
 fn blur_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &BlurFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     activation.avm2().classes().blurfilter.construct(
         activation,
         &[
@@ -350,7 +350,7 @@ fn avm2_to_color_matrix_filter<'gc>(
 fn color_matrix_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &ColorMatrixFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let matrix = ArrayObject::from_storage(
         activation,
         filter.matrix.iter().map(|v| Value::from(*v)).collect(),
@@ -425,7 +425,7 @@ fn avm2_to_convolution_filter<'gc>(
 fn convolution_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &ConvolutionFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let matrix = ArrayObject::from_storage(
         activation,
         filter
@@ -530,7 +530,7 @@ fn avm2_to_displacement_map_filter<'gc>(
 fn displacement_map_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &DisplacementMapFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let point = activation.avm2().classes().point;
     let map_point = point.construct(
         activation,
@@ -546,7 +546,7 @@ fn displacement_map_filter_to_avm2<'gc>(
         activation,
         &[
             Value::Null, // TODO: This should be a BitmapData...
-            map_point.into(),
+            map_point,
             filter.component_x.into(),
             filter.component_y.into(),
             filter.scale_x.into(),
@@ -616,7 +616,7 @@ fn avm2_to_drop_shadow_filter<'gc>(
 fn drop_shadow_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &DropShadowFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     activation.avm2().classes().dropshadowfilter.construct(
         activation,
         &[
@@ -679,7 +679,7 @@ fn avm2_to_glow_filter<'gc>(
 fn glow_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &GlowFilter,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     activation.avm2().classes().glowfilter.construct(
         activation,
         &[
@@ -763,7 +763,7 @@ fn gradient_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &GradientFilter,
     class: ClassObject<'gc>,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let colors = ArrayObject::from_storage(
         activation,
         filter
@@ -855,7 +855,7 @@ fn avm2_to_shader_filter<'gc>(
 fn shader_filter_to_avm2<'gc>(
     activation: &mut Activation<'_, 'gc>,
     filter: &ShaderFilter<'static>,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let object_wrapper: &ObjectWrapper = filter
         .shader_object
         .downcast_ref::<ObjectWrapper>()
diff --git a/core/src/avm2/function.rs b/core/src/avm2/function.rs
index b7dffcf5d161c..2d7c4f32700ce 100644
--- a/core/src/avm2/function.rs
+++ b/core/src/avm2/function.rs
@@ -1,7 +1,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::class::Class;
 use crate::avm2::method::{Method, ParamConfig};
-use crate::avm2::object::{ClassObject, Object};
+use crate::avm2::object::ClassObject;
 use crate::avm2::scope::ScopeChain;
 use crate::avm2::traits::TraitKind;
 use crate::avm2::value::Value;
@@ -24,7 +24,9 @@ pub struct BoundMethod<'gc> {
     ///
     /// If `None`, then the receiver provided by the caller is used. A
     /// `Some` value indicates a bound executable.
-    bound_receiver: Option<Object<'gc>>,
+    ///
+    /// This should never be `Value::Null` or `Value::Undefined`.
+    bound_receiver: Option<Value<'gc>>,
 
     /// The superclass of the bound class for this method.
     ///
@@ -40,7 +42,7 @@ impl<'gc> BoundMethod<'gc> {
     pub fn from_method(
         method: Method<'gc>,
         scope: ScopeChain<'gc>,
-        receiver: Option<Object<'gc>>,
+        receiver: Option<Value<'gc>>,
         superclass: Option<ClassObject<'gc>>,
         class: Option<Class<'gc>>,
     ) -> Self {
@@ -58,7 +60,7 @@ impl<'gc> BoundMethod<'gc> {
         unbound_receiver: Value<'gc>,
         arguments: &[Value<'gc>],
         activation: &mut Activation<'_, 'gc>,
-        callee: Object<'gc>,
+        callee: Value<'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
         let receiver = if let Some(receiver) = self.bound_receiver {
             receiver
@@ -68,7 +70,7 @@ impl<'gc> BoundMethod<'gc> {
                 .expect("No global scope for function call")
                 .values()
         } else {
-            unbound_receiver.coerce_to_object(activation)?
+            unbound_receiver
         };
 
         exec(
@@ -137,16 +139,19 @@ impl<'gc> BoundMethod<'gc> {
 ///
 /// Passed-in arguments will be conformed to the set of method parameters
 /// declared on the function.
+///
+/// It is the caller's responsibility to ensure that the `receiver` passed
+/// to this method is not Value::Null or Value::Undefined.
 #[allow(clippy::too_many_arguments)]
 pub fn exec<'gc>(
     method: Method<'gc>,
     scope: ScopeChain<'gc>,
-    receiver: Object<'gc>,
+    receiver: Value<'gc>,
     bound_superclass: Option<ClassObject<'gc>>,
     bound_class: Option<Class<'gc>>,
     mut arguments: &[Value<'gc>],
     activation: &mut Activation<'_, 'gc>,
-    callee: Object<'gc>,
+    callee: Value<'gc>,
 ) -> Result<Value<'gc>, Error<'gc>> {
     let ret = match method {
         Method::Native(bm) => {
@@ -200,7 +205,7 @@ pub fn exec<'gc>(
                 .context
                 .avm2
                 .push_call(activation.gc(), method, bound_class);
-            (bm.method)(&mut activation, Value::Object(receiver), &arguments)
+            (bm.method)(&mut activation, receiver, &arguments)
         }
         Method::Bytecode(bm) => {
             if bm.is_unchecked() {
diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs
index 1e4bf9656550c..0ffcd80e6fa26 100644
--- a/core/src/avm2/globals.rs
+++ b/core/src/avm2/globals.rs
@@ -8,10 +8,7 @@ use crate::avm2::script::Script;
 use crate::avm2::traits::Trait;
 use crate::avm2::value::Value;
 use crate::avm2::vtable::VTable;
-use crate::avm2::Avm2;
-use crate::avm2::Error;
-use crate::avm2::Namespace;
-use crate::avm2::QName;
+use crate::avm2::{Avm2, Error, Multiname, Namespace, QName};
 use crate::tag_utils::{self, ControlFlow, SwfMovie, SwfSlice, SwfStream};
 use gc_arena::Collect;
 use std::sync::Arc;
@@ -400,7 +397,7 @@ fn define_fn_on_global<'gc>(
         .get_defined_value(activation, qname)
         .expect("Function being defined on global should be defined in domain!");
 
-    global
+    Value::from(global)
         .init_property(&qname.into(), func, activation)
         .expect("Should set property");
 }
@@ -418,7 +415,7 @@ fn dynamic_class<'gc>(
     let class = class_object.inner_class_definition();
     let name = class.name();
 
-    global
+    Value::from(global)
         .init_property(&name.into(), class_object.into(), activation)
         .expect("Should set property");
 
@@ -451,7 +448,7 @@ fn class<'gc>(
 
     let class_object = ClassObject::from_class(activation, class_def, super_class)?;
 
-    global
+    Value::from(global)
         .init_property(&class_name.into(), class_object.into(), activation)
         .expect("Should set property");
 
@@ -482,7 +479,7 @@ fn vector_class<'gc>(
 
     let legacy_name = QName::new(namespaces.vector_internal, legacy_name);
 
-    global
+    Value::from(global)
         .init_property(&legacy_name.into(), vector_cls.into(), activation)
         .expect("Should set property");
 
@@ -572,7 +569,6 @@ pub fn load_player_globals<'gc>(
     ));
 
     let string_class = string::create_class(activation);
-    let boolean_class = boolean::create_class(activation);
     let number_class = number::create_class(activation);
     let int_class = int::create_class(activation);
     let uint_class = uint::create_class(activation);
@@ -597,7 +593,6 @@ pub fn load_player_globals<'gc>(
         (public_ns, "Class", class_i_class),
         (public_ns, "Function", fn_classdef),
         (public_ns, "String", string_class),
-        (public_ns, "Boolean", boolean_class),
         (public_ns, "Number", number_class),
         (public_ns, "int", int_class),
         (public_ns, "uint", uint_class),
@@ -653,7 +648,7 @@ pub fn load_player_globals<'gc>(
     let globals = ScriptObject::custom_object(mc, global_classdef, None, global_classdef.vtable());
 
     let scope = ScopeChain::new(domain);
-    let gs = scope.chain(mc, &[Scope::new(globals)]);
+    let gs = scope.chain(mc, &[Scope::new(globals.into())]);
     activation.set_outer(gs);
 
     let object_class = ClassObject::from_class_partial(activation, object_i_class, None)?;
@@ -707,7 +702,7 @@ pub fn load_player_globals<'gc>(
     let fn_class = fn_class.into_finished_class(activation)?;
 
     // Function's prototype is an instance of itself
-    let fn_proto = fn_class.construct(activation, &[])?;
+    let fn_proto = fn_class.construct(activation, &[])?.as_object().unwrap();
     fn_class.link_prototype(activation, fn_proto)?;
 
     // Object prototype is enough
@@ -740,7 +735,6 @@ pub fn load_player_globals<'gc>(
     // Make sure to initialize superclasses *before* their subclasses!
 
     avm2_system_class!(string, activation, string_class, script);
-    avm2_system_class!(boolean, activation, boolean_class, script);
     avm2_system_class!(number, activation, number_class, script);
     avm2_system_class!(int, activation, int_class, script);
     avm2_system_class!(uint, activation, uint_class, script);
@@ -833,11 +827,12 @@ macro_rules! avm2_system_class_defs_playerglobal {
     ($activation:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => {
         let activation = $activation;
         $(
+            let domain = activation.domain();
+
             // Lookup with the highest version, so we we see all defined classes here
             let ns = Namespace::package($package, ApiVersion::VM_INTERNAL, activation.strings());
-            let name = QName::new(ns, $class_name);
-            let class_object = activation.domain().get_defined_value(activation, name).unwrap_or_else(|e| panic!("Failed to lookup {name:?}: {e:?}"));
-            let class_def = class_object.as_object().unwrap().as_class_object().unwrap().inner_class_definition();
+            let name = Multiname::new(ns, $class_name);
+            let class_def = domain.get_class(activation.context, &name).unwrap_or_else(|| panic!("Failed to lookup {name:?}"));
             let sc = activation.avm2().system_class_defs.as_mut().unwrap();
             sc.$field = class_def;
         )*
@@ -848,11 +843,12 @@ pub fn init_builtin_system_classes(activation: &mut Activation<'_, '_>) {
     avm2_system_classes_playerglobal!(
         &mut *activation,
         [
-            ("", "Error", error),
             ("", "ArgumentError", argumenterror),
-            ("", "QName", qname),
+            ("", "Boolean", boolean),
+            ("", "Error", error),
             ("", "EvalError", evalerror),
             ("", "Namespace", namespace),
+            ("", "QName", qname),
             ("", "RangeError", rangeerror),
             ("", "ReferenceError", referenceerror),
             ("", "SecurityError", securityerror),
@@ -864,10 +860,13 @@ pub fn init_builtin_system_classes(activation: &mut Activation<'_, '_>) {
             ("", "XMLList", xml_list),
         ]
     );
+}
 
+pub fn init_builtin_system_class_defs(activation: &mut Activation<'_, '_>) {
     avm2_system_class_defs_playerglobal!(
         &mut *activation,
         [
+            ("", "Boolean", boolean),
             ("", "Namespace", namespace),
             ("", "XML", xml),
             ("", "XMLList", xml_list),
diff --git a/core/src/avm2/globals/Boolean.as b/core/src/avm2/globals/Boolean.as
index 360ffe7c1bcd3..0d45b42ec67ca 100644
--- a/core/src/avm2/globals/Boolean.as
+++ b/core/src/avm2/globals/Boolean.as
@@ -1,5 +1,51 @@
-// This is a stub - the actual class is defined in `boolean.rs`
 package {
-	public class Boolean {
-	}
+    [Ruffle(CustomConstructor)]
+    [Ruffle(CallHandler)]
+    public final class Boolean {
+        public function Boolean(value:* = void 0) {
+            // The Boolean constructor is implemented natively:
+            // this AS-defined method does nothing
+        }
+
+        prototype.toString = function():String {
+            if (this === Boolean.prototype) {
+                return "false";
+            }
+
+            if (!(this is Boolean)) {
+                throw new TypeError("Error #1004: Method Boolean.prototype.toString was invoked on an incompatible object.", 1004);
+            }
+
+            return this.AS3::toString();
+        };
+
+        prototype.valueOf = function():* {
+            if (this === Boolean.prototype) {
+                return false;
+            }
+
+            if (!(this is Boolean)) {
+                throw new TypeError("Error #1004: Method Boolean.prototype.valueOf was invoked on an incompatible object.", 1004);
+            }
+
+            return this;
+        };
+
+        prototype.setPropertyIsEnumerable("toString", false);
+        prototype.setPropertyIsEnumerable("valueOf", false);
+
+        AS3 function toString():String {
+            if (this) {
+                return "true";
+            } else {
+                return "false";
+            }
+        }
+
+        AS3 function valueOf():Boolean {
+            return this;
+        }
+
+        public static const length:int = 1;
+    }
 }
diff --git a/core/src/avm2/globals/Toplevel.as b/core/src/avm2/globals/Toplevel.as
index 9787710f062ee..db4be5c71e316 100644
--- a/core/src/avm2/globals/Toplevel.as
+++ b/core/src/avm2/globals/Toplevel.as
@@ -32,6 +32,8 @@ package {
 
 include "Error.as"
 
+include "Boolean.as"
+
 include "ArgumentError.as"
 include "DefinitionError.as"
 include "EvalError.as"
diff --git a/core/src/avm2/globals/XML.as b/core/src/avm2/globals/XML.as
index 4c73cba25db04..06876fe03a1f3 100644
--- a/core/src/avm2/globals/XML.as
+++ b/core/src/avm2/globals/XML.as
@@ -103,15 +103,20 @@ package {
         AS3 native function notification():Function;
         AS3 native function setNotification(f:Function):*;
 
-        public static var ignoreComments:Boolean = true;
-        public static var ignoreProcessingInstructions:Boolean = true;
-        public static var ignoreWhitespace:Boolean = true;
+        public static native function get ignoreComments():Boolean;
+        public static native function set ignoreComments(value:Boolean):void;
 
-        [Ruffle(InternalSlot)]
-        public static var prettyPrinting:Boolean = true;
+        public static native function get ignoreProcessingInstructions():Boolean;
+        public static native function set ignoreProcessingInstructions(value:Boolean):void;
 
-        [Ruffle(InternalSlot)]
-        public static var prettyIndent:int = 2;
+        public static native function get ignoreWhitespace():Boolean;
+        public static native function set ignoreWhitespace(value:Boolean):void;
+
+        public static native function get prettyPrinting():Boolean;
+        public static native function set prettyPrinting(value:Boolean):void;
+
+        public static native function get prettyIndent():int;
+        public static native function set prettyIndent(value:int):void;
 
         prototype.hasComplexContent = function():Boolean {
             var self:XML = this;
diff --git a/core/src/avm2/globals/array.rs b/core/src/avm2/globals/array.rs
index f2e23733c5bcd..723349c1a8841 100644
--- a/core/src/avm2/globals/array.rs
+++ b/core/src/avm2/globals/array.rs
@@ -3,7 +3,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::array::ArrayStorage;
 use crate::avm2::class::Class;
-use crate::avm2::error::range_error;
+use crate::avm2::error::{make_error_1125, range_error};
 use crate::avm2::method::{Method, NativeMethodImpl};
 use crate::avm2::object::{array_allocator, ArrayObject, FunctionObject, Object, TObject};
 use crate::avm2::value::Value;
@@ -71,8 +71,6 @@ pub fn instance_init<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     let this = this.as_object().unwrap();
 
-    activation.super_init(this, &[])?;
-
     if let Some(mut array) = this.as_array_storage_mut(activation.gc()) {
         if args.len() == 1 {
             if let Some(expected_len) = args.get(0).filter(|v| v.is_number()).map(|v| v.as_f64()) {
@@ -105,12 +103,11 @@ pub fn class_call<'gc>(
     _this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .array
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 /// Implements `Array`'s class initializer.
@@ -229,6 +226,8 @@ pub fn resolve_array_hole<'gc>(
     }
 
     if let Some(proto) = this.proto() {
+        let proto = Value::from(proto);
+
         proto.get_public_property(
             AvmString::new_utf8(activation.gc(), i.to_string()),
             activation,
@@ -307,10 +306,10 @@ pub fn to_locale_string<'gc>(
     let this = this.as_object().unwrap();
 
     join_inner(act, this, &[",".into()], |v, activation| {
-        if let Ok(o) = v.coerce_to_object(activation) {
-            o.call_public_property("toLocaleString", &[], activation)
-        } else {
+        if matches!(v, Value::Null | Value::Undefined) {
             Ok(v)
+        } else {
+            v.call_public_property("toLocaleString", &[], activation)
         }
     })
 }
@@ -355,7 +354,7 @@ impl<'gc> ArrayIter<'gc> {
         start_index: u32,
         end_index: u32,
     ) -> Result<Self, Error<'gc>> {
-        let length = array_object
+        let length = Value::from(array_object)
             .get_public_property("length", activation)?
             .coerce_to_u32(activation)?;
 
@@ -373,22 +372,25 @@ impl<'gc> ArrayIter<'gc> {
     pub fn next(
         &mut self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Option<Result<(u32, Value<'gc>), Error<'gc>>> {
+    ) -> Result<Option<(u32, Value<'gc>)>, Error<'gc>> {
         if self.index < self.rev_index {
             let i = self.index;
 
             self.index += 1;
 
-            Some(
-                self.array_object
-                    .get_public_property(
-                        AvmString::new_utf8(activation.gc(), i.to_string()),
-                        activation,
-                    )
-                    .map(|val| (i, val)),
-            )
+            let val = self.array_object.get_index_property(i as usize);
+
+            let val = if let Some(storage) = self.array_object.as_vector_storage() {
+                // Special case for Vector- it throws an error if trying to access
+                // an element that was removed
+                val.ok_or_else(|| make_error_1125(activation, i as usize, storage.length()))?
+            } else {
+                val.unwrap_or(Value::Undefined)
+            };
+
+            Ok(Some((i, val)))
         } else {
-            None
+            Ok(None)
         }
     }
 
@@ -399,22 +401,25 @@ impl<'gc> ArrayIter<'gc> {
     pub fn next_back(
         &mut self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Option<Result<(u32, Value<'gc>), Error<'gc>>> {
+    ) -> Result<Option<(u32, Value<'gc>)>, Error<'gc>> {
         if self.index < self.rev_index {
             self.rev_index -= 1;
 
             let i = self.rev_index;
 
-            Some(
-                self.array_object
-                    .get_public_property(
-                        AvmString::new_utf8(activation.gc(), i.to_string()),
-                        activation,
-                    )
-                    .map(|val| (i, val)),
-            )
+            let val = self.array_object.get_index_property(i as usize);
+
+            let val = if let Some(storage) = self.array_object.as_vector_storage() {
+                // Special case for Vector- it throws an error if trying to access
+                // an element that was removed
+                val.ok_or_else(|| make_error_1125(activation, i as usize, storage.length()))?
+            } else {
+                val.unwrap_or(Value::Undefined)
+            };
+
+            Ok(Some((i, val)))
         } else {
-            None
+            Ok(None)
         }
     }
 }
@@ -431,9 +436,7 @@ pub fn for_each<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         callback.call(activation, receiver, &[item, i.into(), this.into()])?;
     }
 
@@ -453,8 +456,7 @@ pub fn map<'gc>(
     let mut new_array = ArrayStorage::new(0);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
+    while let Some((i, item)) = iter.next(activation)? {
         let new_item = callback.call(activation, receiver, &[item, i.into(), this.into()])?;
 
         new_array.push(new_item);
@@ -476,8 +478,7 @@ pub fn filter<'gc>(
     let mut new_array = ArrayStorage::new(0);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
+    while let Some((i, item)) = iter.next(activation)? {
         let is_allowed = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -502,9 +503,7 @@ pub fn every<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let result = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -529,9 +528,7 @@ pub fn some<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let result = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -1300,10 +1297,10 @@ pub fn sort_on<'gc>(
                     // if the object is null/undefined or does not have the field,
                     // it's treated as if the field's value was undefined.
                     // TODO: verify this and fix it
-                    let a_object = a.coerce_to_object(activation)?;
+                    let a_object = a.null_check(activation, None)?;
                     let a_field = a_object.get_public_property(*field_name, activation)?;
 
-                    let b_object = b.coerce_to_object(activation)?;
+                    let b_object = b.null_check(activation, None)?;
                     let b_field = b_object.get_public_property(*field_name, activation)?;
 
                     let ord = if options.contains(SortOptions::NUMERIC) {
diff --git a/core/src/avm2/globals/boolean.rs b/core/src/avm2/globals/boolean.rs
index 43cb8ecbbc778..bc1f78bd598e8 100644
--- a/core/src/avm2/globals/boolean.rs
+++ b/core/src/avm2/globals/boolean.rs
@@ -1,79 +1,20 @@
 //! `Boolean` impl
 
 use crate::avm2::activation::Activation;
-use crate::avm2::class::{Class, ClassAttributes};
-use crate::avm2::error::make_error_1004;
-use crate::avm2::method::{Method, NativeMethodImpl};
-use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::Error;
-use crate::avm2::QName;
 
-/// Implements `Boolean`'s instance initializer.
-fn instance_init<'gc>(
-    activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
+pub fn boolean_constructor<'gc>(
+    _activation: &mut Activation<'_, 'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(mut prim) = this.as_primitive_mut(activation.gc()) {
-        if matches!(*prim, Value::Undefined | Value::Null) {
-            *prim = args
-                .get(0)
-                .cloned()
-                .unwrap_or(Value::Bool(false))
-                .coerce_to_boolean()
-                .into();
-        }
-    }
-
-    Ok(Value::Undefined)
-}
-
-/// Implements `Boolean`'s class initializer.
-fn class_init<'gc>(
-    activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
-    _args: &[Value<'gc>],
-) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let scope = activation.create_scopechain();
-    let gc_context = activation.gc();
-    let this_class = this.as_class_object().unwrap();
-    let boolean_proto = this_class.prototype();
-
-    boolean_proto.set_string_property_local(
-        "toString",
-        FunctionObject::from_method(
-            activation,
-            Method::from_builtin(to_string, "toString", gc_context),
-            scope,
-            None,
-            None,
-            None,
-        )
-        .into(),
-        activation,
-    )?;
-    boolean_proto.set_string_property_local(
-        "valueOf",
-        FunctionObject::from_method(
-            activation,
-            Method::from_builtin(value_of, "valueOf", gc_context),
-            scope,
-            None,
-            None,
-            None,
-        )
-        .into(),
-        activation,
-    )?;
-    boolean_proto.set_local_property_is_enumerable(gc_context, "toString".into(), false);
-    boolean_proto.set_local_property_is_enumerable(gc_context, "valueOf".into(), false);
+    let bool_value = args
+        .get(0)
+        .copied()
+        .unwrap_or(Value::Bool(false))
+        .coerce_to_boolean();
 
-    Ok(Value::Undefined)
+    Ok(bool_value.into())
 }
 
 pub fn call_handler<'gc>(
@@ -88,85 +29,3 @@ pub fn call_handler<'gc>(
         .coerce_to_boolean()
         .into())
 }
-
-/// Implements `Boolean.prototype.toString`
-fn to_string<'gc>(
-    activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
-    _args: &[Value<'gc>],
-) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(this) = this.as_primitive() {
-        match *this {
-            Value::Bool(true) => return Ok("true".into()),
-            Value::Bool(false) => return Ok("false".into()),
-            _ => {}
-        };
-    }
-
-    let boolean_proto = activation.avm2().classes().boolean.prototype();
-    if Object::ptr_eq(boolean_proto, this) {
-        return Ok("false".into());
-    }
-
-    Err(make_error_1004(activation, "Boolean.prototype.toString"))
-}
-
-/// Implements `Boolean.valueOf`
-fn value_of<'gc>(
-    _activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
-    _args: &[Value<'gc>],
-) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(this) = this.as_primitive() {
-        return Ok(*this);
-    }
-
-    Ok(false.into())
-}
-
-/// Construct `Boolean`'s class.
-pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
-    let mc = activation.gc();
-    let namespaces = activation.avm2().namespaces;
-
-    let class = Class::new(
-        QName::new(namespaces.public_all(), "Boolean"),
-        Some(activation.avm2().class_defs().object),
-        Method::from_builtin(instance_init, "<Boolean instance initializer>", mc),
-        Method::from_builtin(class_init, "<Boolean class initializer>", mc),
-        activation.avm2().class_defs().class,
-        mc,
-    );
-
-    class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED);
-    class.set_instance_allocator(mc, primitive_allocator);
-    class.set_call_handler(
-        mc,
-        Method::from_builtin(call_handler, "<Boolean call handler>", mc),
-    );
-
-    const AS3_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] =
-        &[("toString", to_string), ("valueOf", value_of)];
-    class.define_builtin_instance_methods(mc, namespaces.as3, AS3_INSTANCE_METHODS);
-
-    const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
-    class.define_constant_int_class_traits(namespaces.public_all(), CONSTANTS_INT, activation);
-
-    class.mark_traits_loaded(activation.gc());
-    class
-        .init_vtable(activation.context)
-        .expect("Native class's vtable should initialize");
-
-    let c_class = class.c_class().expect("Class::new returns an i_class");
-
-    c_class.mark_traits_loaded(activation.gc());
-    c_class
-        .init_vtable(activation.context)
-        .expect("Native class's vtable should initialize");
-
-    class
-}
diff --git a/core/src/avm2/globals/date.rs b/core/src/avm2/globals/date.rs
index d0cb954152d95..e5718a565f9cf 100644
--- a/core/src/avm2/globals/date.rs
+++ b/core/src/avm2/globals/date.rs
@@ -247,24 +247,24 @@ pub fn call_handler<'gc>(
     _this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    Ok(activation
-        .avm2()
-        .classes()
-        .date
-        .construct(activation, &[])?
-        .into())
+    activation.avm2().classes().date.construct(activation, &[])
 }
 
 /// Implements `getTime` method.
 pub fn get_time<'gc>(
-    activation: &mut Activation<'_, 'gc>,
+    _activation: &mut Activation<'_, 'gc>,
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
     let this = this.as_object().unwrap();
 
     let this = this.as_date_object().unwrap();
-    this.value_of(activation.strings())
+
+    if let Some(date) = this.date_time() {
+        Ok((date.timestamp_millis() as f64).into())
+    } else {
+        Ok(f64::NAN.into())
+    }
 }
 
 /// Implements `setTime` method.
diff --git a/core/src/avm2/globals/error.rs b/core/src/avm2/globals/error.rs
index 442720c77f3e8..181590c38d626 100644
--- a/core/src/avm2/globals/error.rs
+++ b/core/src/avm2/globals/error.rs
@@ -10,12 +10,11 @@ pub fn call_handler<'gc>(
     _this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .error
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 pub fn get_stack_trace<'gc>(
diff --git a/core/src/avm2/globals/flash/crypto.rs b/core/src/avm2/globals/flash/crypto.rs
index 53821b2b16afc..2b1bee8d72037 100644
--- a/core/src/avm2/globals/flash/crypto.rs
+++ b/core/src/avm2/globals/flash/crypto.rs
@@ -19,8 +19,14 @@ pub fn generate_random_bytes<'gc>(
         return Err(make_error_2004(activation, Error2004Type::Error));
     }
 
-    let ba_class = activation.context.avm2.classes().bytearray;
-    let ba = ba_class.construct(activation, &[])?;
+    let ba = activation
+        .avm2()
+        .classes()
+        .bytearray
+        .construct(activation, &[])?
+        .as_object()
+        .unwrap();
+
     let mut ba_write = ba.as_bytearray_mut().unwrap();
     ba_write.set_length(length as usize);
 
diff --git a/core/src/avm2/globals/flash/display/bitmap_data.rs b/core/src/avm2/globals/flash/display/bitmap_data.rs
index 738fdd19dc129..be72552781faa 100644
--- a/core/src/avm2/globals/flash/display/bitmap_data.rs
+++ b/core/src/avm2/globals/flash/display/bitmap_data.rs
@@ -754,8 +754,8 @@ pub fn get_color_bounds_rect<'gc>(
                 .avm2()
                 .classes()
                 .rectangle
-                .construct(activation, &[x.into(), y.into(), w.into(), h.into()])?
-                .into();
+                .construct(activation, &[x.into(), y.into(), w.into(), h.into()])?;
+
             return Ok(rect);
         }
     }
@@ -1144,20 +1144,15 @@ pub fn get_rect<'gc>(
     let this = this.as_object().unwrap();
 
     if let Some(bitmap_data) = this.as_bitmap_data() {
-        return Ok(activation
-            .avm2()
-            .classes()
-            .rectangle
-            .construct(
-                activation,
-                &[
-                    0.into(),
-                    0.into(),
-                    bitmap_data.width().into(),
-                    bitmap_data.height().into(),
-                ],
-            )?
-            .into());
+        return activation.avm2().classes().rectangle.construct(
+            activation,
+            &[
+                0.into(),
+                0.into(),
+                bitmap_data.width().into(),
+                bitmap_data.height().into(),
+            ],
+        );
     }
     Ok(Value::Undefined)
 }
diff --git a/core/src/avm2/globals/flash/display/display_object.rs b/core/src/avm2/globals/flash/display/display_object.rs
index ae63b879c4808..adb51d3a08831 100644
--- a/core/src/avm2/globals/flash/display/display_object.rs
+++ b/core/src/avm2/globals/flash/display/display_object.rs
@@ -58,10 +58,10 @@ pub fn display_object_initializer<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
     activation.super_init(this, &[])?;
 
+    let this = this.as_object().unwrap();
+
     if let Some(dobj) = this.as_display_object() {
         if let Some(clip) = dobj.as_movie_clip() {
             clip.set_constructing_frame(true, activation.gc());
@@ -157,8 +157,7 @@ pub fn get_scale9grid<'gc>(
     if let Some(dobj) = this.as_display_object() {
         let rect = dobj.scaling_grid();
         return if rect.is_valid() {
-            let rect = new_rectangle(activation, rect)?;
-            Ok(rect.into())
+            new_rectangle(activation, rect)
         } else {
             Ok(Value::Null)
         };
@@ -812,12 +811,11 @@ pub fn get_transform<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     let this = this.as_object().unwrap();
 
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .transform
-        .construct(activation, &[this.into()])?
-        .into())
+        .construct(activation, &[this.into()])
 }
 
 pub fn set_transform<'gc>(
@@ -887,7 +885,7 @@ pub fn set_blend_mode<'gc>(
 fn new_rectangle<'gc>(
     activation: &mut Activation<'_, 'gc>,
     rectangle: Rectangle<Twips>,
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let x = rectangle.x_min.to_pixels();
     let y = rectangle.y_min.to_pixels();
     let width = rectangle.width().to_pixels();
@@ -909,7 +907,7 @@ pub fn get_scroll_rect<'gc>(
 
     if let Some(dobj) = this.as_display_object() {
         if dobj.has_scroll_rect() {
-            return Ok(new_rectangle(activation, dobj.next_scroll_rect())?.into());
+            return new_rectangle(activation, dobj.next_scroll_rect());
         } else {
             return Ok(Value::Null);
         }
@@ -987,15 +985,10 @@ pub fn local_to_global<'gc>(
 
         let local = Point::from_pixels(x, y);
         let global = dobj.local_to_global(local);
-        return Ok(activation
-            .avm2()
-            .classes()
-            .point
-            .construct(
-                activation,
-                &[global.x.to_pixels().into(), global.y.to_pixels().into()],
-            )?
-            .into());
+        return activation.avm2().classes().point.construct(
+            activation,
+            &[global.x.to_pixels().into(), global.y.to_pixels().into()],
+        );
     }
 
     Ok(Value::Undefined)
@@ -1019,15 +1012,10 @@ pub fn global_to_local<'gc>(
 
         let global = Point::from_pixels(x, y);
         let local = dobj.global_to_local(global).unwrap_or(global);
-        return Ok(activation
-            .avm2()
-            .classes()
-            .point
-            .construct(
-                activation,
-                &[local.x.to_pixels().into(), local.y.to_pixels().into()],
-            )?
-            .into());
+        return activation.avm2().classes().point.construct(
+            activation,
+            &[local.x.to_pixels().into(), local.y.to_pixels().into()],
+        );
     }
 
     Ok(Value::Undefined)
@@ -1062,7 +1050,7 @@ pub fn get_bounds<'gc>(
             out_bounds = Rectangle::ZERO;
         }
 
-        return Ok(new_rectangle(activation, out_bounds)?.into());
+        return new_rectangle(activation, out_bounds);
     }
     Ok(Value::Undefined)
 }
diff --git a/core/src/avm2/globals/flash/display/loader.rs b/core/src/avm2/globals/flash/display/loader.rs
index 353699d844d4a..55a902cdb059a 100644
--- a/core/src/avm2/globals/flash/display/loader.rs
+++ b/core/src/avm2/globals/flash/display/loader.rs
@@ -149,7 +149,7 @@ pub fn request_from_url_request<'gc>(
         let headers = headers.as_array_storage().unwrap();
 
         for i in 0..headers.length() {
-            let Some(header) = headers.get(i).and_then(|val| val.as_object()) else {
+            let Some(header) = headers.get(i) else {
                 continue;
             };
 
diff --git a/core/src/avm2/globals/flash/display/loader_info.rs b/core/src/avm2/globals/flash/display/loader_info.rs
index fac88bea45eaa..86f534e5a815c 100644
--- a/core/src/avm2/globals/flash/display/loader_info.rs
+++ b/core/src/avm2/globals/flash/display/loader_info.rs
@@ -418,13 +418,11 @@ pub fn get_bytes<'gc>(
     let (root, dobj) = match &*loader_stream {
         LoaderStream::NotYetLoaded(_, None, _) => {
             if loader_info.errored() {
-                return Ok(activation
-                    .context
-                    .avm2
+                return activation
+                    .avm2()
                     .classes()
                     .bytearray
-                    .construct(activation, &[])?
-                    .into());
+                    .construct(activation, &[]);
             }
             // If we haven't even started loading yet (we have no root clip),
             // then return null. FIXME - we should probably store the ByteArray
@@ -435,8 +433,13 @@ pub fn get_bytes<'gc>(
         LoaderStream::Swf(root, dobj) => (root, dobj),
     };
 
-    let ba_class = activation.context.avm2.classes().bytearray;
-    let ba = ba_class.construct(activation, &[])?;
+    let ba = activation
+        .avm2()
+        .classes()
+        .bytearray
+        .construct(activation, &[])?
+        .as_object()
+        .unwrap();
 
     if root.data().is_empty() {
         return Ok(ba.into());
diff --git a/core/src/avm2/globals/flash/display/movie_clip.rs b/core/src/avm2/globals/flash/display/movie_clip.rs
index 796f72539dee7..b017920dca7cb 100644
--- a/core/src/avm2/globals/flash/display/movie_clip.rs
+++ b/core/src/avm2/globals/flash/display/movie_clip.rs
@@ -135,7 +135,7 @@ fn labels_for_scene<'gc>(
         let args = [name, local_frame.into()];
         let frame_label = frame_label_class.construct(activation, &args)?;
 
-        frame_labels.push(Some(frame_label.into()));
+        frame_labels.push(Some(frame_label));
     }
 
     Ok((
@@ -195,7 +195,7 @@ pub fn get_current_scene<'gc>(
 
         let scene = scene_class.construct(activation, &args)?;
 
-        return Ok(scene.into());
+        return Ok(scene);
     }
 
     Ok(Value::Undefined)
@@ -271,7 +271,7 @@ pub fn get_scenes<'gc>(
 
             let scene = scene_class.construct(activation, &args)?;
 
-            scene_objects.push(Some(scene.into()));
+            scene_objects.push(Some(scene));
         }
 
         return Ok(ArrayObject::from_storage(
diff --git a/core/src/avm2/globals/flash/display/shader_parameter.rs b/core/src/avm2/globals/flash/display/shader_parameter.rs
index f5f106d8b23da..7ab1880abf8ab 100644
--- a/core/src/avm2/globals/flash/display/shader_parameter.rs
+++ b/core/src/avm2/globals/flash/display/shader_parameter.rs
@@ -19,37 +19,43 @@ pub fn make_shader_parameter<'gc>(
             metadata,
             ..
         } => {
-            let obj = activation
+            let param_value = activation
                 .avm2()
                 .classes()
                 .shaderparameter
                 .construct(activation, &[])?;
+
+            let param_object = param_value.as_object().unwrap();
+
             let type_name = AvmString::new_utf8(activation.gc(), param_type.to_string());
 
-            obj.set_slot(parameter_slots::_INDEX, index.into(), activation)?;
-            obj.set_slot(parameter_slots::_TYPE, type_name.into(), activation)?;
+            param_object.set_slot(parameter_slots::_INDEX, index.into(), activation)?;
+            param_object.set_slot(parameter_slots::_TYPE, type_name.into(), activation)?;
             for meta in metadata {
                 let name = AvmString::new_utf8(activation.gc(), &meta.key);
                 let value = meta.value.clone().as_avm2_value(activation, false)?;
-                obj.set_public_property(name, value, activation)?;
+                param_value.set_public_property(name, value, activation)?;
 
                 if &*name == b"defaultValue" {
-                    obj.set_slot(parameter_slots::_VALUE, value, activation)?;
+                    param_object.set_slot(parameter_slots::_VALUE, value, activation)?;
                 }
             }
-            obj.set_string_property_local(
+            param_object.set_string_property_local(
                 "name",
                 AvmString::new_utf8(activation.gc(), name).into(),
                 activation,
             )?;
-            Ok(obj.into())
+            Ok(param_value)
         }
         PixelBenderParam::Texture { name, channels, .. } => {
             let obj = activation
                 .avm2()
                 .classes()
                 .shaderinput
-                .construct(activation, &[])?;
+                .construct(activation, &[])?
+                .as_object()
+                .unwrap();
+
             obj.set_slot(input_slots::_CHANNELS, (*channels).into(), activation)?;
             obj.set_slot(input_slots::_INDEX, index.into(), activation)?;
             obj.set_string_property_local(
diff --git a/core/src/avm2/globals/flash/display/stage_3d.rs b/core/src/avm2/globals/flash/display/stage_3d.rs
index a753de22ae043..0a90eb5d19750 100644
--- a/core/src/avm2/globals/flash/display/stage_3d.rs
+++ b/core/src/avm2/globals/flash/display/stage_3d.rs
@@ -35,9 +35,9 @@ pub fn request_context3d_internal<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
+    let this_object = this.as_object().unwrap();
 
-    let this_stage3d = this.as_stage_3d().unwrap();
+    let this_stage3d = this_object.as_stage_3d().unwrap();
     let profiles = args.get_object(activation, 1, "profiles")?;
     let profiles = profiles.as_vector_storage().unwrap();
 
@@ -68,9 +68,7 @@ pub fn request_context3d_internal<'gc>(
             .event
             .construct(activation, &["context3DCreate".into()])?;
 
-        // FIXME - fire this at least one frame later,
-        // since some seems to expect this (e.g. the adobe triangle example)
-        this.call_public_property("dispatchEvent", &[event.into()], activation)?;
+        this.call_public_property("dispatchEvent", &[event], activation)?;
     }
 
     Ok(Value::Undefined)
diff --git a/core/src/avm2/globals/flash/display3D/context_3d.rs b/core/src/avm2/globals/flash/display3D/context_3d.rs
index 43dd4c9e287f8..d07c8b2953411 100644
--- a/core/src/avm2/globals/flash/display3D/context_3d.rs
+++ b/core/src/avm2/globals/flash/display3D/context_3d.rs
@@ -320,12 +320,12 @@ pub fn set_program_constants_from_matrix<'gc>(
         // in column-major order.
         // See https://github.com/openfl/openfl/blob/971a4c9e43b5472fd84d73920a2b7c1b3d8d9257/src/openfl/display3D/Context3D.hx#L1532-L1550
         if user_transposed_matrix {
-            matrix = matrix
+            matrix = Value::from(matrix)
                 .call_public_property("clone", &[], activation)?
                 .as_object()
                 .expect("Matrix3D.clone returns Object");
 
-            matrix.call_public_property("transpose", &[], activation)?;
+            Value::from(matrix).call_public_property("transpose", &[], activation)?;
         }
 
         let matrix_raw_data = matrix
diff --git a/core/src/avm2/globals/flash/events/event_dispatcher.rs b/core/src/avm2/globals/flash/events/event_dispatcher.rs
index c516689d45ff1..d3b2a405188ce 100644
--- a/core/src/avm2/globals/flash/events/event_dispatcher.rs
+++ b/core/src/avm2/globals/flash/events/event_dispatcher.rs
@@ -128,11 +128,11 @@ pub fn dispatch_event<'gc>(
 
     let event = args.get_object(activation, 0, "event")?;
 
-    if event.as_event().is_none() {
-        return Err("Dispatched Events must be subclasses of Event.".into());
-    }
+    // AS3-side typing guarantees that the event is actually an Event
+    let event = event.as_event_object().unwrap();
 
     dispatch_event_internal(activation, this, event, false)?;
-    let not_canceled = !event.as_event().unwrap().is_cancelled();
+
+    let not_canceled = !event.event().is_cancelled();
     Ok(not_canceled.into())
 }
diff --git a/core/src/avm2/globals/flash/geom/transform.rs b/core/src/avm2/globals/flash/geom/transform.rs
index 2f8e2a1ebe0cf..d8b217f0d7a98 100644
--- a/core/src/avm2/globals/flash/geom/transform.rs
+++ b/core/src/avm2/globals/flash/geom/transform.rs
@@ -188,7 +188,7 @@ pub fn color_transform_to_object<'gc>(
     ];
     let ct_class = activation.avm2().classes().colortransform;
     let object = ct_class.construct(activation, &args)?;
-    Ok(object.into())
+    Ok(object)
 }
 
 pub fn matrix_to_object<'gc>(
@@ -208,7 +208,7 @@ pub fn matrix_to_object<'gc>(
         .classes()
         .matrix
         .construct(activation, &args)?;
-    Ok(object.into())
+    Ok(object)
 }
 
 pub fn object_to_matrix<'gc>(
@@ -265,7 +265,7 @@ fn rectangle_to_object<'gc>(
             rectangle.height().to_pixels().into(),
         ],
     )?;
-    Ok(object.into())
+    Ok(object)
 }
 
 pub fn get_matrix_3d<'gc>(
@@ -284,7 +284,7 @@ pub fn get_matrix_3d<'gc>(
             .classes()
             .matrix3d
             .construct(activation, &[])?;
-        Ok(object.into())
+        Ok(object)
     } else {
         Ok(Value::Null)
     }
@@ -332,7 +332,8 @@ pub fn get_perspective_projection<'gc>(
             .classes()
             .perspectiveprojection
             .construct(activation, &[])?;
-        Ok(object.into())
+
+        Ok(object)
     } else {
         Ok(Value::Null)
     }
diff --git a/core/src/avm2/globals/flash/media/sound.rs b/core/src/avm2/globals/flash/media/sound.rs
index 96ded9c0679dd..5c8e7ee1693d5 100644
--- a/core/src/avm2/globals/flash/media/sound.rs
+++ b/core/src/avm2/globals/flash/media/sound.rs
@@ -2,7 +2,7 @@
 
 use crate::avm2::activation::Activation;
 use crate::avm2::globals::slots::flash_net_url_request as url_request_slots;
-use crate::avm2::object::{QueuedPlay, SoundChannelObject, TObject};
+use crate::avm2::object::{EventObject, QueuedPlay, SoundChannelObject, TObject};
 use crate::avm2::parameters::ParametersExt;
 use crate::avm2::value::Value;
 use crate::avm2::Avm2;
@@ -10,7 +10,6 @@ use crate::avm2::Error;
 use crate::backend::navigator::Request;
 use crate::character::Character;
 use crate::display_object::SoundTransform;
-use crate::string::AvmString;
 use crate::{avm2_stub_getter, avm2_stub_method};
 use swf::{SoundEvent, SoundInfo};
 
@@ -22,10 +21,8 @@ pub fn init<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(sound_object) = this.as_sound_object() {
-        let class_def = this.instance_class();
+    if let Some(sound_object) = this.as_object().and_then(|o| o.as_sound_object()) {
+        let class_def = this.instance_class(activation);
 
         if let Some((movie, symbol)) = activation
             .context
@@ -286,21 +283,8 @@ pub fn load_compressed_data_from_byte_array<'gc>(
         Error::RustError(format!("Failed to register sound from bytearray: {e:?}").into())
     })?;
 
-    let progress_evt = activation
-        .avm2()
-        .classes()
-        .progressevent
-        .construct(
-            activation,
-            &[
-                "progress".into(),
-                false.into(),
-                false.into(),
-                bytes.len().into(),
-                bytes.len().into(),
-            ],
-        )
-        .map_err(|e| Error::AvmError(AvmString::new_utf8(activation.gc(), e.to_string()).into()))?;
+    let progress_evt =
+        EventObject::progress_event(activation, "progress", bytes.len(), bytes.len());
 
     Avm2::dispatch_event(activation.context, progress_evt, this);
 
diff --git a/core/src/avm2/globals/flash/net.rs b/core/src/avm2/globals/flash/net.rs
index 75c1be895007f..e4c5bb5bb0451 100644
--- a/core/src/avm2/globals/flash/net.rs
+++ b/core/src/avm2/globals/flash/net.rs
@@ -30,7 +30,7 @@ fn object_to_index_map<'gc>(
             .get_enumerant_name(last_index, activation)?
             .coerce_to_string(activation)?;
         let value = obj
-            .get_public_property(name, activation)?
+            .get_enumerant_value(last_index, activation)?
             .coerce_to_string(activation)?
             .to_utf8_lossy()
             .to_string();
diff --git a/core/src/avm2/globals/flash/net/file_reference.rs b/core/src/avm2/globals/flash/net/file_reference.rs
index 49ff0c3c3bbae..db2380a8b65e4 100644
--- a/core/src/avm2/globals/flash/net/file_reference.rs
+++ b/core/src/avm2/globals/flash/net/file_reference.rs
@@ -222,17 +222,18 @@ pub fn load<'gc>(
         FileReference::FileDialogResult(ref dialog_result) => dialog_result.size().unwrap_or(0),
     };
 
+    let size = size as usize;
+
     let open_evt = EventObject::bare_default_event(activation.context, "open");
     Avm2::dispatch_event(activation.context, open_evt, this.into());
 
-    let progress_evt = EventObject::progress_event(activation, "progress", 0, size, false, false);
+    let progress_evt = EventObject::progress_event(activation, "progress", 0, size);
     Avm2::dispatch_event(activation.context, progress_evt, this.into());
 
     let open_evt2 = EventObject::bare_default_event(activation.context, "open");
     Avm2::dispatch_event(activation.context, open_evt2, this.into());
 
-    let progress_evt2 =
-        EventObject::progress_event(activation, "progress", size, size, false, false);
+    let progress_evt2 = EventObject::progress_event(activation, "progress", size, size);
     Avm2::dispatch_event(activation.context, progress_evt2, this.into());
 
     this.set_loaded(true);
diff --git a/core/src/avm2/globals/flash/net/shared_object.rs b/core/src/avm2/globals/flash/net/shared_object.rs
index 47328648bf90b..af65cc5b4f42c 100644
--- a/core/src/avm2/globals/flash/net/shared_object.rs
+++ b/core/src/avm2/globals/flash/net/shared_object.rs
@@ -1,8 +1,8 @@
 //! `flash.net.SharedObject` builtin/prototype
 
 use crate::avm2::error::error;
-use crate::avm2::object::TObject;
-pub use crate::avm2::object::{shared_object_allocator, SharedObjectObject};
+pub use crate::avm2::object::shared_object_allocator;
+use crate::avm2::object::{ScriptObject, SharedObjectObject, TObject};
 use crate::avm2::{Activation, Error, Object, Value};
 use crate::{avm2_stub_getter, avm2_stub_method, avm2_stub_setter};
 use flash_lso::types::{AMFVersion, Lso};
@@ -159,11 +159,7 @@ pub fn get_local<'gc>(
         data
     } else {
         // No data; create a fresh data object.
-        activation
-            .avm2()
-            .classes()
-            .object
-            .construct(activation, &[])?
+        ScriptObject::new_object(activation)
     };
 
     let created_shared_object =
diff --git a/core/src/avm2/globals/flash/text/engine/text_block.rs b/core/src/avm2/globals/flash/text/engine/text_block.rs
index 9fd206c6088c5..450e6f0b2b3be 100644
--- a/core/src/avm2/globals/flash/text/engine/text_block.rs
+++ b/core/src/avm2/globals/flash/text/engine/text_block.rs
@@ -31,7 +31,7 @@ pub fn create_text_line<'gc>(
     let content = if matches!(content, Value::Null) {
         return Ok(Value::Null);
     } else {
-        content.as_object().unwrap()
+        content
     };
 
     let text = match previous_text_line {
@@ -76,6 +76,7 @@ pub fn create_text_line<'gc>(
     // of the provided text, and set the width of the EditText to that.
     // Some games depend on this (e.g. Realm Grinder).
 
+    let content = content.as_object().unwrap();
     let element_format = content.get_slot(element_slots::_ELEMENT_FORMAT).as_object();
 
     apply_format(activation, display_object, text.as_wstr(), element_format)?;
diff --git a/core/src/avm2/globals/flash/text/text_field.rs b/core/src/avm2/globals/flash/text/text_field.rs
index ff3256e7b9f4f..921f6a6debd6f 100644
--- a/core/src/avm2/globals/flash/text/text_field.rs
+++ b/core/src/avm2/globals/flash/text/text_field.rs
@@ -1245,19 +1245,17 @@ pub fn get_line_metrics<'gc>(
     };
 
     let metrics_class = activation.avm2().classes().textlinemetrics;
-    Ok(metrics_class
-        .construct(
-            activation,
-            &[
-                metrics.x.to_pixels().into(),
-                metrics.width.to_pixels().into(),
-                metrics.height.to_pixels().into(),
-                metrics.ascent.to_pixels().into(),
-                metrics.descent.to_pixels().into(),
-                metrics.leading.to_pixels().into(),
-            ],
-        )?
-        .into())
+    metrics_class.construct(
+        activation,
+        &[
+            metrics.x.to_pixels().into(),
+            metrics.width.to_pixels().into(),
+            metrics.height.to_pixels().into(),
+            metrics.ascent.to_pixels().into(),
+            metrics.descent.to_pixels().into(),
+            metrics.leading.to_pixels().into(),
+        ],
+    )
 }
 
 pub fn get_line_length<'gc>(
@@ -1788,20 +1786,16 @@ pub fn get_char_boundaries<'gc>(
         return Ok(Value::Null);
     }
 
-    let rect = activation
-        .avm2()
-        .classes()
-        .rectangle
-        .construct(
-            activation,
-            &[
-                bounds.x_min.to_pixels().into(),
-                bounds.y_min.to_pixels().into(),
-                bounds.width().to_pixels().into(),
-                bounds.height().to_pixels().into(),
-            ],
-        )?
-        .into();
+    let rect = activation.avm2().classes().rectangle.construct(
+        activation,
+        &[
+            bounds.x_min.to_pixels().into(),
+            bounds.y_min.to_pixels().into(),
+            bounds.width().to_pixels().into(),
+            bounds.height().to_pixels().into(),
+        ],
+    )?;
+
     Ok(rect)
 }
 
diff --git a/core/src/avm2/globals/function.rs b/core/src/avm2/globals/function.rs
index 39467608462c8..7a0c5eb2cef12 100644
--- a/core/src/avm2/globals/function.rs
+++ b/core/src/avm2/globals/function.rs
@@ -13,11 +13,9 @@ use crate::avm2::QName;
 /// Implements `Function`'s instance initializer.
 pub fn instance_init<'gc>(
     activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
+    _this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
     if !args.is_empty() {
         return Err(Error::AvmError(eval_error(
             activation,
@@ -26,8 +24,6 @@ pub fn instance_init<'gc>(
         )?));
     }
 
-    activation.super_init(this, &[])?;
-
     Ok(Value::Undefined)
 }
 
@@ -36,12 +32,11 @@ pub fn class_call<'gc>(
     _this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .function
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 /// Implements `Function`'s class initializer.
diff --git a/core/src/avm2/globals/int.rs b/core/src/avm2/globals/int.rs
index 8204639f9fdfa..671dbf9ebfb08 100644
--- a/core/src/avm2/globals/int.rs
+++ b/core/src/avm2/globals/int.rs
@@ -5,30 +5,32 @@ use crate::avm2::class::{Class, ClassAttributes};
 use crate::avm2::error::{make_error_1003, make_error_1004};
 use crate::avm2::globals::number::print_with_radix;
 use crate::avm2::method::{Method, NativeMethodImpl, ParamConfig};
-use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
+use crate::avm2::object::{FunctionObject, Object, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::{AvmString, Error, QName};
 
 /// Implements `int`'s instance initializer.
+///
+/// Because of the presence of a custom constructor, this method is unreachable.
 fn instance_init<'gc>(
+    _activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    unreachable!()
+}
+
+fn int_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(mut prim) = this.as_primitive_mut(activation.gc()) {
-        if matches!(*prim, Value::Undefined | Value::Null) {
-            *prim = args
-                .get(0)
-                .cloned()
-                .unwrap_or(Value::Undefined)
-                .coerce_to_i32(activation)?
-                .into();
-        }
-    }
+    let int_value = args
+        .get(0)
+        .copied()
+        .unwrap_or(Value::Integer(0))
+        .coerce_to_i32(activation)?;
 
-    Ok(Value::Undefined)
+    Ok(int_value.into())
 }
 
 /// Implements `int`'s class initializer.
@@ -161,20 +163,16 @@ fn to_string<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let int_proto = activation.avm2().classes().int.prototype();
-    if Object::ptr_eq(int_proto, this) {
-        return Ok("0".into());
+    if let Some(this) = this.as_object() {
+        let int_proto = activation.avm2().classes().int.prototype();
+        if Object::ptr_eq(int_proto, this) {
+            return Ok("0".into());
+        }
     }
 
-    let number = if let Some(this) = this.as_primitive() {
-        match *this {
-            Value::Integer(o) => o,
-            _ => return Err(make_error_1004(activation, "int.prototype.toString")),
-        }
-    } else {
-        return Err(make_error_1004(activation, "int.prototype.toString"));
+    let number = match this {
+        Value::Integer(o) => o,
+        _ => return Err(make_error_1004(activation, "int.prototype.toString")),
     };
 
     let radix = args
@@ -196,22 +194,16 @@ fn value_of<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let int_proto = activation.avm2().classes().int.prototype();
-    if Object::ptr_eq(int_proto, this) {
-        return Ok(0.into());
+    if let Some(this) = this.as_object() {
+        let int_proto = activation.avm2().classes().int.prototype();
+        if Object::ptr_eq(int_proto, this) {
+            return Ok(0.into());
+        }
     }
 
-    let primitive = this.as_primitive();
-
-    if let Some(this) = primitive {
-        match *this {
-            Value::Integer(_) => Ok(*this),
-            _ => Err(make_error_1004(activation, "int.prototype.valueOf")),
-        }
-    } else {
-        Err(make_error_1004(activation, "int.prototype.valueOf"))
+    match this {
+        Value::Integer(_) => Ok(this),
+        _ => Err(make_error_1004(activation, "int.prototype.valueOf")),
     }
 }
 
@@ -241,7 +233,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
     );
 
     class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED);
-    class.set_instance_allocator(mc, primitive_allocator);
+    class.set_custom_constructor(mc, int_constructor);
     class.set_call_handler(
         mc,
         Method::from_builtin(call_handler, "<int call handler>", mc),
diff --git a/core/src/avm2/globals/json.rs b/core/src/avm2/globals/json.rs
index 57b417df9f14e..12e9111578b00 100644
--- a/core/src/avm2/globals/json.rs
+++ b/core/src/avm2/globals/json.rs
@@ -117,12 +117,12 @@ impl<'gc> AvmSerializer<'gc> {
         key: impl Fn() -> AvmString<'gc>,
         value: Value<'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
-        let (eval_key, value) = if let Some(obj) = value.as_object() {
-            if obj.has_public_property("toJSON", activation) {
+        let (eval_key, value) = if value.as_object().is_some() {
+            if value.has_public_property("toJSON", activation) {
                 let key = key();
                 (
                     Some(key),
-                    obj.call_public_property("toJSON", &[key.into()], activation)?,
+                    value.call_public_property("toJSON", &[key.into()], activation)?,
                 )
             } else {
                 (None, value)
@@ -151,10 +151,9 @@ impl<'gc> AvmSerializer<'gc> {
         // If the user supplied a PropList, we use that to find properties on the object.
         if let Some(Replacer::PropList(props)) = self.replacer {
             let mut iter = ArrayIter::new(activation, props.into())?;
-            while let Some(r) = iter.next(activation) {
-                let item = r?.1;
+            while let Some((_, item)) = iter.next(activation)? {
                 let key = item.coerce_to_string(activation)?;
-                let value = obj.get_public_property(key, activation)?;
+                let value = Value::from(obj).get_public_property(key, activation)?;
                 let mapped = self.map_value(activation, || key, value)?;
                 if !matches!(mapped, Value::Undefined) {
                     js_obj.insert(
@@ -178,7 +177,7 @@ impl<'gc> AvmSerializer<'gc> {
                     Value::Null => break,
                     name_val => {
                         let name = name_val.coerce_to_string(activation)?;
-                        let value = obj.get_public_property(name, activation)?;
+                        let value = obj.get_enumerant_value(i, activation)?;
                         let mapped = self.map_value(activation, || name, value)?;
                         if !matches!(mapped, Value::Undefined) {
                             js_obj.insert(
@@ -202,8 +201,7 @@ impl<'gc> AvmSerializer<'gc> {
     ) -> Result<JsonValue, Error<'gc>> {
         let mut js_arr = Vec::new();
         let mut iter = ArrayIter::new(activation, iterable)?;
-        while let Some(r) = iter.next(activation) {
-            let (i, item) = r?;
+        while let Some((i, item)) = iter.next(activation)? {
             let mc = activation.gc();
             let mapped =
                 self.map_value(activation, || AvmString::new_utf8(mc, i.to_string()), item)?;
@@ -225,10 +223,6 @@ impl<'gc> AvmSerializer<'gc> {
             Value::Bool(b) => JsonValue::from(b),
             Value::String(s) => JsonValue::from(s.to_utf8_lossy().deref()),
             Value::Object(obj) => {
-                // special case for boxed primitives
-                if let Some(prim) = obj.as_primitive() {
-                    return self.serialize_value(activation, *prim);
-                }
                 if self.obj_stack.contains(&obj) {
                     return Err(Error::AvmError(type_error(
                         activation,
@@ -271,7 +265,8 @@ pub fn parse<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     let input = args.get_string(activation, 0)?;
     let reviver = args
-        .try_get_object(activation, 1)
+        .get_value(1)
+        .as_object()
         .map(|o| o.as_function_object().unwrap());
 
     let parsed = if let Ok(parsed) = serde_json::from_str(&input.to_utf8_lossy()) {
@@ -294,11 +289,11 @@ pub fn stringify<'gc>(
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
     let val = args.get_value(0);
-    let replacer = args.try_get_object(activation, 1);
+    let replacer = args.get_value(1).as_object();
     let spaces = args.get_value(2);
 
     // If the replacer is None, that means it was either undefined or null.
-    if replacer.is_none() && !matches!(args.get(1).unwrap(), Value::Null) {
+    if replacer.is_none() && !matches!(args.get_value(1), Value::Null) {
         return Err(Error::AvmError(type_error(
             activation,
             "Error #1131: Replacer argument to JSON stringifier must be an array or a two parameter function.",
diff --git a/core/src/avm2/globals/namespace.rs b/core/src/avm2/globals/namespace.rs
index 59ec3915b5084..b1f982c4d80fc 100644
--- a/core/src/avm2/globals/namespace.rs
+++ b/core/src/avm2/globals/namespace.rs
@@ -12,7 +12,7 @@ use crate::avm2::Namespace;
 pub fn namespace_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
     args: &[Value<'gc>],
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let api_version = activation.avm2().root_api_version;
     let namespaces = activation.avm2().namespaces;
 
@@ -80,12 +80,11 @@ pub fn call_handler<'gc>(
     _this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .namespace
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 /// Implements `Namespace.prefix`'s getter
diff --git a/core/src/avm2/globals/number.rs b/core/src/avm2/globals/number.rs
index 9a19ce5432192..b58c93fd7df2c 100644
--- a/core/src/avm2/globals/number.rs
+++ b/core/src/avm2/globals/number.rs
@@ -4,31 +4,33 @@ use crate::avm2::activation::Activation;
 use crate::avm2::class::{Class, ClassAttributes};
 use crate::avm2::error::{make_error_1002, make_error_1003, make_error_1004};
 use crate::avm2::method::{Method, NativeMethodImpl};
-use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
+use crate::avm2::object::{FunctionObject, Object, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::QName;
 use crate::avm2::{AvmString, Error};
 
 /// Implements `Number`'s instance initializer.
+///
+/// Because of the presence of a custom constructor, this method is unreachable.
 fn instance_init<'gc>(
+    _activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    unreachable!()
+}
+
+fn number_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(mut prim) = this.as_primitive_mut(activation.gc()) {
-        if matches!(*prim, Value::Undefined | Value::Null) {
-            *prim = args
-                .get(0)
-                .cloned()
-                .unwrap_or(Value::Number(0.0))
-                .coerce_to_number(activation)?
-                .into();
-        }
-    }
+    let number_value = args
+        .get(0)
+        .copied()
+        .unwrap_or(Value::Integer(0))
+        .coerce_to_number(activation)?;
 
-    Ok(Value::Undefined)
+    Ok(number_value.into())
 }
 
 /// Implements `Number`'s class initializer.
@@ -304,21 +306,17 @@ fn to_string<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let number_proto = activation.avm2().classes().number.prototype();
-    if Object::ptr_eq(number_proto, this) {
-        return Ok("0".into());
+    if let Some(this) = this.as_object() {
+        let number_proto = activation.avm2().classes().number.prototype();
+        if Object::ptr_eq(number_proto, this) {
+            return Ok("0".into());
+        }
     }
 
-    let number = if let Some(this) = this.as_primitive() {
-        match *this {
-            Value::Integer(o) => o as f64,
-            Value::Number(o) => o,
-            _ => return Err(make_error_1004(activation, "Number.prototype.toString")),
-        }
-    } else {
-        return Err(make_error_1004(activation, "Number.prototype.toString"));
+    let number = match this {
+        Value::Integer(o) => o as f64,
+        Value::Number(o) => o,
+        _ => return Err(make_error_1004(activation, "Number.prototype.toString")),
     };
 
     let radix = args
@@ -340,23 +338,17 @@ fn value_of<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let number_proto = activation.avm2().classes().number.prototype();
-    if Object::ptr_eq(number_proto, this) {
-        return Ok(0.into());
+    if let Some(this) = this.as_object() {
+        let number_proto = activation.avm2().classes().number.prototype();
+        if Object::ptr_eq(number_proto, this) {
+            return Ok(0.into());
+        }
     }
 
-    let primitive = this.as_primitive();
-
-    if let Some(this) = primitive {
-        match *this {
-            Value::Integer(_) => Ok(*this),
-            Value::Number(_) => Ok(*this),
-            _ => Err(make_error_1004(activation, "Number.prototype.valueOf")),
-        }
-    } else {
-        Err(make_error_1004(activation, "Number.prototype.valueOf"))
+    match this {
+        Value::Integer(_) => Ok(this),
+        Value::Number(_) => Ok(this),
+        _ => Err(make_error_1004(activation, "Number.prototype.valueOf")),
     }
 }
 
@@ -375,7 +367,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
     );
 
     class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED);
-    class.set_instance_allocator(mc, primitive_allocator);
+    class.set_custom_constructor(mc, number_constructor);
     class.set_call_handler(
         mc,
         Method::from_builtin(call_handler, "<Number call handler>", mc),
diff --git a/core/src/avm2/globals/object.rs b/core/src/avm2/globals/object.rs
index 7e02248a7b6b1..e97f82c1bdf6e 100644
--- a/core/src/avm2/globals/object.rs
+++ b/core/src/avm2/globals/object.rs
@@ -2,12 +2,13 @@
 
 use crate::avm2::activation::Activation;
 use crate::avm2::class::{Class, ClassAttributes};
+use crate::avm2::error;
 use crate::avm2::method::{Method, NativeMethodImpl, ParamConfig};
 use crate::avm2::object::{FunctionObject, Object, TObject};
 use crate::avm2::traits::Trait;
 use crate::avm2::value::Value;
-use crate::avm2::Error;
-use crate::avm2::QName;
+use crate::avm2::{Error, Multiname, QName};
+use crate::string::AvmString;
 
 /// Implements `Object`'s instance initializer.
 pub fn instance_init<'gc>(
@@ -26,11 +27,11 @@ fn class_call<'gc>(
     let object_class = activation.avm2().classes().object;
 
     if args.is_empty() {
-        return object_class.construct(activation, args).map(|o| o.into());
+        return object_class.construct(activation, args);
     }
     let arg = args.get(0).cloned().unwrap();
     if matches!(arg, Value::Undefined) || matches!(arg, Value::Null) {
-        return object_class.construct(activation, args).map(|o| o.into());
+        return object_class.construct(activation, args);
     }
     Ok(arg)
 }
@@ -121,7 +122,7 @@ pub fn class_init<'gc>(
         "toLocaleString",
         FunctionObject::from_method(
             activation,
-            Method::from_builtin(to_locale_string, "toLocaleString", gc_context),
+            Method::from_builtin(to_string, "toLocaleString", gc_context),
             scope,
             None,
             None,
@@ -165,31 +166,22 @@ fn to_string<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    this.to_string(activation)
-}
-
-/// Implements `Object.prototype.toLocaleString`
-fn to_locale_string<'gc>(
-    activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
-    _args: &[Value<'gc>],
-) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
+    if let Some(this) = this.as_object() {
+        this.to_string(activation)
+    } else {
+        let class_name = this.instance_class(activation).name().local_name();
 
-    this.to_locale_string(activation)
+        Ok(AvmString::new_utf8(activation.gc(), format!("[object {class_name}]")).into())
+    }
 }
 
 /// Implements `Object.prototype.valueOf`
 fn value_of<'gc>(
-    activation: &mut Activation<'_, 'gc>,
+    _activation: &mut Activation<'_, 'gc>,
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    this.value_of(activation.strings())
+    Ok(this)
 }
 
 /// `Object.prototype.hasOwnProperty`
@@ -198,13 +190,16 @@ pub fn has_own_property<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
+    let name = args.get(0).expect("No name specified");
+    let name = name.coerce_to_string(activation)?;
 
-    let name: Result<&Value<'gc>, Error<'gc>> =
-        args.get(0).ok_or_else(|| "No name specified".into());
-    let name = name?.coerce_to_string(activation)?;
+    if let Some(this) = this.as_object() {
+        Ok(this.has_own_property_string(name, activation)?.into())
+    } else {
+        let name = Multiname::new(activation.avm2().find_public_namespace(), name);
 
-    Ok(this.has_own_property_string(name, activation)?.into())
+        Ok(this.has_trait(activation, &name).into())
+    }
 }
 
 /// `Object.prototype.isPrototypeOf`
@@ -213,16 +208,16 @@ pub fn is_prototype_of<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
+    if let Some(this) = this.as_object() {
+        let mut target_proto = args.get(0).cloned().unwrap_or(Value::Undefined);
 
-    let mut target_proto = args.get(0).cloned().unwrap_or(Value::Undefined);
+        while let Value::Object(proto) = target_proto {
+            if Object::ptr_eq(this, proto) {
+                return Ok(true.into());
+            }
 
-    while let Value::Object(proto) = target_proto {
-        if Object::ptr_eq(this, proto) {
-            return Ok(true.into());
+            target_proto = proto.proto().map(|o| o.into()).unwrap_or(Value::Undefined);
         }
-
-        target_proto = proto.proto().map(|o| o.into()).unwrap_or(Value::Undefined);
     }
 
     Ok(false.into())
@@ -234,13 +229,14 @@ pub fn property_is_enumerable<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let name: Result<&Value<'gc>, Error<'gc>> =
-        args.get(0).ok_or_else(|| "No name specified".into());
-    let name = name?.coerce_to_string(activation)?;
+    if let Some(this) = this.as_object() {
+        let name = args.get(0).expect("No name specified");
+        let name = name.coerce_to_string(activation)?;
 
-    Ok(this.property_is_enumerable(name).into())
+        Ok(this.property_is_enumerable(name).into())
+    } else {
+        Ok(false.into())
+    }
 }
 
 /// `Object.prototype.setPropertyIsEnumerable`
@@ -249,14 +245,23 @@ pub fn set_property_is_enumerable<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
+    let name = args.get(0).expect("No name specified");
+    let name = name.coerce_to_string(activation)?;
 
-    let name: Result<&Value<'gc>, Error<'gc>> =
-        args.get(0).ok_or_else(|| "No name specified".into());
-    let name = name?.coerce_to_string(activation)?;
+    if let Some(this) = this.as_object() {
+        if let Some(Value::Bool(is_enum)) = args.get(1) {
+            this.set_local_property_is_enumerable(activation.gc(), name, *is_enum);
+        }
+    } else {
+        let instance_class = this.instance_class(activation);
+        let multiname = Multiname::new(activation.avm2().find_public_namespace(), name);
 
-    if let Some(Value::Bool(is_enum)) = args.get(1) {
-        this.set_local_property_is_enumerable(activation.gc(), name, *is_enum);
+        return Err(error::make_reference_error(
+            activation,
+            error::ReferenceErrorCode::InvalidWrite,
+            &multiname,
+            instance_class,
+        ));
     }
 
     Ok(Value::Undefined)
diff --git a/core/src/avm2/globals/q_name.rs b/core/src/avm2/globals/q_name.rs
index d018b6012ab91..c8eb21771114a 100644
--- a/core/src/avm2/globals/q_name.rs
+++ b/core/src/avm2/globals/q_name.rs
@@ -26,19 +26,18 @@ pub fn call_handler<'gc>(
 
     // 2. Create and return a new QName object exactly as if the QName constructor had been called with the
     //    same arguments (section 13.3.2).
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .qname
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 /// Implements a custom constructor for `QName`.
 pub fn q_name_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
     args: &[Value<'gc>],
-) -> Result<Object<'gc>, Error<'gc>> {
+) -> Result<Value<'gc>, Error<'gc>> {
     let this = QNameObject::new_empty(activation);
 
     let namespace = if args.len() >= 2 {
diff --git a/core/src/avm2/globals/reg_exp.rs b/core/src/avm2/globals/reg_exp.rs
index fa5ecbafd85c3..fc18181f6403f 100644
--- a/core/src/avm2/globals/reg_exp.rs
+++ b/core/src/avm2/globals/reg_exp.rs
@@ -76,7 +76,7 @@ pub fn call_handler<'gc>(
             return Ok(arg);
         }
     }
-    this_class.construct(activation, args).map(|o| o.into())
+    this_class.construct(activation, args)
 }
 
 /// Implements `RegExp.dotall`
diff --git a/core/src/avm2/globals/string.rs b/core/src/avm2/globals/string.rs
index a4646f712391a..41b88e2df2c8b 100644
--- a/core/src/avm2/globals/string.rs
+++ b/core/src/avm2/globals/string.rs
@@ -4,7 +4,7 @@ use crate::avm2::activation::Activation;
 use crate::avm2::class::{Class, ClassAttributes};
 use crate::avm2::error::make_error_1004;
 use crate::avm2::method::{Method, NativeMethodImpl};
-use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
+use crate::avm2::object::{FunctionObject, Object, TObject};
 use crate::avm2::regexp::{RegExp, RegExpFlags};
 use crate::avm2::value::Value;
 use crate::avm2::Error;
@@ -37,25 +37,26 @@ const PUBLIC_INSTANCE_AND_PROTO_METHODS: &[(&str, NativeMethodImpl)] = &[
 ];
 
 /// Implements `String`'s instance initializer.
-pub fn instance_init<'gc>(
+///
+/// Because of the presence of a custom constructor, this method is unreachable.
+fn instance_init<'gc>(
+    _activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    unreachable!()
+}
+
+fn string_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    activation.super_init(this, &[])?;
-
-    if let Some(mut value) = this.as_primitive_mut(activation.gc()) {
-        if !matches!(*value, Value::String(_)) {
-            *value = match args.get(0) {
-                Some(arg) => arg.coerce_to_string(activation)?.into(),
-                None => activation.strings().empty().into(),
-            }
-        }
-    }
+    let string_value = match args.get(0) {
+        Some(arg) => arg.coerce_to_string(activation)?,
+        None => activation.strings().empty(),
+    };
 
-    Ok(Value::Undefined)
+    Ok(string_value.into())
 }
 
 /// Implements `String`'s class initializer.
@@ -103,13 +104,11 @@ pub fn call_handler<'gc>(
 
 /// Implements `length` property's getter
 fn length<'gc>(
-    activation: &mut Activation<'_, 'gc>,
+    _activation: &mut Activation<'_, 'gc>,
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Value::String(s) = this.value_of(activation.strings())? {
+    if let Value::String(s) = this {
         return Ok(s.len().into());
     }
 
@@ -122,9 +121,7 @@ fn char_at<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Value::String(s) = this.value_of(activation.strings())? {
+    if let Value::String(s) = this {
         // This function takes Number, so if we use coerce_to_i32 instead of coerce_to_number, the value may overflow.
         let n = args
             .get(0)
@@ -152,9 +149,7 @@ fn char_code_at<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Value::String(s) = this.value_of(activation.strings())? {
+    if let Value::String(s) = this {
         // This function takes Number, so if we use coerce_to_i32 instead of coerce_to_number, the value may overflow.
         let n = args
             .get(0)
@@ -297,15 +292,14 @@ fn match_s<'gc>(
     let pattern = args.get(0).unwrap_or(&Value::Undefined);
 
     let regexp_class = activation.avm2().classes().regexp;
-    let pattern = if !pattern.is_of_type(activation, regexp_class.inner_class_definition()) {
+    let pattern = if pattern.is_of_type(activation, regexp_class.inner_class_definition()) {
+        *pattern
+    } else {
         let string = pattern.coerce_to_string(activation)?;
         regexp_class.construct(activation, &[Value::String(string)])?
-    } else {
-        pattern
-            .as_object()
-            .expect("Regexp objects must be Value::Object")
     };
 
+    let pattern = pattern.as_object().unwrap();
     if let Some(mut regexp) = pattern.as_regexp_mut(activation.gc()) {
         let mut storage = ArrayStorage::new(0);
         if regexp.flags().contains(RegExpFlags::GLOBAL) {
@@ -406,15 +400,14 @@ fn search<'gc>(
     let pattern = args.get(0).unwrap_or(&Value::Undefined);
 
     let regexp_class = activation.avm2().classes().regexp;
-    let pattern = if !pattern.is_of_type(activation, regexp_class.inner_class_definition()) {
+    let pattern = if pattern.is_of_type(activation, regexp_class.inner_class_definition()) {
+        *pattern
+    } else {
         let string = pattern.coerce_to_string(activation)?;
         regexp_class.construct(activation, &[Value::String(string)])?
-    } else {
-        pattern
-            .as_object()
-            .expect("Regexp objects must be Value::Object")
     };
 
+    let pattern = pattern.as_object().unwrap();
     if let Some(mut regexp) = pattern.as_regexp_mut(activation.gc()) {
         let old = regexp.last_index();
         regexp.set_last_index(0);
@@ -623,17 +616,15 @@ fn to_string<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(this) = this.as_primitive() {
-        if let Value::String(v) = *this {
-            return Ok(v.into());
-        }
+    if let Value::String(v) = this {
+        return Ok(v.into());
     }
 
-    let string_proto = activation.avm2().classes().string.prototype();
-    if Object::ptr_eq(string_proto, this) {
-        return Ok(activation.strings().empty().into());
+    if let Some(this) = this.as_object() {
+        let string_proto = activation.avm2().classes().string.prototype();
+        if Object::ptr_eq(string_proto, this) {
+            return Ok(activation.strings().empty().into());
+        }
     }
 
     Err(make_error_1004(activation, "String.prototype.toString"))
@@ -641,13 +632,11 @@ fn to_string<'gc>(
 
 /// Implements `String.valueOf`
 fn value_of<'gc>(
-    activation: &mut Activation<'_, 'gc>,
+    _activation: &mut Activation<'_, 'gc>,
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    this.value_of(activation.strings())
+    Ok(this)
 }
 
 /// Implements `String.toUpperCase`
@@ -673,13 +662,10 @@ fn is_dependent<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(prim) = this.as_primitive() {
-        if let Value::String(s) = *prim {
-            return Ok(s.is_dependent().into());
-        }
+    if let Value::String(s) = this {
+        return Ok(s.is_dependent().into());
     }
+
     panic!();
 }
 
@@ -698,7 +684,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
     );
 
     class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED);
-    class.set_instance_allocator(mc, primitive_allocator);
+    class.set_custom_constructor(mc, string_constructor);
     class.set_call_handler(
         mc,
         Method::from_builtin(call_handler, "<String call handler>", mc),
diff --git a/core/src/avm2/globals/stubs.as b/core/src/avm2/globals/stubs.as
index ba8aec6991f45..b9444b7b3ffca 100644
--- a/core/src/avm2/globals/stubs.as
+++ b/core/src/avm2/globals/stubs.as
@@ -6,7 +6,6 @@ include "Object.as"
 // List is ordered alphabetically, except where superclasses
 // are listed before subclasses
 include "Array.as"
-include "Boolean.as"
 include "Class.as"
 
 include "Function.as"
diff --git a/core/src/avm2/globals/uint.rs b/core/src/avm2/globals/uint.rs
index 37c7c8dadf71d..f641fea79dfb5 100644
--- a/core/src/avm2/globals/uint.rs
+++ b/core/src/avm2/globals/uint.rs
@@ -5,30 +5,32 @@ use crate::avm2::class::{Class, ClassAttributes};
 use crate::avm2::error::{make_error_1003, make_error_1004};
 use crate::avm2::globals::number::print_with_radix;
 use crate::avm2::method::{Method, NativeMethodImpl, ParamConfig};
-use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
+use crate::avm2::object::{FunctionObject, Object, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::{AvmString, Error, QName};
 
 /// Implements `uint`'s instance initializer.
+///
+/// Because of the presence of a custom constructor, this method is unreachable.
 fn instance_init<'gc>(
+    _activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    unreachable!()
+}
+
+fn uint_constructor<'gc>(
     activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    if let Some(mut prim) = this.as_primitive_mut(activation.gc()) {
-        if matches!(*prim, Value::Undefined | Value::Null) {
-            *prim = args
-                .get(0)
-                .cloned()
-                .unwrap_or(Value::Undefined)
-                .coerce_to_u32(activation)?
-                .into();
-        }
-    }
+    let uint_value = args
+        .get(0)
+        .copied()
+        .unwrap_or(Value::Integer(0))
+        .coerce_to_u32(activation)?;
 
-    Ok(Value::Undefined)
+    Ok(uint_value.into())
 }
 
 /// Implements `uint`'s class initializer.
@@ -161,21 +163,17 @@ fn to_string<'gc>(
     this: Value<'gc>,
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let uint_proto = activation.avm2().classes().uint.prototype();
-    if Object::ptr_eq(uint_proto, this) {
-        return Ok("0".into());
+    if let Some(this) = this.as_object() {
+        let uint_proto = activation.avm2().classes().uint.prototype();
+        if Object::ptr_eq(uint_proto, this) {
+            return Ok("0".into());
+        }
     }
 
-    let number = if let Some(this) = this.as_primitive() {
-        match *this {
-            Value::Integer(o) => o as f64,
-            Value::Number(o) => o,
-            _ => return Err(make_error_1004(activation, "uint.prototype.toString")),
-        }
-    } else {
-        return Err(make_error_1004(activation, "uint.prototype.toString"));
+    let number = match this {
+        Value::Integer(o) => o as f64,
+        Value::Number(o) => o,
+        _ => return Err(make_error_1004(activation, "uint.prototype.toString")),
     };
 
     let radix = args
@@ -197,22 +195,16 @@ fn value_of<'gc>(
     this: Value<'gc>,
     _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    let uint_proto = activation.avm2().classes().uint.prototype();
-    if Object::ptr_eq(uint_proto, this) {
-        return Ok(0.into());
+    if let Some(this) = this.as_object() {
+        let uint_proto = activation.avm2().classes().uint.prototype();
+        if Object::ptr_eq(uint_proto, this) {
+            return Ok(0.into());
+        }
     }
 
-    let primitive = this.as_primitive();
-
-    if let Some(this) = primitive {
-        match *this {
-            Value::Integer(_) => Ok(*this),
-            _ => Err(make_error_1004(activation, "uint.prototype.valueOf")),
-        }
-    } else {
-        Err(make_error_1004(activation, "uint.prototype.valueOf"))
+    match this {
+        Value::Integer(_) => Ok(this),
+        _ => Err(make_error_1004(activation, "uint.prototype.valueOf")),
     }
 }
 
@@ -242,7 +234,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
     );
 
     class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED);
-    class.set_instance_allocator(mc, primitive_allocator);
+    class.set_custom_constructor(mc, uint_constructor);
     class.set_call_handler(
         mc,
         Method::from_builtin(call_handler, "<uint call handler>", mc),
diff --git a/core/src/avm2/globals/vector.rs b/core/src/avm2/globals/vector.rs
index 37b3bce13ae22..c7c76a7b0ceba 100644
--- a/core/src/avm2/globals/vector.rs
+++ b/core/src/avm2/globals/vector.rs
@@ -37,8 +37,6 @@ pub fn instance_init<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     let this = this.as_object().unwrap();
 
-    activation.super_init(this, &[])?;
-
     if let Some(mut vector) = this.as_vector_storage_mut(activation.gc()) {
         let length = args
             .get(0)
@@ -83,16 +81,17 @@ fn class_call<'gc>(
         .expect("Cannot convert to unparametrized Vector"); // technically unreachable
 
     let arg = args.get(0).cloned().unwrap();
-    let arg = arg.as_object().ok_or("Cannot convert to Vector")?;
 
-    if arg.instance_class() == this_class {
-        return Ok(arg.into());
+    if arg.instance_class(activation) == this_class {
+        return Ok(arg);
     }
 
     let length = arg
         .get_public_property("length", activation)?
         .coerce_to_i32(activation)?;
 
+    let arg = arg.as_object().ok_or("Cannot convert to Vector")?;
+
     let mut new_storage = VectorStorage::new(0, false, value_type, activation);
     new_storage.reserve_exact(length as usize);
 
@@ -100,8 +99,7 @@ fn class_call<'gc>(
 
     let mut iter = ArrayIter::new(activation, arg)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (_, item) = r?;
+    while let Some((_, item)) = iter.next(activation)? {
         let coerced_item = item.coerce_to_type(activation, value_type_for_coercion)?;
         new_storage.push(coerced_item, activation)?;
     }
@@ -110,13 +108,11 @@ fn class_call<'gc>(
 }
 
 pub fn generic_init<'gc>(
-    activation: &mut Activation<'_, 'gc>,
-    this: Value<'gc>,
-    args: &[Value<'gc>],
+    _activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
-    let this = this.as_object().unwrap();
-
-    activation.super_init(this, args)
+    Ok(Value::Undefined)
 }
 
 fn class_init<'gc>(
@@ -263,7 +259,7 @@ pub fn concat<'gc>(
     let val_class = new_vector_storage.value_type_for_coercion(activation);
 
     for arg in args {
-        let arg_obj = arg.coerce_to_object_or_typeerror(activation, None)?;
+        let arg = arg.null_check(activation, None)?;
 
         // this is Vector.<int/uint/Number/*>
         let my_base_vector_class = activation
@@ -275,20 +271,24 @@ pub fn concat<'gc>(
                 .name()
                 .to_qualified_name_err_message(activation.gc());
 
+            let instance_of_class_name = arg.instance_of_class_name(activation);
+
             return Err(Error::AvmError(type_error(
                 activation,
                 &format!(
                     "Error #1034: Type Coercion failed: cannot convert {}@00000000000 to {}.",
-                    arg_obj.instance_of_class_name(activation.gc()),
-                    base_vector_name,
+                    instance_of_class_name, base_vector_name,
                 ),
                 1034,
             )?));
         }
 
-        let old_vec = arg_obj.as_vector_storage();
-        let old_vec: Vec<Value<'gc>> = if let Some(old_vec) = old_vec {
-            old_vec.iter().collect()
+        let old_vec: Vec<Value<'gc>> = if let Some(old_vec) = arg.as_object() {
+            if let Some(old_vec) = old_vec.as_vector_storage() {
+                old_vec.iter().collect()
+            } else {
+                continue;
+            }
         } else {
             continue;
         };
@@ -376,11 +376,11 @@ pub fn to_locale_string<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     let this = this.as_object().unwrap();
 
-    join_inner(activation, this, &[",".into()], |v, act| {
-        if let Ok(o) = v.coerce_to_object(act) {
-            o.call_public_property("toLocaleString", &[], act)
-        } else {
+    join_inner(activation, this, &[",".into()], |v, activation| {
+        if matches!(v, Value::Null | Value::Undefined) {
             Ok(v)
+        } else {
+            v.call_public_property("toLocaleString", &[], activation)
         }
     })
 }
@@ -397,9 +397,7 @@ pub fn every<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let result = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -424,9 +422,7 @@ pub fn some<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let result = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -457,9 +453,7 @@ pub fn filter<'gc>(
     let mut new_storage = VectorStorage::new(0, false, value_type, activation);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let result = callback
             .call(activation, receiver, &[item, i.into(), this.into()])?
             .coerce_to_boolean();
@@ -484,9 +478,7 @@ pub fn for_each<'gc>(
     let receiver = args.get(1).cloned().unwrap_or(Value::Null);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         callback.call(activation, receiver, &[item, i.into(), this.into()])?;
     }
 
@@ -518,9 +510,7 @@ pub fn index_of<'gc>(
 
     let mut iter = ArrayIter::with_bounds(activation, this, from_index, u32::MAX)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         if item == search_for {
             return Ok(i.into());
         }
@@ -554,9 +544,7 @@ pub fn last_index_of<'gc>(
 
     let mut iter = ArrayIter::with_bounds(activation, this, 0, from_index)?;
 
-    while let Some(r) = iter.next_back(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next_back(activation)? {
         if item == search_for {
             return Ok(i.into());
         }
@@ -584,9 +572,7 @@ pub fn map<'gc>(
     let value_type_for_coercion = new_storage.value_type_for_coercion(activation);
     let mut iter = ArrayIter::new(activation, this)?;
 
-    while let Some(r) = iter.next(activation) {
-        let (i, item) = r?;
-
+    while let Some((i, item)) = iter.next(activation)? {
         let new_item = callback.call(activation, receiver, &[item, i.into(), this.into()])?;
         let coerced_item = new_item.coerce_to_type(activation, value_type_for_coercion)?;
 
diff --git a/core/src/avm2/globals/xml.rs b/core/src/avm2/globals/xml.rs
index d9f636ac7170c..c7ad44cbb0e89 100644
--- a/core/src/avm2/globals/xml.rs
+++ b/core/src/avm2/globals/xml.rs
@@ -102,6 +102,106 @@ pub fn init<'gc>(
     Ok(Value::Undefined)
 }
 
+pub fn get_ignore_comments<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    Ok(Value::Bool(activation.avm2().xml_settings.ignore_comments))
+}
+
+pub fn set_ignore_comments<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    activation.avm2().xml_settings.ignore_comments = args.get_bool(0);
+
+    Ok(Value::Undefined)
+}
+
+pub fn get_ignore_processing_instructions<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    Ok(Value::Bool(
+        activation
+            .avm2()
+            .xml_settings
+            .ignore_processing_instructions,
+    ))
+}
+
+pub fn set_ignore_processing_instructions<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    activation
+        .avm2()
+        .xml_settings
+        .ignore_processing_instructions = args.get_bool(0);
+
+    Ok(Value::Undefined)
+}
+
+pub fn get_ignore_whitespace<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    Ok(Value::Bool(
+        activation.avm2().xml_settings.ignore_whitespace,
+    ))
+}
+
+pub fn set_ignore_whitespace<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    activation.avm2().xml_settings.ignore_whitespace = args.get_bool(0);
+
+    Ok(Value::Undefined)
+}
+
+pub fn get_pretty_printing<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    Ok(Value::Bool(activation.avm2().xml_settings.pretty_printing))
+}
+
+pub fn set_pretty_printing<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    activation.avm2().xml_settings.pretty_printing = args.get_bool(0);
+
+    Ok(Value::Undefined)
+}
+
+pub fn get_pretty_indent<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    _args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    Ok(Value::Integer(activation.avm2().xml_settings.pretty_indent))
+}
+
+pub fn set_pretty_indent<'gc>(
+    activation: &mut Activation<'_, 'gc>,
+    _this: Value<'gc>,
+    args: &[Value<'gc>],
+) -> Result<Value<'gc>, Error<'gc>> {
+    activation.avm2().xml_settings.pretty_indent = args.get_i32(activation, 0)?;
+
+    Ok(Value::Undefined)
+}
+
 pub fn normalize<'gc>(
     activation: &mut Activation<'_, 'gc>,
     this: Value<'gc>,
@@ -164,6 +264,8 @@ pub fn set_name<'gc>(
         .classes()
         .qname
         .construct(activation, &[name])?
+        .as_object()
+        .unwrap()
         .as_qname_object()
         .unwrap();
 
@@ -276,6 +378,8 @@ pub fn add_namespace<'gc>(
         .classes()
         .namespace
         .construct(activation, &[value])?
+        .as_object()
+        .unwrap()
         .as_namespace_object()
         .unwrap();
 
@@ -321,8 +425,11 @@ pub fn set_namespace<'gc>(
         .classes()
         .namespace
         .construct(activation, &[value])?
+        .as_object()
+        .unwrap()
         .as_namespace_object()
         .unwrap();
+
     let ns = E4XNamespace {
         prefix: ns.prefix(),
         uri: ns.namespace().as_uri(activation.strings()),
@@ -372,6 +479,8 @@ pub fn remove_namespace<'gc>(
         .classes()
         .namespace
         .construct(activation, &[value])?
+        .as_object()
+        .unwrap()
         .as_namespace_object()
         .unwrap();
     let ns = E4XNamespace {
@@ -725,7 +834,7 @@ pub fn call_handler<'gc>(
     args: &[Value<'gc>],
 ) -> Result<Value<'gc>, Error<'gc>> {
     if args.len() == 1 {
-        if let Some(obj) = args.try_get_object(activation, 0) {
+        if let Some(obj) = args.get_value(0).as_object() {
             // We do *not* create a new object when AS does 'XML(someXML)'
             if let Some(xml) = obj.as_xml_object() {
                 return Ok(xml.into());
@@ -742,12 +851,7 @@ pub fn call_handler<'gc>(
         }
     }
 
-    Ok(activation
-        .avm2()
-        .classes()
-        .xml
-        .construct(activation, args)?
-        .into())
+    activation.avm2().classes().xml.construct(activation, args)
 }
 
 pub fn node_kind<'gc>(
@@ -1124,7 +1228,6 @@ pub fn replace<'gc>(
                 .classes()
                 .xml
                 .construct(activation, &[value])?
-                .into()
         } else {
             value
         }
diff --git a/core/src/avm2/globals/xml_list.rs b/core/src/avm2/globals/xml_list.rs
index d0b1ed0841ba4..1fc47c6b0cad8 100644
--- a/core/src/avm2/globals/xml_list.rs
+++ b/core/src/avm2/globals/xml_list.rs
@@ -81,19 +81,18 @@ pub fn call_handler<'gc>(
 ) -> Result<Value<'gc>, Error<'gc>> {
     if args.len() == 1 {
         // We do *not* create a new object when AS does 'XMLList(someXMLList)'
-        if let Some(obj) = args.try_get_object(activation, 0) {
+        if let Some(obj) = args.get_value(0).as_object() {
             if let Some(xml_list) = obj.as_xml_list_object() {
                 return Ok(xml_list.into());
             }
         }
     }
 
-    Ok(activation
+    activation
         .avm2()
         .classes()
         .xml_list
-        .construct(activation, args)?
-        .into())
+        .construct(activation, args)
 }
 
 // ECMA-357 13.5.4.11 XMLList.prototype.elements ([name])
@@ -593,9 +592,9 @@ macro_rules! define_xml_proxy {
                 let mut children = list.children_mut(activation.gc());
                 match &mut children[..] {
                     [child] => {
-                        child
-                            .get_or_create_xml(activation)
-                            .call_property(&Multiname::new(namespaces.as3, $as_name), args, activation)
+                        let child = child.get_or_create_xml(activation);
+
+                        Value::from(child).call_property(&Multiname::new(namespaces.as3, $as_name), args, activation)
                     }
                     _ => Err(make_error_1086(activation, $as_name)),
                 }
@@ -644,7 +643,7 @@ pub fn namespace_internal_impl<'gc>(
     };
 
     match &mut children[..] {
-        [child] => child.get_or_create_xml(activation).call_property(
+        [child] => Value::from(child.get_or_create_xml(activation)).call_property(
             &Multiname::new(namespaces.as3, "namespace"),
             args,
             activation,
diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs
index 9928243b948b8..6f8e798eafe25 100644
--- a/core/src/avm2/object.rs
+++ b/core/src/avm2/object.rs
@@ -7,12 +7,12 @@ use crate::avm2::class::Class;
 use crate::avm2::domain::Domain;
 use crate::avm2::error;
 use crate::avm2::events::{DispatchList, Event};
-use crate::avm2::function::{exec, BoundMethod};
+use crate::avm2::function::BoundMethod;
 use crate::avm2::property::Property;
 use crate::avm2::regexp::RegExp;
 use crate::avm2::value::{Hint, Value};
 use crate::avm2::vector::VectorStorage;
-use crate::avm2::vtable::{ClassBoundMethod, VTable};
+use crate::avm2::vtable::VTable;
 use crate::avm2::Error;
 use crate::avm2::Multiname;
 use crate::avm2::Namespace;
@@ -20,7 +20,7 @@ use crate::bitmap::bitmap_data::BitmapDataWrapper;
 use crate::display_object::DisplayObject;
 use crate::html::TextFormat;
 use crate::streams::NetStream;
-use crate::string::{AvmString, StringContext};
+use crate::string::AvmString;
 use gc_arena::{Collect, Gc, GcWeak, Mutation};
 use ruffle_macros::enum_trait_object;
 use std::cell::{Ref, RefMut};
@@ -47,7 +47,6 @@ mod local_connection_object;
 mod namespace_object;
 mod net_connection_object;
 mod netstream_object;
-mod primitive_object;
 mod program_3d_object;
 mod proxy_object;
 mod qname_object;
@@ -112,9 +111,6 @@ pub use crate::avm2::object::net_connection_object::{
 pub use crate::avm2::object::netstream_object::{
     netstream_allocator, NetStreamObject, NetStreamObjectWeak,
 };
-pub use crate::avm2::object::primitive_object::{
-    primitive_allocator, PrimitiveObject, PrimitiveObjectWeak,
-};
 pub use crate::avm2::object::program_3d_object::{Program3DObject, Program3DObjectWeak};
 pub use crate::avm2::object::proxy_object::{proxy_allocator, ProxyObject, ProxyObjectWeak};
 pub use crate::avm2::object::qname_object::{QNameObject, QNameObjectWeak};
@@ -123,7 +119,8 @@ pub use crate::avm2::object::responder_object::{
     responder_allocator, ResponderObject, ResponderObjectWeak,
 };
 pub use crate::avm2::object::script_object::{
-    scriptobject_allocator, ScriptObject, ScriptObjectData, ScriptObjectWeak, ScriptObjectWrapper,
+    maybe_int_property, scriptobject_allocator, ScriptObject, ScriptObjectData, ScriptObjectWeak,
+    ScriptObjectWrapper,
 };
 pub use crate::avm2::object::shader_data_object::{
     shader_data_allocator, ShaderDataObject, ShaderDataObjectWeak,
@@ -171,7 +168,6 @@ use crate::font::Font;
     pub enum Object<'gc> {
         ScriptObject(ScriptObject<'gc>),
         FunctionObject(FunctionObject<'gc>),
-        PrimitiveObject(PrimitiveObject<'gc>),
         NamespaceObject(NamespaceObject<'gc>),
         ArrayObject(ArrayObject<'gc>),
         StageObject(StageObject<'gc>),
@@ -250,72 +246,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         self.get_property_local(&name, activation)
     }
 
-    /// Retrieve a property by Multiname lookup.
-    ///
-    /// This method should not be overridden.
-    ///
-    /// This corresponds directly to the AVM2 operation `getproperty`, with the
-    /// exception that it does not special-case object lookups on dictionary
-    /// structured objects.
-    #[allow(unused_mut)] //Not unused.
-    #[no_dynamic]
-    fn get_property(
-        mut self,
-        multiname: &Multiname<'gc>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        match self.vtable().get_trait(multiname) {
-            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
-                Ok(self.base().get_slot(slot_id))
-            }
-            Some(Property::Method { disp_id }) => {
-                // avmplus has a special case for XML and XMLList objects, so we need one as well
-                // https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/Toplevel.cpp#L629-L634
-                if (self.as_xml_object().is_some() || self.as_xml_list_object().is_some())
-                    && multiname.contains_public_namespace()
-                {
-                    return self.get_property_local(multiname, activation);
-                }
-
-                if let Some(bound_method) = self.get_bound_method(disp_id) {
-                    return Ok(bound_method.into());
-                }
-
-                let bound_method = self
-                    .vtable()
-                    .make_bound_method(activation, self.into(), disp_id)
-                    .ok_or_else(|| format!("Method not found with id {disp_id}"))?;
-
-                self.install_bound_method(activation.gc(), disp_id, bound_method);
-
-                Ok(bound_method.into())
-            }
-            Some(Property::Virtual { get: Some(get), .. }) => {
-                self.call_method(get, &[], activation)
-            }
-            Some(Property::Virtual { get: None, .. }) => Err(error::make_reference_error(
-                activation,
-                error::ReferenceErrorCode::ReadFromWriteOnly,
-                multiname,
-                self.instance_class(),
-            )),
-            None => self.get_property_local(multiname, activation),
-        }
-    }
-
-    /// Same as get_property, but constructs a public Multiname for you.
-    #[no_dynamic]
-    fn get_public_property(
-        self,
-        name: impl Into<AvmString<'gc>>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        self.get_property(
-            &Multiname::new(activation.avm2().find_public_namespace(), name),
-            activation,
-        )
-    }
-
     /// Purely an optimization for "array-like" access. This should return
     /// `None` when the lookup needs to be forwarded to the base or throw.
     fn get_index_property(self, _index: usize) -> Option<Value<'gc>> {
@@ -352,72 +282,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         self.set_property_local(&name, value, activation)
     }
 
-    /// Set a property by Multiname lookup.
-    ///
-    /// This method should not be overridden.
-    ///
-    /// This corresponds directly with the AVM2 operation `setproperty`, with
-    /// the exception that it does not special-case object lookups on
-    /// dictionary structured objects.
-    #[no_dynamic]
-    fn set_property(
-        &self,
-        multiname: &Multiname<'gc>,
-        value: Value<'gc>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<(), Error<'gc>> {
-        match self.vtable().get_trait(multiname) {
-            Some(Property::Slot { slot_id }) => {
-                let value = self
-                    .vtable()
-                    .coerce_trait_value(slot_id, value, activation)?;
-
-                self.base().set_slot(slot_id, value, activation.gc());
-
-                Ok(())
-            }
-            Some(Property::Method { .. }) => {
-                // Similar to the get_property special case for XML/XMLList.
-                if (self.as_xml_object().is_some() || self.as_xml_list_object().is_some())
-                    && multiname.contains_public_namespace()
-                {
-                    return self.set_property_local(multiname, value, activation);
-                }
-
-                Err(error::make_reference_error(
-                    activation,
-                    error::ReferenceErrorCode::AssignToMethod,
-                    multiname,
-                    self.instance_class(),
-                ))
-            }
-            Some(Property::Virtual { set: Some(set), .. }) => {
-                self.call_method(set, &[value], activation).map(|_| ())
-            }
-            Some(Property::ConstSlot { .. }) | Some(Property::Virtual { set: None, .. }) => {
-                Err(error::make_reference_error(
-                    activation,
-                    error::ReferenceErrorCode::WriteToReadOnly,
-                    multiname,
-                    self.instance_class(),
-                ))
-            }
-            None => self.set_property_local(multiname, value, activation),
-        }
-    }
-
-    /// Same as set_property, but constructs a public Multiname for you.
-    #[no_dynamic]
-    fn set_public_property(
-        &self,
-        name: impl Into<AvmString<'gc>>,
-        value: Value<'gc>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<(), Error<'gc>> {
-        let name = Multiname::new(activation.avm2().namespaces.public_vm_internal(), name);
-        self.set_property(&name, value, activation)
-    }
-
     /// Init a local property of the object. The Multiname should always be public.
     ///
     /// This skips class field lookups and looks at:
@@ -436,47 +300,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         base.init_property_local(name, value, activation)
     }
 
-    /// Initialize a property by Multiname lookup.
-    ///
-    /// This method should not be overridden.
-    ///
-    /// This corresponds directly with the AVM2 operation `initproperty`.
-    #[no_dynamic]
-    fn init_property(
-        &self,
-        multiname: &Multiname<'gc>,
-        value: Value<'gc>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<(), Error<'gc>> {
-        match self.vtable().get_trait(multiname) {
-            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
-                let value = self
-                    .vtable()
-                    .coerce_trait_value(slot_id, value, activation)?;
-
-                self.base().set_slot(slot_id, value, activation.gc());
-
-                Ok(())
-            }
-            Some(Property::Method { .. }) => Err(error::make_reference_error(
-                activation,
-                error::ReferenceErrorCode::AssignToMethod,
-                multiname,
-                self.instance_class(),
-            )),
-            Some(Property::Virtual { set: Some(set), .. }) => {
-                self.call_method(set, &[value], activation).map(|_| ())
-            }
-            Some(Property::Virtual { set: None, .. }) => Err(error::make_reference_error(
-                activation,
-                error::ReferenceErrorCode::WriteToReadOnly,
-                multiname,
-                self.instance_class(),
-            )),
-            None => self.init_property_local(multiname, value, activation),
-        }
-    }
-
     /// Call a local property of the object. The Multiname should always be public.
     ///
     /// This skips class field lookups and looks at:
@@ -498,56 +321,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         result.call(activation, self_val, arguments)
     }
 
-    /// Call a named property on the object.
-    ///
-    /// This method should not be overridden.
-    ///
-    /// This corresponds directly to the `callproperty` operation in AVM2.
-    #[allow(unused_mut)]
-    #[no_dynamic]
-    fn call_property(
-        mut self,
-        multiname: &Multiname<'gc>,
-        arguments: &[Value<'gc>],
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        match self.vtable().get_trait(multiname) {
-            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
-                let obj = self.base().get_slot(slot_id);
-
-                obj.call(activation, Value::from(self.into()), arguments)
-            }
-            Some(Property::Method { disp_id }) => self.call_method(disp_id, arguments, activation),
-            Some(Property::Virtual { get: Some(get), .. }) => {
-                let obj = self.call_method(get, &[], activation)?;
-
-                obj.call(activation, Value::from(self.into()), arguments)
-            }
-            Some(Property::Virtual { get: None, .. }) => Err(error::make_reference_error(
-                activation,
-                error::ReferenceErrorCode::ReadFromWriteOnly,
-                multiname,
-                self.instance_class(),
-            )),
-            None => self.call_property_local(multiname, arguments, activation),
-        }
-    }
-
-    /// Same as call_property, but constructs a public Multiname for you.
-    #[no_dynamic]
-    fn call_public_property(
-        self,
-        name: impl Into<AvmString<'gc>>,
-        arguments: &[Value<'gc>],
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        self.call_property(
-            &Multiname::new(activation.avm2().find_public_namespace(), name),
-            arguments,
-            activation,
-        )
-    }
-
     /// Retrieve a slot by its index.
     #[no_dynamic]
     #[inline(always)]
@@ -580,53 +353,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         base.set_slot(id, value, mc);
     }
 
-    /// Call a method by its index.
-    ///
-    /// This directly corresponds with the AVM2 operation `callmethod`.
-    #[no_dynamic]
-    fn call_method(
-        self,
-        id: u32,
-        arguments: &[Value<'gc>],
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        if let Some(bound_method) = self.get_bound_method(id) {
-            return bound_method.call(activation, Value::from(self.into()), arguments);
-        }
-
-        let full_method = self
-            .vtable()
-            .get_full_method(id)
-            .ok_or_else(|| format!("Cannot call unknown method id {id}"))?;
-
-        // Execute immediately if this method doesn't require binding
-        if !full_method.method.needs_arguments_object() {
-            let ClassBoundMethod {
-                class,
-                super_class_obj,
-                scope,
-                method,
-            } = full_method;
-
-            return exec(
-                method,
-                scope.expect("Scope should exist here"),
-                self.into(),
-                super_class_obj,
-                Some(class),
-                arguments,
-                activation,
-                self.into(), // Callee deliberately invalid.
-            );
-        }
-
-        let bound_method = VTable::bind_method(activation, self.into(), full_method);
-
-        self.install_bound_method(activation.gc(), id, bound_method);
-
-        bound_method.call(activation, Value::from(self.into()), arguments)
-    }
-
     /// Implements the `in` opcode and AS3 operator.
     ///
     /// By default, this just calls `has_property`, but may be overridden by
@@ -651,19 +377,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         }
     }
 
-    /// Same as has_property, but constructs a public Multiname for you.
-    #[no_dynamic]
-    fn has_public_property(
-        self,
-        name: impl Into<AvmString<'gc>>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> bool {
-        self.has_property(&Multiname::new(
-            activation.avm2().find_public_namespace(),
-            name,
-        ))
-    }
-
     /// Indicates whether or not a property or trait exists on an object and is
     /// not part of the prototype chain.
     fn has_own_property(self, name: &Multiname<'gc>) -> bool {
@@ -685,8 +398,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
     /// Returns true if an object has one or more traits of a given name.
     #[no_dynamic]
     fn has_trait(self, name: &Multiname<'gc>) -> bool {
-        let base = self.base();
-        base.has_trait(name)
+        self.vtable().has_trait(name)
     }
 
     /// Delete a property by QName, after multiname resolution and all other
@@ -715,44 +427,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         self.delete_property_local(activation, &name)
     }
 
-    /// Delete a named property from the object.
-    ///
-    /// Returns false if the property cannot be deleted.
-    #[no_dynamic]
-    fn delete_property(
-        &self,
-        activation: &mut Activation<'_, 'gc>,
-        multiname: &Multiname<'gc>,
-    ) -> Result<bool, Error<'gc>> {
-        if self.as_primitive().is_some() {
-            return Err(error::make_reference_error(
-                activation,
-                error::ReferenceErrorCode::InvalidDelete,
-                multiname,
-                self.instance_class(),
-            ));
-        }
-
-        match self.vtable().get_trait(multiname) {
-            None => {
-                if self.instance_class().is_sealed() {
-                    Ok(false)
-                } else {
-                    self.delete_property_local(activation, multiname)
-                }
-            }
-            _ => {
-                // Similar to the get_property special case for XML/XMLList.
-                if (self.as_xml_object().is_some() || self.as_xml_list_object().is_some())
-                    && multiname.contains_public_namespace()
-                {
-                    return self.delete_property_local(activation, multiname);
-                }
-                Ok(false)
-            }
-        }
-    }
-
     /// Retrieve the `__proto__` of a given object.
     ///
     /// The proto is another object used to resolve methods across a class of
@@ -826,8 +500,9 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         let name = self
             .get_enumerant_name(index, activation)?
             .coerce_to_string(activation)?;
+
         // todo: this probably doesn't need non-public accesses
-        self.get_public_property(name, activation)
+        Value::from(self.into()).get_public_property(name, activation)
     }
 
     /// Determine if a property is currently enumerable.
@@ -864,21 +539,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         base.install_bound_method(mc, disp_id, function)
     }
 
-    /// Construct a property of this object by Multiname lookup.
-    ///
-    /// This corresponds directly to the AVM2 operation `constructprop`.
-    #[no_dynamic]
-    fn construct_prop(
-        self,
-        multiname: &Multiname<'gc>,
-        args: &[Value<'gc>],
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Object<'gc>, Error<'gc>> {
-        let ctor = self.get_property(multiname, activation)?;
-
-        ctor.construct(activation, args)
-    }
-
     /// Construct a parameterization of this particular type and return it.
     ///
     /// This is called specifically to parameterize generic types, of which
@@ -926,35 +586,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         Ok(AvmString::new_utf8(activation.gc(), format!("[object {class_name}]")).into())
     }
 
-    /// Implement the result of calling `Object.prototype.toLocaleString` on this
-    /// object class.
-    ///
-    /// `toLocaleString` is a method used to request an object be coerced to a
-    /// locale-dependent string value. The default implementation appears to
-    /// generate a debug-style string based on the name of the class this
-    /// object is, in the format of `[object Class]` (where `Class` is the name
-    /// of the class that created this object).
-    fn to_locale_string(
-        &self,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        let class_name = self.instance_class().name().local_name();
-
-        Ok(AvmString::new_utf8(activation.gc(), format!("[object {class_name}]")).into())
-    }
-
-    /// Implement the result of calling `Object.prototype.valueOf` on this
-    /// object class.
-    ///
-    /// `valueOf` is a method used to request an object be coerced to a
-    /// primitive value. Typically, this would be a number of some kind.
-    ///
-    /// The default implementation wraps the object in a `Value`, using the
-    /// `Into<Object<'gc>>` implementation.
-    fn value_of(&self, _context: &mut StringContext<'gc>) -> Result<Value<'gc>, Error<'gc>> {
-        Ok(Value::Object((*self).into()))
-    }
-
     /// Returns all public properties from this object's vtable, together with their values.
     /// This includes normal fields, const fields, and getter methods
     /// This is used for JSON serialization.
@@ -972,9 +603,10 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
                 Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
                     values.push((name, self.base().get_slot(slot_id)));
                 }
-                Property::Virtual { get: Some(get), .. } => {
-                    values.push((name, self.call_method(get, &[], activation)?))
-                }
+                Property::Virtual { get: Some(get), .. } => values.push((
+                    name,
+                    Value::from((*self).into()).call_method(get, &[], activation)?,
+                )),
                 _ => {}
             }
         }
@@ -1115,6 +747,11 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         None
     }
 
+    /// Unwrap this object as an event.
+    fn as_event_object(self) -> Option<EventObject<'gc>> {
+        None
+    }
+
     /// Unwrap this object as an event.
     fn as_event(&self) -> Option<Ref<Event<'gc>>> {
         None
@@ -1135,20 +772,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
         None
     }
 
-    /// Unwrap this object as an immutable primitive value.
-    ///
-    /// This function should not be called in cases where a normal `Value`
-    /// coercion would do. It *only* accounts for boxed primitives, and not
-    /// `valueOf`.
-    fn as_primitive(&self) -> Option<Ref<Value<'gc>>> {
-        None
-    }
-
-    /// Unwrap this object as a mutable primitive value.
-    fn as_primitive_mut(&self, _mc: &Mutation<'gc>) -> Option<RefMut<Value<'gc>>> {
-        None
-    }
-
     /// Unwrap this object as a regexp.
     fn as_regexp_object(&self) -> Option<RegExpObject<'gc>> {
         None
@@ -1309,7 +932,6 @@ impl<'gc> Object<'gc> {
         match self {
             Self::ScriptObject(o) => WeakObject::ScriptObject(ScriptObjectWeak(Gc::downgrade(o.0))),
             Self::FunctionObject(o) => WeakObject::FunctionObject(FunctionObjectWeak(Gc::downgrade(o.0))),
-            Self::PrimitiveObject(o) => WeakObject::PrimitiveObject(PrimitiveObjectWeak(Gc::downgrade(o.0))),
             Self::NamespaceObject(o) => WeakObject::NamespaceObject(NamespaceObjectWeak(Gc::downgrade(o.0))),
             Self::ArrayObject(o) => WeakObject::ArrayObject(ArrayObjectWeak(Gc::downgrade(o.0))),
             Self::StageObject(o) => WeakObject::StageObject(StageObjectWeak(Gc::downgrade(o.0))),
@@ -1373,7 +995,6 @@ impl Hash for Object<'_> {
 pub enum WeakObject<'gc> {
     ScriptObject(ScriptObjectWeak<'gc>),
     FunctionObject(FunctionObjectWeak<'gc>),
-    PrimitiveObject(PrimitiveObjectWeak<'gc>),
     NamespaceObject(NamespaceObjectWeak<'gc>),
     ArrayObject(ArrayObjectWeak<'gc>),
     StageObject(StageObjectWeak<'gc>),
@@ -1420,7 +1041,6 @@ impl<'gc> WeakObject<'gc> {
         match self {
             Self::ScriptObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
             Self::FunctionObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
-            Self::PrimitiveObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
             Self::NamespaceObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
             Self::ArrayObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
             Self::StageObject(o) => GcWeak::as_ptr(o.0) as *const ObjectPtr,
@@ -1467,7 +1087,6 @@ impl<'gc> WeakObject<'gc> {
         Some(match self {
             Self::ScriptObject(o) => ScriptObject(o.0.upgrade(mc)?).into(),
             Self::FunctionObject(o) => FunctionObject(o.0.upgrade(mc)?).into(),
-            Self::PrimitiveObject(o) => PrimitiveObject(o.0.upgrade(mc)?).into(),
             Self::NamespaceObject(o) => NamespaceObject(o.0.upgrade(mc)?).into(),
             Self::ArrayObject(o) => ArrayObject(o.0.upgrade(mc)?).into(),
             Self::StageObject(o) => StageObject(o.0.upgrade(mc)?).into(),
diff --git a/core/src/avm2/object/class_object.rs b/core/src/avm2/object/class_object.rs
index 096026493f5a1..6f9d37bd6c081 100644
--- a/core/src/avm2/object/class_object.rs
+++ b/core/src/avm2/object/class_object.rs
@@ -7,7 +7,7 @@ use crate::avm2::function::exec;
 use crate::avm2::method::Method;
 use crate::avm2::object::function_object::FunctionObject;
 use crate::avm2::object::script_object::ScriptObjectData;
-use crate::avm2::object::{Object, ObjectPtr, TObject};
+use crate::avm2::object::{Object, ObjectPtr, ScriptObject, TObject};
 use crate::avm2::property::Property;
 use crate::avm2::scope::{Scope, ScopeChain};
 use crate::avm2::value::Value;
@@ -90,11 +90,7 @@ impl<'gc> ClassObject<'gc> {
         activation: &mut Activation<'_, 'gc>,
         superclass_object: Option<ClassObject<'gc>>,
     ) -> Result<Object<'gc>, Error<'gc>> {
-        let proto = activation
-            .avm2()
-            .classes()
-            .object
-            .construct(activation, &[])?;
+        let proto = ScriptObject::new_object(activation);
 
         if let Some(superclass_object) = superclass_object {
             let base_proto = superclass_object.prototype();
@@ -323,7 +319,7 @@ impl<'gc> ClassObject<'gc> {
         self,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<(), Error<'gc>> {
-        let object: Object<'gc> = self.into();
+        let self_value: Value<'gc> = self.into();
         let class_classobject = activation.avm2().classes().class;
 
         let scope = self.0.class_scope;
@@ -337,17 +333,19 @@ impl<'gc> ClassObject<'gc> {
             activation,
             class_initializer,
             scope,
-            Some(object),
+            Some(self_value),
             Some(class_classobject),
             Some(c_class),
         );
 
-        class_init_fn.call(activation, object.into(), &[])?;
+        class_init_fn.call(activation, self_value, &[])?;
 
         Ok(())
     }
 
     /// Call the instance initializer.
+    ///
+    /// This method may panic if called with a Null or Undefined receiver.
     pub fn call_init(
         self,
         receiver: Value<'gc>,
@@ -359,7 +357,7 @@ impl<'gc> ClassObject<'gc> {
         exec(
             method,
             scope,
-            receiver.coerce_to_object(activation)?,
+            receiver,
             self.superclass_object(),
             Some(self.inner_class_definition()),
             arguments,
@@ -429,14 +427,14 @@ impl<'gc> ClassObject<'gc> {
                 activation,
                 method,
                 scope.expect("Scope should exist here"),
-                Some(receiver),
+                Some(receiver.into()),
                 super_class_obj,
                 Some(class),
             );
 
             callee.call(activation, receiver.into(), arguments)
         } else {
-            receiver.call_property(multiname, arguments, activation)
+            Value::from(receiver).call_property(multiname, arguments, activation)
         }
     }
 
@@ -490,7 +488,7 @@ impl<'gc> ClassObject<'gc> {
                     activation,
                     method,
                     scope.expect("Scope should exist here"),
-                    Some(receiver),
+                    Some(receiver.into()),
                     super_class_obj,
                     Some(class),
                 );
@@ -508,7 +506,7 @@ impl<'gc> ClassObject<'gc> {
             )
             .into()),
             Some(Property::Slot { .. } | Property::ConstSlot { .. }) => {
-                receiver.get_property(multiname, activation)
+                Value::from(receiver).get_property(multiname, activation)
             }
             None => Err(format!(
                 "Attempted to supercall method {:?}, which does not exist",
@@ -571,13 +569,13 @@ impl<'gc> ClassObject<'gc> {
                     method,
                 } = self.instance_vtable().get_full_method(disp_id).unwrap();
                 let callee =
-                    FunctionObject::from_method(activation, method, scope.expect("Scope should exist here"), Some(receiver), super_class_obj, Some(class));
+                    FunctionObject::from_method(activation, method, scope.expect("Scope should exist here"), Some(receiver.into()), super_class_obj, Some(class));
 
                 callee.call(activation, receiver.into(), &[value])?;
                 Ok(())
             }
             Some(Property::Slot { .. }) => {
-                receiver.set_property(multiname, value, activation)?;
+                Value::from(receiver).set_property(multiname, value, activation)?;
                 Ok(())
             }
             _ => {
@@ -669,7 +667,7 @@ impl<'gc> ClassObject<'gc> {
         self,
         activation: &mut Activation<'_, 'gc>,
         arguments: &[Value<'gc>],
-    ) -> Result<Object<'gc>, Error<'gc>> {
+    ) -> Result<Value<'gc>, Error<'gc>> {
         if let Some(custom_constructor) = self.custom_constructor() {
             custom_constructor(activation, arguments)
         } else {
@@ -679,7 +677,7 @@ impl<'gc> ClassObject<'gc> {
 
             self.call_init(instance.into(), arguments, activation)?;
 
-            Ok(instance)
+            Ok(instance.into())
         }
     }
 
@@ -771,13 +769,6 @@ impl<'gc> TObject<'gc> for ClassObject<'gc> {
         .into())
     }
 
-    fn to_locale_string(
-        &self,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        self.to_string(activation)
-    }
-
     fn as_class_object(&self) -> Option<ClassObject<'gc>> {
         Some(*self)
     }
diff --git a/core/src/avm2/object/date_object.rs b/core/src/avm2/object/date_object.rs
index 8b7d22095b057..6baad8c995825 100644
--- a/core/src/avm2/object/date_object.rs
+++ b/core/src/avm2/object/date_object.rs
@@ -1,9 +1,8 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::object::script_object::ScriptObjectData;
 use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
-use crate::avm2::value::{Hint, Value};
+use crate::avm2::value::Hint;
 use crate::avm2::Error;
-use crate::string::StringContext;
 use chrono::{DateTime, Utc};
 use core::fmt;
 use gc_arena::{Collect, Gc, GcWeak};
@@ -97,14 +96,6 @@ impl<'gc> TObject<'gc> for DateObject<'gc> {
         Gc::as_ptr(self.0) as *const ObjectPtr
     }
 
-    fn value_of(&self, _context: &mut StringContext<'gc>) -> Result<Value<'gc>, Error<'gc>> {
-        if let Some(date) = self.date_time() {
-            Ok((date.timestamp_millis() as f64).into())
-        } else {
-            Ok(f64::NAN.into())
-        }
-    }
-
     fn default_hint(&self) -> Hint {
         Hint::String
     }
diff --git a/core/src/avm2/object/event_object.rs b/core/src/avm2/object/event_object.rs
index 386b6178f842d..18da68d9cb2b2 100644
--- a/core/src/avm2/object/event_object.rs
+++ b/core/src/avm2/object/event_object.rs
@@ -3,7 +3,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::events::Event;
 use crate::avm2::object::script_object::ScriptObjectData;
-use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
+use crate::avm2::object::{ClassObject, Object, ObjectPtr, ScriptObject, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::Error;
 use crate::context::UpdateContext;
@@ -61,7 +61,10 @@ impl<'gc> EventObject<'gc> {
     /// It's just slightly faster and doesn't require an Activation.
     /// This is equivalent to
     /// classes.event.construct(activation, &[event_type, false, false])
-    pub fn bare_default_event<S>(context: &mut UpdateContext<'gc>, event_type: S) -> Object<'gc>
+    pub fn bare_default_event<S>(
+        context: &mut UpdateContext<'gc>,
+        event_type: S,
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
@@ -76,7 +79,7 @@ impl<'gc> EventObject<'gc> {
         event_type: S,
         bubbles: bool,
         cancelable: bool,
-    ) -> Object<'gc>
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
@@ -87,15 +90,30 @@ impl<'gc> EventObject<'gc> {
         event.set_bubbles(bubbles);
         event.set_cancelable(cancelable);
 
-        let event_object = EventObject(Gc::new(
+        EventObject(Gc::new(
             context.gc(),
             EventObjectData {
                 base,
                 event: RefLock::new(event),
             },
-        ));
+        ))
+    }
 
-        event_object.into()
+    #[inline]
+    pub fn from_class_and_args(
+        activation: &mut Activation<'_, 'gc>,
+        class: ClassObject<'gc>,
+        args: &[Value<'gc>],
+    ) -> EventObject<'gc> {
+        // We don't expect Event classes to error in their constructors or to
+        // return anything other than an EventObject
+        class
+            .construct(activation, args)
+            .unwrap()
+            .as_object()
+            .unwrap()
+            .as_event_object()
+            .unwrap()
     }
 
     pub fn mouse_event<S>(
@@ -106,7 +124,7 @@ impl<'gc> EventObject<'gc> {
         delta: i32,
         bubbles: bool,
         button: MouseButton,
-    ) -> Object<'gc>
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
@@ -115,47 +133,46 @@ impl<'gc> EventObject<'gc> {
         let event_type: AvmString<'gc> = event_type.into();
 
         let mouse_event_cls = activation.avm2().classes().mouseevent;
-        mouse_event_cls
-            .construct(
-                activation,
-                &[
-                    event_type.into(),
-                    // bubbles
-                    bubbles.into(),
-                    // cancellable
-                    false.into(),
-                    // localX
-                    local.x.to_pixels().into(),
-                    // localY
-                    local.y.to_pixels().into(),
-                    // relatedObject
-                    related_object
-                        .map(|o| o.as_displayobject().object2())
-                        .unwrap_or(Value::Null),
-                    // ctrlKey
-                    activation
-                        .context
-                        .input
-                        .is_key_down(KeyCode::CONTROL)
-                        .into(),
-                    // altKey
-                    activation.context.input.is_key_down(KeyCode::ALT).into(),
-                    // shiftKey
-                    activation.context.input.is_key_down(KeyCode::SHIFT).into(),
-                    // buttonDown
-                    activation.context.input.is_key_down(button.into()).into(),
-                    // delta
-                    delta.into(),
-                ],
-            )
-            .unwrap() // we don't expect to break here
+        Self::from_class_and_args(
+            activation,
+            mouse_event_cls,
+            &[
+                event_type.into(),
+                // bubbles
+                bubbles.into(),
+                // cancellable
+                false.into(),
+                // localX
+                local.x.to_pixels().into(),
+                // localY
+                local.y.to_pixels().into(),
+                // relatedObject
+                related_object
+                    .map(|o| o.as_displayobject().object2())
+                    .unwrap_or(Value::Null),
+                // ctrlKey
+                activation
+                    .context
+                    .input
+                    .is_key_down(KeyCode::CONTROL)
+                    .into(),
+                // altKey
+                activation.context.input.is_key_down(KeyCode::ALT).into(),
+                // shiftKey
+                activation.context.input.is_key_down(KeyCode::SHIFT).into(),
+                // buttonDown
+                activation.context.input.is_key_down(button.into()).into(),
+                // delta
+                delta.into(),
+            ],
+        )
     }
 
     pub fn mouse_event_down(
         activation: &mut Activation<'_, 'gc>,
         target: DisplayObject<'gc>,
         button: MouseButton,
-    ) -> Object<'gc> {
+    ) -> EventObject<'gc> {
         Self::mouse_event(
             activation,
             match button {
@@ -176,7 +193,7 @@ impl<'gc> EventObject<'gc> {
         activation: &mut Activation<'_, 'gc>,
         target: DisplayObject<'gc>,
         button: MouseButton,
-    ) -> Object<'gc> {
+    ) -> EventObject<'gc> {
         Self::mouse_event(
             activation,
             match button {
@@ -197,7 +214,7 @@ impl<'gc> EventObject<'gc> {
         activation: &mut Activation<'_, 'gc>,
         target: DisplayObject<'gc>,
         button: MouseButton,
-    ) -> Object<'gc> {
+    ) -> EventObject<'gc> {
         Self::mouse_event(
             activation,
             match button {
@@ -220,97 +237,81 @@ impl<'gc> EventObject<'gc> {
         text: AvmString<'gc>,
         bubbles: bool,
         cancelable: bool,
-    ) -> Object<'gc>
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
         let event_type: AvmString<'gc> = event_type.into();
 
         let text_event_cls = activation.avm2().classes().textevent;
-        text_event_cls
-            .construct(
-                activation,
-                &[
-                    event_type.into(),
-                    // bubbles
-                    bubbles.into(),
-                    // cancelable
-                    cancelable.into(),
-                    // text
-                    text.into(),
-                ],
-            )
-            .unwrap() // we don't expect to break here
+        Self::from_class_and_args(
+            activation,
+            text_event_cls,
+            &[
+                event_type.into(),
+                // bubbles
+                bubbles.into(),
+                // cancelable
+                cancelable.into(),
+                // text
+                text.into(),
+            ],
+        )
     }
 
-    pub fn net_status_event<S>(
+    pub fn net_status_event(
         activation: &mut Activation<'_, 'gc>,
-        event_type: S,
         info: Vec<(impl Into<AvmString<'gc>>, impl Into<AvmString<'gc>>)>,
-    ) -> Object<'gc>
-    where
-        S: Into<AvmString<'gc>>,
-    {
-        let info_object = activation
-            .avm2()
-            .classes()
-            .object
-            .construct(activation, &[])
-            .unwrap();
+    ) -> EventObject<'gc> {
+        let info_object = ScriptObject::new_object(activation);
         for (key, value) in info {
             info_object
                 .set_string_property_local(key.into(), Value::String(value.into()), activation)
                 .unwrap();
         }
 
-        let event_type: AvmString<'gc> = event_type.into();
-
         let net_status_cls = activation.avm2().classes().netstatusevent;
-        net_status_cls
-            .construct(
-                activation,
-                &[
-                    event_type.into(),
-                    //bubbles
-                    false.into(),
-                    //cancelable
-                    false.into(),
-                    info_object.into(),
-                ],
-            )
-            .unwrap() // we don't expect to break here
+        Self::from_class_and_args(
+            activation,
+            net_status_cls,
+            &[
+                "netStatus".into(),
+                //bubbles
+                false.into(),
+                //cancelable
+                false.into(),
+                info_object.into(),
+            ],
+        )
     }
 
     pub fn progress_event<S>(
         activation: &mut Activation<'_, 'gc>,
         event_type: S,
-        bytes_loaded: u64,
-        bytes_total: u64,
-        bubbles: bool,
-        cancelable: bool,
-    ) -> Object<'gc>
+        bytes_loaded: usize,
+        bytes_total: usize,
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
         let event_type: AvmString<'gc> = event_type.into();
 
         let progress_event_cls = activation.avm2().classes().progressevent;
-        progress_event_cls
-            .construct(
-                activation,
-                &[
-                    event_type.into(),
-                    // bubbles
-                    bubbles.into(),
-                    // cancelable
-                    cancelable.into(),
-                    // bytesLoaded
-                    (bytes_loaded as f64).into(),
-                    // bytesToal
-                    (bytes_total as f64).into(),
-                ],
-            )
-            .unwrap() // we don't expect to break here
+        Self::from_class_and_args(
+            activation,
+            progress_event_cls,
+            &[
+                event_type.into(),
+                // bubbles
+                false.into(),
+                // cancelable
+                false.into(),
+                // bytesLoaded
+                (bytes_loaded as f64).into(),
+                // bytesToal
+                (bytes_total as f64).into(),
+            ],
+        )
     }
 
     pub fn focus_event<S>(
@@ -319,30 +320,75 @@ impl<'gc> EventObject<'gc> {
         cancelable: bool,
         related_object: Option<InteractiveObject<'gc>>,
         key_code: u32,
-    ) -> Object<'gc>
+    ) -> EventObject<'gc>
     where
         S: Into<AvmString<'gc>>,
     {
         let event_type: AvmString<'gc> = event_type.into();
         let shift_key = activation.context.input.is_key_down(KeyCode::SHIFT);
 
-        let class = activation.avm2().classes().focusevent;
-        class
-            .construct(
-                activation,
-                &[
-                    event_type.into(),
-                    true.into(),
-                    cancelable.into(),
-                    related_object
-                        .map(|o| o.as_displayobject().object2())
-                        .unwrap_or(Value::Null),
-                    shift_key.into(),
-                    key_code.into(),
-                    "none".into(), // TODO implement direction
-                ],
-            )
-            .unwrap()
+        let focus_event_cls = activation.avm2().classes().focusevent;
+        Self::from_class_and_args(
+            activation,
+            focus_event_cls,
+            &[
+                event_type.into(),
+                true.into(),
+                cancelable.into(),
+                related_object
+                    .map(|o| o.as_displayobject().object2())
+                    .unwrap_or(Value::Null),
+                shift_key.into(),
+                key_code.into(),
+                "none".into(), // TODO implement direction
+            ],
+        )
+    }
+
+    pub fn io_error_event(
+        activation: &mut Activation<'_, 'gc>,
+        error_msg: AvmString<'gc>,
+        error_code: u32,
+    ) -> EventObject<'gc> {
+        let io_error_event_cls = activation.avm2().classes().ioerrorevent;
+        Self::from_class_and_args(
+            activation,
+            io_error_event_cls,
+            &[
+                "ioError".into(),
+                false.into(),
+                false.into(),
+                error_msg.into(),
+                error_code.into(),
+            ],
+        )
+    }
+
+    pub fn http_status_event(
+        activation: &mut Activation<'_, 'gc>,
+        status: u16,
+        redirected: bool,
+    ) -> EventObject<'gc> {
+        let http_status_event_cls = activation.avm2().classes().httpstatusevent;
+        Self::from_class_and_args(
+            activation,
+            http_status_event_cls,
+            &[
+                "httpStatus".into(),
+                false.into(),
+                false.into(),
+                status.into(),
+                redirected.into(),
+            ],
+        )
+    }
+
+    pub fn event(&self) -> Ref<Event<'gc>> {
+        self.0.event.borrow()
+    }
+
+    pub fn event_mut(&self, mc: &Mutation<'gc>) -> RefMut<Event<'gc>> {
+        unlock!(Gc::write(mc, self.0), EventObjectData, event).borrow_mut()
     }
 }
 
@@ -359,6 +405,10 @@ impl<'gc> TObject<'gc> for EventObject<'gc> {
         Gc::as_ptr(self.0) as *const ObjectPtr
     }
 
+    fn as_event_object(self) -> Option<EventObject<'gc>> {
+        Some(self)
+    }
+
     fn as_event(&self) -> Option<Ref<Event<'gc>>> {
         Some(self.0.event.borrow())
     }
diff --git a/core/src/avm2/object/function_object.rs b/core/src/avm2/object/function_object.rs
index 499094738aca1..8ebcc82dc88f7 100644
--- a/core/src/avm2/object/function_object.rs
+++ b/core/src/avm2/object/function_object.rs
@@ -106,11 +106,14 @@ impl<'gc> FunctionObject<'gc> {
     /// This associated constructor will also create and initialize an empty
     /// `Object` prototype for the function. The given `receiver`, if supplied,
     /// will override any user-specified `this` parameter.
+    ///
+    /// It is the caller's responsibility to ensure that the `receiver` passed
+    /// to this method is not Value::Null or Value::Undefined.
     pub fn from_method(
         activation: &mut Activation<'_, 'gc>,
         method: Method<'gc>,
         scope: ScopeChain<'gc>,
-        receiver: Option<Object<'gc>>,
+        receiver: Option<Value<'gc>>,
         bound_superclass_object: Option<ClassObject<'gc>>,
         bound_class: Option<Class<'gc>>,
     ) -> FunctionObject<'gc> {
@@ -200,13 +203,6 @@ impl<'gc> TObject<'gc> for FunctionObject<'gc> {
         Gc::as_ptr(self.0) as *const ObjectPtr
     }
 
-    fn to_locale_string(
-        &self,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        self.to_string(activation)
-    }
-
     fn as_executable(&self) -> Option<Ref<BoundMethod<'gc>>> {
         Some(self.0.exec.borrow())
     }
diff --git a/core/src/avm2/object/loaderinfo_object.rs b/core/src/avm2/object/loaderinfo_object.rs
index 17823ed52fd24..de62f3a07ed37 100644
--- a/core/src/avm2/object/loaderinfo_object.rs
+++ b/core/src/avm2/object/loaderinfo_object.rs
@@ -134,13 +134,17 @@ impl<'gc> LoaderInfoObject<'gc> {
                     .avm2
                     .classes()
                     .eventdispatcher
-                    .construct(activation, &[])?,
+                    .construct(activation, &[])?
+                    .as_object()
+                    .unwrap(),
                 uncaught_error_events: activation
                     .context
                     .avm2
                     .classes()
                     .uncaughterrorevents
-                    .construct(activation, &[])?,
+                    .construct(activation, &[])?
+                    .as_object()
+                    .unwrap(),
                 cached_avm1movie: Lock::new(None),
                 content_type: Cell::new(ContentType::Swf),
                 expose_content: Cell::new(false),
@@ -181,13 +185,17 @@ impl<'gc> LoaderInfoObject<'gc> {
                     .avm2
                     .classes()
                     .eventdispatcher
-                    .construct(activation, &[])?,
+                    .construct(activation, &[])?
+                    .as_object()
+                    .unwrap(),
                 uncaught_error_events: activation
                     .context
                     .avm2
                     .classes()
                     .uncaughterrorevents
-                    .construct(activation, &[])?,
+                    .construct(activation, &[])?
+                    .as_object()
+                    .unwrap(),
                 cached_avm1movie: Lock::new(None),
                 content_type: Cell::new(ContentType::Unknown),
                 expose_content: Cell::new(false),
@@ -276,21 +284,8 @@ impl<'gc> LoaderInfoObject<'gc> {
             if should_complete {
                 let mut activation = Activation::from_nothing(context);
                 if from_url {
-                    let http_status_evt = activation
-                        .avm2()
-                        .classes()
-                        .httpstatusevent
-                        .construct(
-                            &mut activation,
-                            &[
-                                "httpStatus".into(),
-                                false.into(),
-                                false.into(),
-                                status.into(),
-                                redirected.into(),
-                            ],
-                        )
-                        .unwrap();
+                    let http_status_evt =
+                        EventObject::http_status_event(&mut activation, status, redirected);
 
                     Avm2::dispatch_event(context, http_status_evt, (*self).into());
                 }
diff --git a/core/src/avm2/object/local_connection_object.rs b/core/src/avm2/object/local_connection_object.rs
index 518c535ef4689..3248fbfcea94a 100644
--- a/core/src/avm2/object/local_connection_object.rs
+++ b/core/src/avm2/object/local_connection_object.rs
@@ -1,7 +1,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::amf::deserialize_value;
 use crate::avm2::object::script_object::ScriptObjectData;
-use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
+use crate::avm2::object::{ClassObject, EventObject, Object, ObjectPtr, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::{Avm2, Domain, Error};
 use crate::context::UpdateContext;
@@ -105,8 +105,11 @@ impl<'gc> LocalConnectionObject<'gc> {
 
     pub fn send_status(&self, context: &mut UpdateContext<'gc>, status: &'static str) {
         let mut activation = Activation::from_nothing(context);
-        if let Ok(event) = activation.avm2().classes().statusevent.construct(
+
+        let status_event_cls = activation.avm2().classes().statusevent;
+        let event = EventObject::from_class_and_args(
             &mut activation,
+            status_event_cls,
             &[
                 "status".into(),
                 false.into(),
@@ -114,9 +117,9 @@ impl<'gc> LocalConnectionObject<'gc> {
                 Value::Null,
                 status.into(),
             ],
-        ) {
-            Avm2::dispatch_event(activation.context, event, (*self).into());
-        }
+        );
+
+        Avm2::dispatch_event(activation.context, event, (*self).into());
     }
 
     pub fn run_method(
@@ -134,12 +137,14 @@ impl<'gc> LocalConnectionObject<'gc> {
                 .push(deserialize_value(&mut activation, &argument).unwrap_or(Value::Undefined));
         }
 
-        let client = self.client();
+        let client = Value::from(self.client());
         if let Err(e) = client.call_public_property(method_name, &arguments, &mut activation) {
             match e {
                 Error::AvmError(error) => {
-                    if let Ok(event) = activation.avm2().classes().asyncerrorevent.construct(
+                    let async_error_event_cls = activation.avm2().classes().asyncerrorevent;
+                    let event = EventObject::from_class_and_args(
                         &mut activation,
+                        async_error_event_cls,
                         &[
                             "asyncError".into(),
                             false.into(),
@@ -147,9 +152,9 @@ impl<'gc> LocalConnectionObject<'gc> {
                             error,
                             error,
                         ],
-                    ) {
-                        Avm2::dispatch_event(activation.context, event, (*self).into());
-                    }
+                    );
+
+                    Avm2::dispatch_event(activation.context, event, (*self).into());
                 }
                 _ => {
                     tracing::error!("Unhandled error dispatching AVM2 LocalConnection method call to '{method_name}': {e}");
diff --git a/core/src/avm2/object/namespace_object.rs b/core/src/avm2/object/namespace_object.rs
index 837ef8d93c243..36d386b494ced 100644
--- a/core/src/avm2/object/namespace_object.rs
+++ b/core/src/avm2/object/namespace_object.rs
@@ -6,7 +6,7 @@ use crate::avm2::object::{ObjectPtr, TObject};
 use crate::avm2::value::Value;
 use crate::avm2::Error;
 use crate::avm2::Namespace;
-use crate::string::{AvmString, StringContext};
+use crate::string::AvmString;
 use core::fmt;
 use gc_arena::{Collect, Gc, GcWeak};
 
@@ -106,10 +106,6 @@ impl<'gc> TObject<'gc> for NamespaceObject<'gc> {
         Gc::as_ptr(self.0) as *const ObjectPtr
     }
 
-    fn value_of(&self, context: &mut StringContext<'gc>) -> Result<Value<'gc>, Error<'gc>> {
-        Ok(self.0.namespace.as_uri(context).into())
-    }
-
     fn as_namespace(&self) -> Option<Namespace<'gc>> {
         Some(self.0.namespace)
     }
diff --git a/core/src/avm2/object/primitive_object.rs b/core/src/avm2/object/primitive_object.rs
deleted file mode 100644
index 4bf99320de735..0000000000000
--- a/core/src/avm2/object/primitive_object.rs
+++ /dev/null
@@ -1,152 +0,0 @@
-//! Boxed primitives
-
-use core::fmt;
-use std::cell::{Ref, RefMut};
-
-use crate::avm2::activation::Activation;
-use crate::avm2::object::script_object::ScriptObjectData;
-use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
-use crate::avm2::value::Value;
-use crate::avm2::Error;
-use crate::string::{AvmString, StringContext};
-use gc_arena::barrier::unlock;
-use gc_arena::{lock::RefLock, Collect, Gc, GcWeak, Mutation};
-
-/// A class instance allocator that allocates primitive objects.
-pub fn primitive_allocator<'gc>(
-    class: ClassObject<'gc>,
-    activation: &mut Activation<'_, 'gc>,
-) -> Result<Object<'gc>, Error<'gc>> {
-    let base = ScriptObjectData::new(class);
-
-    Ok(PrimitiveObject(Gc::new(
-        activation.gc(),
-        PrimitiveObjectData {
-            base,
-            primitive: RefLock::new(Value::Undefined),
-        },
-    ))
-    .into())
-}
-
-/// An Object which represents a primitive value of some other kind.
-#[derive(Collect, Clone, Copy)]
-#[collect(no_drop)]
-pub struct PrimitiveObject<'gc>(pub Gc<'gc, PrimitiveObjectData<'gc>>);
-
-#[derive(Collect, Clone, Copy, Debug)]
-#[collect(no_drop)]
-pub struct PrimitiveObjectWeak<'gc>(pub GcWeak<'gc, PrimitiveObjectData<'gc>>);
-
-impl fmt::Debug for PrimitiveObject<'_> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("PrimitiveObject")
-            .field("ptr", &Gc::as_ptr(self.0))
-            .finish()
-    }
-}
-
-#[derive(Collect, Clone)]
-#[collect(no_drop)]
-#[repr(C, align(8))]
-pub struct PrimitiveObjectData<'gc> {
-    /// All normal script data.
-    base: ScriptObjectData<'gc>,
-
-    /// The primitive value this object represents.
-    primitive: RefLock<Value<'gc>>,
-}
-
-const _: () = assert!(std::mem::offset_of!(PrimitiveObjectData, base) == 0);
-const _: () = assert!(
-    std::mem::align_of::<PrimitiveObjectData>() == std::mem::align_of::<ScriptObjectData>()
-);
-
-impl<'gc> PrimitiveObject<'gc> {
-    /// Box a primitive into an object.
-    ///
-    /// This function will yield an error if `primitive` is `Undefined`, `Null`,
-    /// or an object already.
-    ///
-    /// In order to prevent stack overflow, this function does *not* call the
-    /// initializer of the primitive class being constructed.
-    pub fn from_primitive(
-        primitive: Value<'gc>,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Object<'gc>, Error<'gc>> {
-        if !primitive.is_primitive() {
-            return Err("Attempted to box an object as a primitive".into());
-        }
-
-        if matches!(primitive, Value::Undefined) {
-            return Err("Cannot box an undefined value".into());
-        } else if matches!(primitive, Value::Null) {
-            return Err("Cannot box a null value".into());
-        }
-
-        let class = match primitive {
-            Value::Bool(_) => activation.avm2().classes().boolean,
-            Value::Number(_) => activation.avm2().classes().number,
-            Value::Integer(_) => activation.avm2().classes().int,
-            Value::String(_) => activation.avm2().classes().string,
-            _ => unreachable!(),
-        };
-
-        let base = ScriptObjectData::new(class);
-        let this: Object<'gc> = PrimitiveObject(Gc::new(
-            activation.gc(),
-            PrimitiveObjectData {
-                base,
-                primitive: RefLock::new(primitive),
-            },
-        ))
-        .into();
-
-        //We explicitly DO NOT CALL the native initializers of primitives here.
-        //If we did so, then those primitive initializers' method types would
-        //trigger the construction of primitive objects... which would need to
-        //be initialized, which forms a cycle.
-
-        Ok(this)
-    }
-}
-
-impl<'gc> TObject<'gc> for PrimitiveObject<'gc> {
-    fn gc_base(&self) -> Gc<'gc, ScriptObjectData<'gc>> {
-        // SAFETY: Object data is repr(C), and a compile-time assert ensures
-        // that the ScriptObjectData stays at offset 0 of the struct- so the
-        // layouts are compatible
-
-        unsafe { Gc::cast(self.0) }
-    }
-
-    fn as_ptr(&self) -> *const ObjectPtr {
-        Gc::as_ptr(self.0) as *const ObjectPtr
-    }
-
-    fn to_locale_string(
-        &self,
-        activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Value<'gc>, Error<'gc>> {
-        match *self.0.primitive.borrow() {
-            val @ Value::Integer(_) => Ok(val),
-            _ => {
-                let class_name = self.instance_class().name().local_name();
-
-                Ok(AvmString::new_utf8(activation.gc(), format!("[object {class_name}]")).into())
-            }
-        }
-    }
-
-    fn value_of(&self, _context: &mut StringContext<'gc>) -> Result<Value<'gc>, Error<'gc>> {
-        Ok(*self.0.primitive.borrow())
-    }
-
-    fn as_primitive(&self) -> Option<Ref<Value<'gc>>> {
-        Some(self.0.primitive.borrow())
-    }
-
-    fn as_primitive_mut(&self, mc: &Mutation<'gc>) -> Option<RefMut<Value<'gc>>> {
-        Some(unlock!(Gc::write(mc, self.0), PrimitiveObjectData, primitive).borrow_mut())
-    }
-}
diff --git a/core/src/avm2/object/proxy_object.rs b/core/src/avm2/object/proxy_object.rs
index 4a43297ff6bc9..9c9758a96c817 100644
--- a/core/src/avm2/object/proxy_object.rs
+++ b/core/src/avm2/object/proxy_object.rs
@@ -66,9 +66,11 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         multiname: &Multiname<'gc>,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let qname = QNameObject::from_name(activation, multiname.clone());
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "getProperty");
-        self.call_property(&prop, &[qname.into()], activation)
+        self_val.call_property(&prop, &[qname.into()], activation)
     }
 
     fn set_property_local(
@@ -77,9 +79,11 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         value: Value<'gc>,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<(), Error<'gc>> {
+        let self_val = Value::from(self);
+
         let qname = QNameObject::from_name(activation, multiname.clone());
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "setProperty");
-        self.call_property(&prop, &[qname.into(), value], activation)?;
+        self_val.call_property(&prop, &[qname.into(), value], activation)?;
 
         Ok(())
     }
@@ -90,12 +94,14 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         arguments: &[Value<'gc>],
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let qname = QNameObject::from_name(activation, multiname.clone());
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "callProperty");
 
         let mut args = vec![qname.into()];
         args.extend_from_slice(arguments);
-        self.call_property(&prop, &args, activation)
+        self_val.call_property(&prop, &args, activation)
     }
 
     fn delete_property_local(
@@ -103,10 +109,12 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         activation: &mut Activation<'_, 'gc>,
         multiname: &Multiname<'gc>,
     ) -> Result<bool, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let qname = QNameObject::from_name(activation, multiname.clone());
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "deleteProperty");
 
-        Ok(self
+        Ok(self_val
             .call_property(&prop, &[qname.into()], activation)?
             .coerce_to_boolean())
     }
@@ -116,8 +124,10 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         activation: &mut Activation<'_, 'gc>,
         name: &Multiname<'gc>,
     ) -> Result<bool, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "hasProperty");
-        Ok(self
+        Ok(self_val
             .call_property(
                 &prop,
                 &[name
@@ -134,9 +144,11 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         name: impl Into<AvmString<'gc>>,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<bool, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let name = name.into();
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "hasProperty");
-        Ok(self
+        Ok(self_val
             .call_property(&prop, &[name.into()], activation)?
             .coerce_to_boolean())
     }
@@ -146,9 +158,12 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         last_index: u32,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<u32, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "nextNameIndex");
 
-        self.call_property(&prop, &[last_index.into()], activation)?
+        self_val
+            .call_property(&prop, &[last_index.into()], activation)?
             .coerce_to_u32(activation)
     }
 
@@ -157,8 +172,10 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         index: u32,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "nextName");
-        self.call_property(&prop, &[index.into()], activation)
+        self_val.call_property(&prop, &[index.into()], activation)
     }
 
     fn get_enumerant_value(
@@ -166,7 +183,9 @@ impl<'gc> TObject<'gc> for ProxyObject<'gc> {
         index: u32,
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
+        let self_val = Value::from(self);
+
         let prop = Multiname::new(activation.avm2().namespaces.proxy, "nextValue");
-        self.call_property(&prop, &[index.into()], activation)
+        self_val.call_property(&prop, &[index.into()], activation)
     }
 }
diff --git a/core/src/avm2/object/regexp_object.rs b/core/src/avm2/object/regexp_object.rs
index 23cd9d5460b10..ad1706ae0b2fa 100644
--- a/core/src/avm2/object/regexp_object.rs
+++ b/core/src/avm2/object/regexp_object.rs
@@ -3,10 +3,8 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::object::script_object::ScriptObjectData;
 use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
-use crate::avm2::regexp::{RegExp, RegExpFlags};
-use crate::avm2::value::Value;
+use crate::avm2::regexp::RegExp;
 use crate::avm2::Error;
-use crate::string::{AvmString, StringContext, WString};
 use core::fmt;
 use gc_arena::barrier::unlock;
 use gc_arena::{lock::RefLock, Collect, Gc, GcWeak, Mutation};
@@ -95,34 +93,6 @@ impl<'gc> TObject<'gc> for RegExpObject<'gc> {
         Gc::as_ptr(self.0) as *const ObjectPtr
     }
 
-    fn value_of(&self, context: &mut StringContext<'gc>) -> Result<Value<'gc>, Error<'gc>> {
-        let regexp = self.0.regexp.borrow();
-        let mut s = WString::new();
-        s.push_byte(b'/');
-        s.push_str(&regexp.source());
-        s.push_byte(b'/');
-
-        let flags = regexp.flags();
-
-        if flags.contains(RegExpFlags::GLOBAL) {
-            s.push_byte(b'g');
-        }
-        if flags.contains(RegExpFlags::IGNORE_CASE) {
-            s.push_byte(b'i');
-        }
-        if flags.contains(RegExpFlags::MULTILINE) {
-            s.push_byte(b'm');
-        }
-        if flags.contains(RegExpFlags::DOTALL) {
-            s.push_byte(b's');
-        }
-        if flags.contains(RegExpFlags::EXTENDED) {
-            s.push_byte(b'x');
-        }
-
-        Ok(AvmString::new(context.gc(), s).into())
-    }
-
     /// Unwrap this object as a regexp.
     fn as_regexp_object(&self) -> Option<RegExpObject<'gc>> {
         Some(*self)
diff --git a/core/src/avm2/object/script_object.rs b/core/src/avm2/object/script_object.rs
index e5cf8aaf4a6f6..1fdc51845a2be 100644
--- a/core/src/avm2/object/script_object.rs
+++ b/core/src/avm2/object/script_object.rs
@@ -75,7 +75,7 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
     }
 }
 
-fn maybe_int_property(name: AvmString<'_>) -> DynamicKey<'_> {
+pub fn maybe_int_property(name: AvmString<'_>) -> DynamicKey<'_> {
     // TODO: this should use a custom implementation, not parse()
     // FP is much stricter here, only allowing pure natural numbers without sign or leading zeros
     if let Ok(val) = name.parse::<u32>() {
@@ -349,12 +349,6 @@ impl<'gc> ScriptObjectWrapper<'gc> {
         self.bound_methods().get(id as usize).and_then(|v| *v)
     }
 
-    pub fn has_trait(&self, name: &Multiname<'gc>) -> bool {
-        // Class instances have instance traits from any class in the base
-        // class chain.
-        self.vtable().has_trait(name)
-    }
-
     pub fn has_own_dynamic_property(&self, name: &Multiname<'gc>) -> bool {
         if name.contains_public_namespace() {
             if let Some(name) = name.local_name() {
@@ -366,7 +360,7 @@ impl<'gc> ScriptObjectWrapper<'gc> {
     }
 
     pub fn has_own_property(&self, name: &Multiname<'gc>) -> bool {
-        self.has_trait(name) || self.has_own_dynamic_property(name)
+        self.vtable().has_trait(name) || self.has_own_dynamic_property(name)
     }
 
     pub fn proto(&self) -> Option<Object<'gc>> {
diff --git a/core/src/avm2/object/sound_object.rs b/core/src/avm2/object/sound_object.rs
index 22023e396677f..c7dc52aa9d0ab 100644
--- a/core/src/avm2/object/sound_object.rs
+++ b/core/src/avm2/object/sound_object.rs
@@ -173,7 +173,10 @@ impl<'gc> SoundObject<'gc> {
             .classes()
             .id3info
             .construct(activation, &[])
-            .expect("failed to construct ID3Info object");
+            .expect("failed to construct ID3Info object")
+            .as_object()
+            .unwrap();
+
         let tag = Tag::read_from2(Cursor::new(bytes));
         if let Ok(ref tag) = tag {
             if let Some(v) = tag.album() {
diff --git a/core/src/avm2/object/xml_list_object.rs b/core/src/avm2/object/xml_list_object.rs
index 7c76310a334c7..9c1a5d52327f2 100644
--- a/core/src/avm2/object/xml_list_object.rs
+++ b/core/src/avm2/object/xml_list_object.rs
@@ -572,9 +572,7 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> {
         arguments: &[Value<'gc>],
         activation: &mut Activation<'_, 'gc>,
     ) -> Result<Value<'gc>, Error<'gc>> {
-        let method = self
-            .proto()
-            .expect("XMLList missing prototype")
+        let method = Value::from(self.proto().expect("XMLList missing prototype"))
             .get_property(multiname, activation)?;
 
         // See https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/XMLListObject.cpp#L50
@@ -604,11 +602,10 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> {
             if let Some(list) = prop.as_object().and_then(|obj| obj.as_xml_list_object()) {
                 if list.length() == 0 && self.length() == 1 {
                     let mut children = self.children_mut(activation.gc());
-                    return children
-                        .first_mut()
-                        .unwrap()
-                        .get_or_create_xml(activation)
-                        .call_property(multiname, arguments, activation);
+
+                    let child = children.first_mut().unwrap().get_or_create_xml(activation);
+
+                    return Value::from(child).call_property(multiname, arguments, activation);
                 }
             }
         }
@@ -977,6 +974,8 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> {
                                 .classes()
                                 .xml
                                 .construct(activation, &[value])?
+                                .as_object()
+                                .unwrap()
                                 .as_xml_object()
                                 .expect("Should be XML Object");
                             children[index] = E4XOrXml::Xml(xml);
diff --git a/core/src/avm2/object/xml_object.rs b/core/src/avm2/object/xml_object.rs
index e4e5bf74a5523..afe774875c83f 100644
--- a/core/src/avm2/object/xml_object.rs
+++ b/core/src/avm2/object/xml_object.rs
@@ -17,7 +17,6 @@ use gc_arena::{lock::Lock, Collect, Gc, GcWeak, Mutation};
 use ruffle_wstr::WString;
 
 use super::xml_list_object::{E4XOrXml, XmlOrXmlListObject};
-use super::PrimitiveObject;
 
 /// A class instance allocator that allocates XML objects.
 pub fn xml_allocator<'gc>(
@@ -357,9 +356,7 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
     ) -> Result<Value<'gc>, Error<'gc>> {
         let this = self.as_xml_object().unwrap();
 
-        let method = self
-            .proto()
-            .expect("XMLList missing prototype")
+        let method = Value::from(self.proto().expect("XMLList missing prototype"))
             .get_property(multiname, activation)?;
 
         // If the method doesn't exist on the prototype, and we have simple content,
@@ -374,10 +371,8 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
             let prop = self.get_property_local(multiname, activation)?;
             if let Some(list) = prop.as_object().and_then(|obj| obj.as_xml_list_object()) {
                 if list.length() == 0 && this.node().has_simple_content() {
-                    let receiver = PrimitiveObject::from_primitive(
-                        this.node().xml_to_string(activation).into(),
-                        activation,
-                    )?;
+                    let receiver = Value::String(this.node().xml_to_string(activation));
+
                     return receiver.call_property(multiname, arguments, activation);
                 }
             }
diff --git a/core/src/avm2/optimize.rs b/core/src/avm2/optimize.rs
index 2fa4f74b200b2..3e0cbe54f70c1 100644
--- a/core/src/avm2/optimize.rs
+++ b/core/src/avm2/optimize.rs
@@ -1,7 +1,6 @@
 use crate::avm2::error::verify_error;
 use crate::avm2::method::{BytecodeMethod, ResolvedParamConfig};
 use crate::avm2::multiname::Multiname;
-use crate::avm2::object::TObject;
 use crate::avm2::op::Op;
 use crate::avm2::property::Property;
 use crate::avm2::verify::{Exception, JumpSource};
@@ -1140,7 +1139,7 @@ pub fn optimize<'gc>(
                         .outer()
                         .get_unchecked(*index as usize)
                         .values()
-                        .instance_class();
+                        .instance_class(activation);
                     stack.push_class(activation, class)?;
                 }
                 Op::Pop => {
@@ -1190,7 +1189,9 @@ pub fn optimize<'gc>(
                         }
 
                         if !stack_push_done {
-                            if let Some(info) = outer_scope.get_entry_for_multiname(&multiname) {
+                            if let Some(info) =
+                                outer_scope.get_entry_for_multiname(activation, &multiname)
+                            {
                                 if let Some((class, index)) = info {
                                     *op = Op::GetOuterScope { index };
 
@@ -1682,8 +1683,9 @@ pub fn optimize<'gc>(
                     let outer_scope = activation.outer();
                     if !outer_scope.is_empty() {
                         let global_scope = outer_scope.get_unchecked(0);
+                        let global_class = global_scope.values().instance_class(activation);
 
-                        stack.push_class(activation, global_scope.values().instance_class())?;
+                        stack.push_class(activation, global_class)?;
                     } else if has_simple_scoping {
                         stack.push(activation, this_value)?;
                     } else {
@@ -1713,7 +1715,7 @@ pub fn optimize<'gc>(
                     if !outer_scope.is_empty() {
                         let global_scope = outer_scope.get_unchecked(0);
 
-                        let class = global_scope.values().instance_class();
+                        let class = global_scope.values().instance_class(activation);
                         let mut value_class = class.vtable().slot_classes()[*slot_id as usize];
                         let resolved_value_class = value_class.get_class(activation);
                         if let Ok(class) = resolved_value_class {
diff --git a/core/src/avm2/parameters.rs b/core/src/avm2/parameters.rs
index abd006c302c6a..eb84c340144d1 100644
--- a/core/src/avm2/parameters.rs
+++ b/core/src/avm2/parameters.rs
@@ -1,5 +1,4 @@
 use crate::avm2::error::make_error_2007;
-use crate::avm2::object::PrimitiveObject;
 use crate::avm2::Object;
 use crate::avm2::{Activation, Error, Value};
 use crate::string::AvmString;
@@ -12,9 +11,10 @@ pub trait ParametersExt<'gc> {
     /// Gets the value at the given index.
     fn get_value(&self, index: usize) -> Value<'gc>;
 
-    /// Gets the value at the given index and coerces it to an Object.
+    /// Gets the value at the given index as an Object. It is expected that the
+    /// value is either Object or Null.
     ///
-    /// If the value is null or is undefined, a TypeError 2007 is raised.
+    /// If the value is null, a TypeError 2007 is raised.
     fn get_object(
         &self,
         activation: &mut Activation<'_, 'gc>,
@@ -22,9 +22,10 @@ pub trait ParametersExt<'gc> {
         name: &'static str,
     ) -> Result<Object<'gc>, Error<'gc>>;
 
-    /// Tries to get the value at the given index and coerce it to an Object.
+    /// Gets the value at the given index as an Object. It is expected that the
+    /// value is either Object or Null.
     ///
-    /// If the value is null or is undefined, None is returned.
+    /// If the value is null, None is returned.
     fn try_get_object(
         &self,
         activation: &mut Activation<'_, 'gc>,
@@ -107,25 +108,21 @@ impl<'gc> ParametersExt<'gc> for &[Value<'gc>] {
         name: &'static str,
     ) -> Result<Object<'gc>, Error<'gc>> {
         match self[index] {
-            Value::Null | Value::Undefined => Err(make_error_2007(activation, name)),
+            Value::Null => Err(make_error_2007(activation, name)),
             Value::Object(o) => Ok(o),
-            primitive => Ok(PrimitiveObject::from_primitive(primitive, activation)
-                .expect("Primitive object is infallible at this point")),
+            _ => panic!("get_object should read Object or Null"),
         }
     }
 
     fn try_get_object(
         &self,
-        activation: &mut Activation<'_, 'gc>,
+        _activation: &mut Activation<'_, 'gc>,
         index: usize,
     ) -> Option<Object<'gc>> {
         match self[index] {
-            Value::Null | Value::Undefined => None,
+            Value::Null => None,
             Value::Object(o) => Some(o),
-            primitive => Some(
-                PrimitiveObject::from_primitive(primitive, activation)
-                    .expect("Primitive object is infallible at this point"),
-            ),
+            _ => panic!("try_get_object should read Object or Null"),
         }
     }
 
diff --git a/core/src/avm2/scope.rs b/core/src/avm2/scope.rs
index ec0aef8f7cf17..c89d18565d870 100644
--- a/core/src/avm2/scope.rs
+++ b/core/src/avm2/scope.rs
@@ -3,7 +3,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::class::Class;
 use crate::avm2::domain::Domain;
-use crate::avm2::object::{Object, TObject};
+use crate::avm2::object::TObject;
 use crate::avm2::value::Value;
 use crate::avm2::Error;
 use crate::avm2::{Multiname, Namespace};
@@ -19,7 +19,7 @@ use super::property_map::PropertyMap;
 #[collect(no_drop)]
 pub struct Scope<'gc> {
     /// The underlying object of this Scope
-    values: Object<'gc>,
+    values: Value<'gc>,
 
     /// Indicates whether or not this is a `with` scope.
     ///
@@ -29,16 +29,22 @@ pub struct Scope<'gc> {
 }
 
 impl<'gc> Scope<'gc> {
-    /// Creates a new regular Scope
-    pub fn new(values: Object<'gc>) -> Self {
+    /// Creates a new regular Scope.
+    ///
+    /// It is the caller's responsibility to ensure that the `values` passed
+    /// to this method is not Value::Null or Value::Undefined.
+    pub fn new(values: Value<'gc>) -> Self {
         Self {
             values,
             with: false,
         }
     }
 
-    /// Creates a new `with` Scope
-    pub fn new_with(values: Object<'gc>) -> Self {
+    /// Creates a new `with` Scope.
+    ///
+    /// It is the caller's responsibility to ensure that the `values` passed
+    /// to this method is not Value::Null or Value::Undefined.
+    pub fn new_with(values: Value<'gc>) -> Self {
         Self { values, with: true }
     }
 
@@ -46,7 +52,7 @@ impl<'gc> Scope<'gc> {
         self.with
     }
 
-    pub fn values(&self) -> Object<'gc> {
+    pub fn values(&self) -> Value<'gc> {
         self.values
     }
 }
@@ -60,7 +66,7 @@ struct ScopeContainer<'gc> {
 
     /// The cache of this ScopeChain. A value of None indicates that caching is disabled
     /// for this ScopeChain.
-    cache: Option<RefLock<PropertyMap<'gc, Object<'gc>>>>,
+    cache: Option<RefLock<PropertyMap<'gc, Value<'gc>>>>,
 }
 
 impl<'gc> ScopeContainer<'gc> {
@@ -174,20 +180,20 @@ impl<'gc> ScopeChain<'gc> {
         &self,
         multiname: &Multiname<'gc>,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Option<(Option<Namespace<'gc>>, Object<'gc>)>, Error<'gc>> {
+    ) -> Result<Option<(Option<Namespace<'gc>>, Value<'gc>)>, Error<'gc>> {
         if let Some(container) = self.container {
             // We skip the scope at depth 0 (the global scope). The global scope will be checked in a different phase.
             for scope in container.scopes.iter().skip(1).rev() {
                 // NOTE: We are manually searching the vtable's traits so we can figure out which namespace the trait
                 // belongs to.
                 let values = scope.values();
-                let vtable = values.vtable();
+                let vtable = values.vtable(activation);
                 if let Some((namespace, _)) = vtable.get_trait_with_ns(multiname) {
                     return Ok(Some((Some(namespace), values)));
                 }
 
                 // Wasn't in the objects traits, let's try dynamic properties if this is a with scope.
-                if scope.with() && values.has_own_property(multiname) {
+                if scope.with() && values.has_own_property(activation, multiname) {
                     // NOTE: We return the QName as `None` to indicate that we should never cache this result.
                     // We NEVER cache the result of dynamic properties (and can't anyway because of the check
                     // in ScopeContainer::new).
@@ -199,7 +205,7 @@ impl<'gc> ScopeChain<'gc> {
         if let Some((qname, script)) = self.domain.get_defining_script(multiname)? {
             return Ok(Some((
                 Some(qname.namespace()),
-                script.globals(activation.context)?,
+                script.globals(activation.context)?.into(),
             )));
         }
         Ok(None)
@@ -209,7 +215,7 @@ impl<'gc> ScopeChain<'gc> {
         &self,
         multiname: &Multiname<'gc>,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Option<Object<'gc>>, Error<'gc>> {
+    ) -> Result<Option<Value<'gc>>, Error<'gc>> {
         // First we check the cache of our container
         if let Some(container) = self.container {
             if let Some(cache) = &container.cache {
@@ -237,6 +243,7 @@ impl<'gc> ScopeChain<'gc> {
 
     pub fn get_entry_for_multiname(
         &self,
+        activation: &mut Activation<'_, 'gc>,
         multiname: &Multiname<'gc>,
     ) -> Option<Option<(Class<'gc>, u32)>> {
         if let Some(container) = self.container {
@@ -248,8 +255,8 @@ impl<'gc> ScopeChain<'gc> {
                 }
 
                 let values = scope.values();
-                if values.has_trait(multiname) {
-                    return Some(Some((values.instance_class(), index as u32)));
+                if values.has_trait(activation, multiname) {
+                    return Some(Some((values.instance_class(activation), index as u32)));
                 }
             }
         }
@@ -277,24 +284,73 @@ impl<'gc> ScopeChain<'gc> {
 /// The `global` parameter indicates whether we are on global$init (script initializer).
 /// When the `global` parameter is true, the scope at depth 0 is considered the global scope, and is skipped.
 pub fn search_scope_stack<'gc>(
-    scopes: &[Scope<'gc>],
+    activation: &mut Activation<'_, 'gc>,
     multiname: &Multiname<'gc>,
     global: bool,
-) -> Result<Option<Object<'gc>>, Error<'gc>> {
+) -> Result<Option<Value<'gc>>, Error<'gc>> {
+    let classes = activation.context.avm2.classes();
+
+    let scopes = activation.scope_frame();
+
     for (depth, scope) in scopes.iter().enumerate().rev() {
         if depth == 0 && global {
             continue;
         }
         let values = scope.values();
 
-        if values.has_trait(multiname) {
+        if value_has_trait(classes, values, multiname) {
             return Ok(Some(values));
         } else if scope.with() {
             // We search the dynamic properties if this is a with scope.
-            if values.has_own_property(multiname) {
+            if value_has_own_property(classes, values, multiname) {
                 return Ok(Some(values));
             }
         }
     }
     Ok(None)
 }
+
+use crate::avm2::globals::SystemClasses;
+
+// Check if a Value has a trait without using an Activation. Used only in `search_scope_stack`.
+fn value_has_trait<'gc>(
+    classes: &SystemClasses<'gc>,
+    value: Value<'gc>,
+    multiname: &Multiname<'gc>,
+) -> bool {
+    let vtable = match value {
+        Value::Bool(_) => classes.boolean.instance_vtable(),
+        Value::Number(_) | Value::Integer(_) => classes.number.instance_vtable(),
+        Value::String(_) => classes.string.instance_vtable(),
+        Value::Object(obj) => obj.vtable(),
+
+        Value::Undefined | Value::Null => {
+            unreachable!("Should not have Undefined or Null scope")
+        }
+    };
+
+    vtable.has_trait(multiname)
+}
+
+// Check if a Value has a property without using an Activation. Used only in `search_scope_stack`.
+fn value_has_own_property<'gc>(
+    classes: &SystemClasses<'gc>,
+    value: Value<'gc>,
+    multiname: &Multiname<'gc>,
+) -> bool {
+    let vtable = match value {
+        Value::Bool(_) => classes.boolean.instance_vtable(),
+        Value::Number(_) | Value::Integer(_) => classes.number.instance_vtable(),
+        Value::String(_) => classes.string.instance_vtable(),
+        Value::Object(obj) => obj.vtable(),
+
+        Value::Undefined | Value::Null => {
+            unreachable!("Should not have Undefined or Null scope")
+        }
+    };
+
+    match value {
+        Value::Object(object) => object.has_own_property(multiname),
+        _ => vtable.has_trait(multiname),
+    }
+}
diff --git a/core/src/avm2/value.rs b/core/src/avm2/value.rs
index 6e4eedfbbd034..87303b628c4f7 100644
--- a/core/src/avm2/value.rs
+++ b/core/src/avm2/value.rs
@@ -3,11 +3,12 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::error;
 use crate::avm2::error::type_error;
-use crate::avm2::object::{NamespaceObject, Object, PrimitiveObject, TObject};
+use crate::avm2::function::exec;
+use crate::avm2::object::{NamespaceObject, Object, TObject};
+use crate::avm2::property::Property;
 use crate::avm2::script::TranslationUnit;
-use crate::avm2::Error;
-use crate::avm2::Multiname;
-use crate::avm2::Namespace;
+use crate::avm2::vtable::{ClassBoundMethod, VTable};
+use crate::avm2::{Error, Multiname, Namespace};
 use crate::ecma_conversions::{f64_to_wrapping_i32, f64_to_wrapping_u32};
 use crate::string::{AvmAtom, AvmString, WStr};
 use gc_arena::Collect;
@@ -600,13 +601,7 @@ impl<'gc> Value<'gc> {
             Value::Number(f) => !f.is_nan() && *f != 0.0,
             Value::Integer(i) => *i != 0,
             Value::String(s) => !s.is_empty(),
-            Value::Object(o) => {
-                if let Some(prim) = o.as_primitive() {
-                    prim.coerce_to_boolean()
-                } else {
-                    true
-                }
-            }
+            Value::Object(_) => true,
         }
     }
 
@@ -633,48 +628,41 @@ impl<'gc> Value<'gc> {
         });
 
         match self {
-            Value::Object(Object::PrimitiveObject(o)) => o.value_of(activation.strings()),
-            Value::Object(o) if hint == Hint::String => {
-                let object = *o;
-
-                let prim = object.call_public_property("toString", &[], activation)?;
+            Value::Object(_) if hint == Hint::String => {
+                let prim = self.call_public_property("toString", &[], activation)?;
                 if prim.is_primitive() {
                     return Ok(prim);
                 }
 
-                let prim = object.call_public_property("valueOf", &[], activation)?;
+                let prim = self.call_public_property("valueOf", &[], activation)?;
                 if prim.is_primitive() {
                     return Ok(prim);
                 }
 
+                let class_name = self.instance_of_class_name(activation);
+
                 Err(Error::AvmError(type_error(
                     activation,
-                    &format!(
-                        "Error #1050: Cannot convert {} to primitive.",
-                        o.instance_of_class_name(activation.gc())
-                    ),
+                    &format!("Error #1050: Cannot convert {} to primitive.", class_name,),
                     1050,
                 )?))
             }
-            Value::Object(o) if hint == Hint::Number => {
-                let object = *o;
-
-                let prim = object.call_public_property("valueOf", &[], activation)?;
+            Value::Object(_) if hint == Hint::Number => {
+                let prim = self.call_public_property("valueOf", &[], activation)?;
                 if prim.is_primitive() {
                     return Ok(prim);
                 }
 
-                let prim = object.call_public_property("toString", &[], activation)?;
+                let prim = self.call_public_property("toString", &[], activation)?;
                 if prim.is_primitive() {
                     return Ok(prim);
                 }
 
+                let class_name = self.instance_of_class_name(activation);
+
                 Err(Error::AvmError(type_error(
                     activation,
-                    &format!(
-                        "Error #1050: Cannot convert {} to primitive.",
-                        o.instance_of_class_name(activation.gc())
-                    ),
+                    &format!("Error #1050: Cannot convert {} to primitive.", class_name,),
                     1050,
                 )?))
             }
@@ -836,68 +824,564 @@ impl<'gc> Value<'gc> {
     /// contexts, where strings themselves also get quoted. Such contexts would
     /// include things like `valueOf`/`toString` on classes that expose their
     /// properties as part of the string.
-    pub fn coerce_to_debug_string<'a>(
-        &'a self,
+    pub fn as_debug_string(
+        &self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<AvmString<'gc>, Error<'gc>> {
+    ) -> Result<String, Error<'gc>> {
         Ok(match self {
-            Value::String(s) => AvmString::new_utf8(activation.gc(), format!("\"{s}\"")),
-            Value::Object(_) => self
-                .coerce_to_primitive(Some(Hint::String), activation)?
-                .coerce_to_debug_string(activation)?,
-            _ => self.coerce_to_string(activation)?,
+            Value::String(s) => format!("\"{s}\""),
+            Value::Object(obj) => {
+                // Flash prints the class name (ignoring the toString() impl on the object),
+                // followed by something that looks like an address (it varies between executions).
+                // For now, we just set the "address" to all zeroes, on the off chance that some
+                // application is trying to parse the error message.
+                format!(
+                    "{}@00000000000",
+                    obj.instance_of_class_name(activation.gc())
+                )
+            }
+            _ => self.coerce_to_string(activation)?.to_string(),
         })
     }
 
-    /// Coerce the value to an Object.
-    ///
-    /// TODO: Once `PrimitiveObject` is removed, this method will be able
-    /// to be removed too, since all that this will do then is a null/undefined check.
-    pub fn coerce_to_object(
+    #[inline(always)]
+    pub fn null_check(
         &self,
         activation: &mut Activation<'_, 'gc>,
-    ) -> Result<Object<'gc>, Error<'gc>> {
+        name: Option<&Multiname<'gc>>,
+    ) -> Result<Value<'gc>, Error<'gc>> {
+        if matches!(self, Value::Null | Value::Undefined) {
+            return Err(error::make_null_or_undefined_error(activation, *self, name));
+        }
+
+        Ok(*self)
+    }
+
+    #[inline(always)]
+    pub fn as_object(&self) -> Option<Object<'gc>> {
         match self {
-            Value::Undefined => return Err("TypeError: undefined is not an Object".into()),
-            Value::Null => return Err("TypeError: null is not an Object".into()),
-            Value::Object(o) => return Ok(*o),
-            _ => {}
-        };
+            Value::Object(o) => Some(*o),
+            _ => None,
+        }
+    }
+
+    /// Retrieve a property by Multiname lookup.
+    ///
+    /// This corresponds directly to the AVM2 operation `getproperty`, with the
+    /// exception that it does not special-case object lookups on dictionary
+    /// structured objects.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn get_property(
+        &self,
+        multiname: &Multiname<'gc>,
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<Value<'gc>, Error<'gc>> {
+        let vtable = self.vtable(activation);
+
+        match vtable.get_trait(multiname) {
+            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
+                // Only objects can have slots
+                let object = self.as_object().unwrap();
+
+                Ok(object.get_slot(slot_id))
+            }
+            Some(Property::Method { disp_id }) => {
+                if let Some(object) = self.as_object() {
+                    // avmplus has a special case for XML and XMLList objects, so we need one as well
+                    // https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/Toplevel.cpp#L629-L634
+                    if (object.as_xml_object().is_some() || object.as_xml_list_object().is_some())
+                        && multiname.contains_public_namespace()
+                    {
+                        return object.get_property_local(multiname, activation);
+                    }
+
+                    if let Some(bound_method) = object.get_bound_method(disp_id) {
+                        return Ok(bound_method.into());
+                    }
+
+                    let bound_method = vtable
+                        .make_bound_method(activation, *self, disp_id)
+                        .expect("Method should exist");
+
+                    // TODO: Bound methods should be cached on the Method in a
+                    // WeakKeyHashMap<Value, FunctionObject>, not on the Object
+                    object.install_bound_method(activation.gc(), disp_id, bound_method);
+
+                    Ok(bound_method.into())
+                } else {
+                    let bound_method = vtable
+                        .make_bound_method(activation, *self, disp_id)
+                        .expect("Method should exist");
+
+                    // TODO: Bound methods should be cached on the Method in a
+                    // WeakKeyHashMap<Value, FunctionObject>, not on the Object
+
+                    Ok(bound_method.into())
+                }
+            }
+            Some(Property::Virtual { get: Some(get), .. }) => {
+                self.call_method(get, &[], activation)
+            }
+            Some(Property::Virtual { get: None, .. }) => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::ReadFromWriteOnly,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            None => {
+                if let Some(object) = self.as_object() {
+                    object.get_property_local(multiname, activation)
+                } else {
+                    let instance_class = self.instance_class(activation);
+
+                    if !multiname.contains_public_namespace() {
+                        return Err(error::make_reference_error(
+                            activation,
+                            error::ReferenceErrorCode::InvalidRead,
+                            multiname,
+                            instance_class,
+                        ));
+                    }
+
+                    let Some(local_name) = multiname.local_name() else {
+                        // when can this happen?
+                        return Err(error::make_reference_error(
+                            activation,
+                            error::ReferenceErrorCode::InvalidRead,
+                            multiname,
+                            instance_class,
+                        ));
+                    };
+
+                    let key = crate::avm2::object::maybe_int_property(local_name);
+
+                    // `get_property` also checks prototype chain
+                    let mut proto = self.proto(activation);
+
+                    while let Some(obj) = proto {
+                        let obj = obj.base();
+                        let values = obj.values();
+                        let value = values.as_hashmap().get(&key);
+                        if let Some(value) = value {
+                            return Ok(value.value);
+                        }
+                        proto = obj.proto();
+                    }
+
+                    // Primitive classes are sealed
+                    Err(error::make_reference_error(
+                        activation,
+                        error::ReferenceErrorCode::InvalidRead,
+                        multiname,
+                        instance_class,
+                    ))
+                }
+            }
+        }
+    }
 
-        PrimitiveObject::from_primitive(*self, activation)
+    /// Same as get_property, but constructs a public Multiname for you.
+    pub fn get_public_property(
+        &self,
+        name: impl Into<AvmString<'gc>>,
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<Value<'gc>, Error<'gc>> {
+        self.get_property(
+            &Multiname::new(activation.avm2().find_public_namespace(), name),
+            activation,
+        )
     }
 
-    /// Coerce the value to an object, and throw a TypeError relating to object
-    /// receivers being null or undefined otherwise.
-    /// Note: The error may contain a non-spec info about the way in which it was to be used.
-    pub fn coerce_to_object_or_typeerror(
+    /// Set a property by Multiname lookup.
+    ///
+    /// This corresponds directly with the AVM2 operation `setproperty`, with
+    /// the exception that it does not special-case object lookups on
+    /// dictionary structured objects.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn set_property(
         &self,
+        multiname: &Multiname<'gc>,
+        value: Value<'gc>,
         activation: &mut Activation<'_, 'gc>,
-        name: Option<&Multiname<'gc>>,
-    ) -> Result<Object<'gc>, Error<'gc>> {
-        if matches!(self, Value::Null | Value::Undefined) {
-            return Err(error::make_null_or_undefined_error(activation, *self, name));
+    ) -> Result<(), Error<'gc>> {
+        let vtable = self.vtable(activation);
+
+        match vtable.get_trait(multiname) {
+            Some(Property::Slot { slot_id }) => {
+                // Only objects can have slots
+                let object = self.as_object().unwrap();
+
+                object.set_slot(slot_id, value, activation)
+            }
+            Some(Property::Method { .. }) => {
+                if let Some(object) = self.as_object() {
+                    // Similar to the get_property special case for XML/XMLList.
+                    if (object.as_xml_object().is_some() || object.as_xml_list_object().is_some())
+                        && multiname.contains_public_namespace()
+                    {
+                        return object.set_property_local(multiname, value, activation);
+                    }
+                }
+
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::AssignToMethod,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            Some(Property::Virtual { set: Some(set), .. }) => {
+                self.call_method(set, &[value], activation).map(|_| ())
+            }
+            Some(Property::ConstSlot { .. }) | Some(Property::Virtual { set: None, .. }) => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::WriteToReadOnly,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            None => {
+                if let Some(object) = self.as_object() {
+                    object.set_property_local(multiname, value, activation)
+                } else {
+                    let instance_class = self.instance_class(activation);
+
+                    // Primitive classes are sealed
+                    Err(error::make_reference_error(
+                        activation,
+                        error::ReferenceErrorCode::InvalidWrite,
+                        multiname,
+                        instance_class,
+                    ))
+                }
+            }
         }
-        self.coerce_to_object(activation)
     }
 
-    #[inline(always)]
-    pub fn null_check(
+    /// Same as set_property, but constructs a public Multiname for you.
+    pub fn set_public_property(
         &self,
+        name: impl Into<AvmString<'gc>>,
+        value: Value<'gc>,
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<(), Error<'gc>> {
+        let name = Multiname::new(activation.avm2().namespaces.public_vm_internal(), name);
+        self.set_property(&name, value, activation)
+    }
+
+    /// Initialize a property by Multiname lookup.
+    ///
+    /// This corresponds directly with the AVM2 operation `initproperty`.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn init_property(
+        &self,
+        multiname: &Multiname<'gc>,
+        value: Value<'gc>,
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<(), Error<'gc>> {
+        let vtable = self.vtable(activation);
+
+        match vtable.get_trait(multiname) {
+            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
+                // Only objects can have slots
+                let object = self.as_object().unwrap();
+
+                object.set_slot(slot_id, value, activation)
+            }
+            Some(Property::Method { .. }) => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::AssignToMethod,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            Some(Property::Virtual { set: Some(set), .. }) => {
+                self.call_method(set, &[value], activation).map(|_| ())
+            }
+            Some(Property::Virtual { set: None, .. }) => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::WriteToReadOnly,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            None => {
+                if let Some(object) = self.as_object() {
+                    object.init_property_local(multiname, value, activation)
+                } else {
+                    let instance_class = self.instance_class(activation);
+
+                    // Primitive classes are sealed
+                    Err(error::make_reference_error(
+                        activation,
+                        error::ReferenceErrorCode::InvalidWrite,
+                        multiname,
+                        instance_class,
+                    ))
+                }
+            }
+        }
+    }
+
+    /// Call a named property on the object.
+    ///
+    /// This corresponds directly to the `callproperty` operation in AVM2.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn call_property(
+        &self,
+        multiname: &Multiname<'gc>,
+        arguments: &[Value<'gc>],
         activation: &mut Activation<'_, 'gc>,
-        name: Option<&Multiname<'gc>>,
     ) -> Result<Value<'gc>, Error<'gc>> {
-        if matches!(self, Value::Null | Value::Undefined) {
-            return Err(error::make_null_or_undefined_error(activation, *self, name));
+        let vtable = self.vtable(activation);
+
+        match vtable.get_trait(multiname) {
+            Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
+                // Only objects can have slots
+                let object = self.as_object().unwrap();
+
+                let func = object.get_slot(slot_id);
+                func.call(activation, *self, arguments)
+            }
+            Some(Property::Method { disp_id }) => self.call_method(disp_id, arguments, activation),
+            Some(Property::Virtual { get: Some(get), .. }) => {
+                let obj = self.call_method(get, &[], activation)?;
+
+                obj.call(activation, *self, arguments)
+            }
+            Some(Property::Virtual { get: None, .. }) => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::ReadFromWriteOnly,
+                    multiname,
+                    instance_class,
+                ))
+            }
+            None => {
+                if let Some(object) = self.as_object() {
+                    object.call_property_local(multiname, arguments, activation)
+                } else {
+                    let instance_class = self.instance_class(activation);
+
+                    if !multiname.contains_public_namespace() {
+                        return Err(error::make_reference_error(
+                            activation,
+                            error::ReferenceErrorCode::InvalidRead,
+                            multiname,
+                            instance_class,
+                        ));
+                    }
+
+                    let Some(local_name) = multiname.local_name() else {
+                        // when can this happen?
+                        return Err(error::make_reference_error(
+                            activation,
+                            error::ReferenceErrorCode::InvalidRead,
+                            multiname,
+                            instance_class,
+                        ));
+                    };
+
+                    let key = crate::avm2::object::maybe_int_property(local_name);
+
+                    // Check prototype chain
+                    let mut proto = self.proto(activation);
+
+                    while let Some(obj) = proto {
+                        let obj = obj.base();
+                        let values = obj.values();
+                        let value = values.as_hashmap().get(&key);
+                        if let Some(value) = value {
+                            return value.value.call(activation, *self, arguments);
+                        }
+                        proto = obj.proto();
+                    }
+
+                    Err(Error::AvmError(type_error(
+                        activation,
+                        "Error #1006: value is not a function.",
+                        1006,
+                    )?))
+                }
+            }
         }
+    }
 
-        Ok(*self)
+    /// Same as call_property, but constructs a public Multiname for you.
+    pub fn call_public_property(
+        &self,
+        name: impl Into<AvmString<'gc>>,
+        arguments: &[Value<'gc>],
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<Value<'gc>, Error<'gc>> {
+        self.call_property(
+            &Multiname::new(activation.avm2().find_public_namespace(), name),
+            arguments,
+            activation,
+        )
     }
 
-    pub fn as_object(&self) -> Option<Object<'gc>> {
+    /// Call a method by its index.
+    ///
+    /// This directly corresponds with the AVM2 operation `callmethod`.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn call_method(
+        &self,
+        id: u32,
+        arguments: &[Value<'gc>],
+        activation: &mut Activation<'_, 'gc>,
+    ) -> Result<Value<'gc>, Error<'gc>> {
+        // TODO: Bound methods should be cached on the Method in a
+        // WeakKeyHashMap<Value, FunctionObject>, not on the Object
+        if let Some(object) = self.as_object() {
+            if let Some(bound_method) = object.get_bound_method(id) {
+                return bound_method.call(activation, *self, arguments);
+            }
+        }
+
+        let vtable = self.vtable(activation);
+
+        let full_method = vtable.get_full_method(id).expect("Method should exist");
+
+        // Execute immediately if this method doesn't require binding
+        if !full_method.method.needs_arguments_object() {
+            let ClassBoundMethod {
+                class,
+                super_class_obj,
+                scope,
+                method,
+            } = full_method;
+
+            return exec(
+                method,
+                scope.expect("Scope should exist here"),
+                *self,
+                super_class_obj,
+                Some(class),
+                arguments,
+                activation,
+                *self, // Callee deliberately invalid.
+            );
+        }
+
+        let bound_method = VTable::bind_method(activation, *self, full_method);
+
+        // TODO: Bound methods should be cached on the Method in a
+        // WeakKeyHashMap<Value, FunctionObject>, not on the Object
+        if let Some(object) = self.as_object() {
+            object.install_bound_method(activation.gc(), id, bound_method);
+        }
+
+        bound_method.call(activation, *self, arguments)
+    }
+
+    /// Delete a named property from the value.
+    ///
+    /// Returns false if the property cannot be deleted.
+    ///
+    /// This method will return unexpected results if called on null or undefined!
+    /// The value should be `null_check`ed before calling this method on it!
+    pub fn delete_property(
+        &self,
+        activation: &mut Activation<'_, 'gc>,
+        multiname: &Multiname<'gc>,
+    ) -> Result<bool, Error<'gc>> {
         match self {
-            Value::Object(o) => Some(*o),
-            _ => None,
+            Value::Object(object) => {
+                match object.vtable().get_trait(multiname) {
+                    None => {
+                        if object.instance_class().is_sealed() {
+                            Ok(false)
+                        } else {
+                            object.delete_property_local(activation, multiname)
+                        }
+                    }
+                    _ => {
+                        // Similar to the get_property special case for XML/XMLList.
+                        if (object.as_xml_object().is_some()
+                            || object.as_xml_list_object().is_some())
+                            && multiname.contains_public_namespace()
+                        {
+                            return object.delete_property_local(activation, multiname);
+                        }
+
+                        Ok(false)
+                    }
+                }
+            }
+            _ => {
+                let instance_class = self.instance_class(activation);
+
+                Err(error::make_reference_error(
+                    activation,
+                    error::ReferenceErrorCode::InvalidDelete,
+                    multiname,
+                    instance_class,
+                ))
+            }
+        }
+    }
+
+    /// Returns true if the value has one or more traits of a given name.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn has_trait(&self, activation: &mut Activation<'_, 'gc>, name: &Multiname<'gc>) -> bool {
+        self.vtable(activation).has_trait(name)
+    }
+
+    /// Returns true if the value has one or more traits of a given name.
+    ///
+    /// This method will panic if called on null or undefined.
+    pub fn has_own_property(
+        &self,
+        activation: &mut Activation<'_, 'gc>,
+        name: &Multiname<'gc>,
+    ) -> bool {
+        match self {
+            Value::Object(object) => object.has_own_property(name),
+            _ => self.vtable(activation).has_trait(name),
+        }
+    }
+
+    pub fn has_public_property(
+        self,
+        name: impl Into<AvmString<'gc>>,
+        activation: &mut Activation<'_, 'gc>,
+    ) -> bool {
+        let name = Multiname::new(activation.avm2().find_public_namespace(), name);
+
+        if let Some(object) = self.as_object() {
+            if object.has_own_property(&name) {
+                return true;
+            }
+        }
+
+        if let Some(proto) = self.proto(activation) {
+            proto.has_property(&name)
+        } else {
+            false
         }
     }
 
@@ -930,11 +1414,11 @@ impl<'gc> Value<'gc> {
         &self,
         activation: &mut Activation<'_, 'gc>,
         args: &[Value<'gc>],
-    ) -> Result<Object<'gc>, Error<'gc>> {
+    ) -> Result<Value<'gc>, Error<'gc>> {
         match self.as_object() {
             Some(Object::ClassObject(class_object)) => class_object.construct(activation, args),
             Some(Object::FunctionObject(function_object)) => {
-                function_object.construct(activation, args)
+                function_object.construct(activation, args).map(Into::into)
             }
             _ => {
                 let error = if activation.context.swf.version() < 11 {
@@ -1004,19 +1488,7 @@ impl<'gc> Value<'gc> {
 
         let name = class.name().to_qualified_name_err_message(activation.gc());
 
-        let debug_str = match self {
-            Value::Object(obj) if obj.as_primitive().is_none() => {
-                // Flash prints the class name (ignoring the toString() impl on the object),
-                // followed by something that looks like an address (it varies between executions).
-                // For now, we just set the "address" to all zeroes, on the off chance that some
-                // application is trying to parse the error message.
-                format!(
-                    "{}@00000000000",
-                    obj.instance_of_class_name(activation.gc())
-                )
-            }
-            _ => self.coerce_to_debug_string(activation)?.to_string(),
-        };
+        let debug_str = self.as_debug_string(activation)?;
 
         Err(Error::AvmError(type_error(
             activation,
@@ -1027,12 +1499,7 @@ impl<'gc> Value<'gc> {
 
     /// Determine if this value is any kind of number.
     pub fn is_number(&self) -> bool {
-        match self {
-            Value::Number(_) => true,
-            Value::Integer(_) => true,
-            Value::Object(o) => o.as_primitive().is_some_and(|p| p.is_number()),
-            _ => false,
-        }
+        matches!(self, Value::Number(_) | Value::Integer(_))
     }
 
     /// Determine if this value is a number representable as a u32 without loss
@@ -1042,7 +1509,6 @@ impl<'gc> Value<'gc> {
         match self {
             Value::Number(n) => *n == (*n as u32 as f64),
             Value::Integer(i) => *i >= 0,
-            Value::Object(o) => o.as_primitive().is_some_and(|p| p.is_u32()),
             _ => false,
         }
     }
@@ -1054,7 +1520,6 @@ impl<'gc> Value<'gc> {
         match self {
             Value::Number(n) => *n == (*n as i32 as f64),
             Value::Integer(_) => true,
-            Value::Object(o) => o.as_primitive().is_some_and(|p| p.is_i32()),
             _ => false,
         }
     }
@@ -1099,9 +1564,29 @@ impl<'gc> Value<'gc> {
         }
     }
 
+    /// Get the vtable associated with this value.
+    ///
+    /// This function will panic if called on null or undefined.
+    #[inline]
+    pub fn vtable(&self, activation: &mut Activation<'_, 'gc>) -> VTable<'gc> {
+        let classes = activation.avm2().classes();
+
+        match self {
+            Value::Bool(_) => classes.boolean.instance_vtable(),
+            Value::Number(_) | Value::Integer(_) => classes.number.instance_vtable(),
+            Value::String(_) => classes.string.instance_vtable(),
+            Value::Object(obj) => obj.vtable(),
+
+            Value::Undefined | Value::Null => {
+                unreachable!("Should not have Undefined or Null in `vtable`")
+            }
+        }
+    }
+
     /// Get the class that this Value is of.
     ///
     /// This function will panic if called on null or undefined.
+    #[inline]
     pub fn instance_class(&self, activation: &mut Activation<'_, 'gc>) -> Class<'gc> {
         let class_defs = activation.avm2().class_defs();
 
@@ -1120,6 +1605,7 @@ impl<'gc> Value<'gc> {
     /// Get the prototype object corresponding to this Value's type.
     ///
     /// This function will panic if called on null or undefined.
+    #[inline]
     pub fn proto(&self, activation: &mut Activation<'_, 'gc>) -> Option<Object<'gc>> {
         let classes = activation.avm2().classes();
 
diff --git a/core/src/avm2/vtable.rs b/core/src/avm2/vtable.rs
index 3c10c481f8d28..a86902783c409 100644
--- a/core/src/avm2/vtable.rs
+++ b/core/src/avm2/vtable.rs
@@ -1,7 +1,7 @@
 use crate::avm2::activation::Activation;
 use crate::avm2::metadata::Metadata;
 use crate::avm2::method::Method;
-use crate::avm2::object::{ClassObject, FunctionObject, Object};
+use crate::avm2::object::{ClassObject, FunctionObject};
 use crate::avm2::property::{Property, PropertyClass};
 use crate::avm2::property_map::PropertyMap;
 use crate::avm2::scope::ScopeChain;
@@ -492,7 +492,7 @@ impl<'gc> VTable<'gc> {
 
     /// Retrieve a bound instance method suitable for use as a value.
     ///
-    /// This returns the bound method object itself, as well as it's dispatch
+    /// This returns the bound method object itself, as well as its dispatch
     /// ID. You will need the additional properties in order to install the
     /// method into your object.
     ///
@@ -500,10 +500,13 @@ impl<'gc> VTable<'gc> {
     /// the result. Otherwise, code that relies on bound methods having stable
     /// object identitities (e.g. `EventDispatcher.removeEventListener`) will
     /// fail.
+    ///
+    /// It is the caller's responsibility to ensure that the `receiver` passed
+    /// to this method is not Value::Null or Value::Undefined.
     pub fn make_bound_method(
         self,
         activation: &mut Activation<'_, 'gc>,
-        receiver: Object<'gc>,
+        receiver: Value<'gc>,
         disp_id: u32,
     ) -> Option<FunctionObject<'gc>> {
         self.get_full_method(disp_id)
@@ -511,9 +514,12 @@ impl<'gc> VTable<'gc> {
     }
 
     /// Bind an instance method to a receiver, allowing it to be used as a value. See `VTable::make_bound_method`
+    ///
+    /// It is the caller's responsibility to ensure that the `receiver` passed
+    /// to this method is not Value::Null or Value::Undefined.
     pub fn bind_method(
         activation: &mut Activation<'_, 'gc>,
-        receiver: Object<'gc>,
+        receiver: Value<'gc>,
         method: ClassBoundMethod<'gc>,
     ) -> FunctionObject<'gc> {
         let ClassBoundMethod {
diff --git a/core/src/debug_ui/avm2.rs b/core/src/debug_ui/avm2.rs
index 69538bb4b295c..8744fab7bb0b4 100644
--- a/core/src/debug_ui/avm2.rs
+++ b/core/src/debug_ui/avm2.rs
@@ -359,6 +359,7 @@ impl Avm2ObjectWindow {
                 });
             });
         };
+
         match prop {
             Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
                 body.row(18.0, |mut row| {
@@ -379,7 +380,7 @@ impl Avm2ObjectWindow {
                     label_col(&mut row);
                     row.col(|ui| {
                         if self.call_getters {
-                            let value = object.call_method(get, &[], activation);
+                            let value = Value::from(object).call_method(get, &[], activation);
                             ValueResultWidget::new(activation, value).show(ui, messages);
                         } else {
                             let value = self.getter_values.get_mut(&key);
@@ -387,7 +388,8 @@ impl Avm2ObjectWindow {
                                 // Empty entry means we want to refresh it,
                                 // so let's do that now
                                 let widget = value.get_or_insert_with(|| {
-                                    let value = object.call_method(get, &[], activation);
+                                    let value =
+                                        Value::from(object).call_method(get, &[], activation);
                                     ValueResultWidget::new(activation, value)
                                 });
                                 widget.show(ui, messages);
diff --git a/core/src/display_object.rs b/core/src/display_object.rs
index c6f5843a04860..b8d2997903f3d 100644
--- a/core/src/display_object.rs
+++ b/core/src/display_object.rs
@@ -2039,8 +2039,8 @@ pub trait TDisplayObject<'gc>:
         //TODO: Don't report missing property errors.
         //TODO: Don't attempt to set properties if object was placed without a name.
         if self.has_explicit_name() {
-            if let Some(Avm2Value::Object(p)) = self.parent().map(|p| p.object2()) {
-                if let Avm2Value::Object(c) = self.object2() {
+            if let Some(parent @ Avm2Value::Object(_)) = self.parent().map(|p| p.object2()) {
+                if let Avm2Value::Object(child) = self.object2() {
                     let domain = context
                         .library
                         .library_for_movie(self.movie())
@@ -2049,7 +2049,7 @@ pub trait TDisplayObject<'gc>:
                     let mut activation = Avm2Activation::from_domain(context, domain);
                     let name =
                         Avm2Multiname::new(activation.avm2().find_public_namespace(), self.name());
-                    if let Err(e) = p.init_property(&name, c.into(), &mut activation) {
+                    if let Err(e) = parent.init_property(&name, child.into(), &mut activation) {
                         tracing::error!(
                             "Got error when setting AVM2 child named \"{}\": {}",
                             &self.name(),
@@ -2743,6 +2743,8 @@ impl SoundTransform {
             .classes()
             .soundtransform
             .construct(activation, &[])?
+            .as_object()
+            .unwrap()
             .as_sound_transform()
             .unwrap();
 
diff --git a/core/src/display_object/container.rs b/core/src/display_object/container.rs
index 9bad656dba85f..d4929dcbb791c 100644
--- a/core/src/display_object/container.rs
+++ b/core/src/display_object/container.rs
@@ -3,7 +3,7 @@
 use crate::avm1::{Activation, ActivationIdentifier, TObject};
 use crate::avm2::{
     Activation as Avm2Activation, Avm2, EventObject as Avm2EventObject, Multiname as Avm2Multiname,
-    TObject as _, Value as Avm2Value,
+    Value as Avm2Value,
 };
 use crate::context::{RenderContext, UpdateContext};
 use crate::display_object::avm1_button::Avm1Button;
@@ -719,6 +719,8 @@ impl<'gc> ChildContainer<'gc> {
                 );
                 if child.has_explicit_name() {
                     if let Avm2Value::Object(parent_obj) = parent.object2() {
+                        let parent_obj = Avm2Value::from(parent_obj);
+
                         let mut activation = Avm2Activation::from_nothing(context);
                         let name = Avm2Multiname::new(
                             activation.avm2().find_public_namespace(),
diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs
index 0e140941b478a..67ac8cab92ea8 100644
--- a/core/src/display_object/edit_text.rs
+++ b/core/src/display_object/edit_text.rs
@@ -8,11 +8,11 @@ use crate::avm1::{
     Value as Avm1Value,
 };
 use crate::avm2::object::StyleSheetObject;
-use crate::avm2::Avm2;
-use crate::avm2::{
-    Activation as Avm2Activation, ClassObject as Avm2ClassObject, EventObject as Avm2EventObject,
-    Object as Avm2Object, StageObject as Avm2StageObject, TObject as _,
+use crate::avm2::object::{
+    ClassObject as Avm2ClassObject, EventObject as Avm2EventObject, Object as Avm2Object,
+    StageObject as Avm2StageObject,
 };
+use crate::avm2::{Activation as Avm2Activation, Avm2};
 use crate::backend::ui::MouseCursor;
 use crate::context::{RenderContext, UpdateContext};
 use crate::display_object::interactive::{
@@ -1935,7 +1935,7 @@ impl<'gc> EditText<'gc> {
             );
             Avm2::dispatch_event(activation.context, text_evt, target);
 
-            if text_evt.as_event().unwrap().is_cancelled() {
+            if text_evt.event().is_cancelled() {
                 return;
             }
         }
diff --git a/core/src/display_object/stage.rs b/core/src/display_object/stage.rs
index 209b48ad9c53f..b109974c9c883 100644
--- a/core/src/display_object/stage.rs
+++ b/core/src/display_object/stage.rs
@@ -717,20 +717,20 @@ impl<'gc> Stage<'gc> {
                 );
             }
         } else if let Avm2Value::Object(stage) = self.object2() {
-            let full_screen_event_cls = context.avm2.classes().fullscreenevent;
             let mut activation = Avm2Activation::from_nothing(context);
-            let full_screen_event = full_screen_event_cls
-                .construct(
-                    &mut activation,
-                    &[
-                        "fullScreen".into(),
-                        false.into(),
-                        false.into(),
-                        self.is_fullscreen().into(),
-                        true.into(),
-                    ],
-                )
-                .unwrap(); // we don't expect to break here
+
+            let full_screen_event_cls = activation.avm2().classes().fullscreenevent;
+            let full_screen_event = Avm2EventObject::from_class_and_args(
+                &mut activation,
+                full_screen_event_cls,
+                &[
+                    "fullScreen".into(),
+                    false.into(),
+                    false.into(),
+                    self.is_fullscreen().into(),
+                    true.into(),
+                ],
+            );
 
             Avm2::dispatch_event(context, full_screen_event, stage);
         }
@@ -794,6 +794,8 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> {
                             .stage3d
                             .construct(&mut activation, &[])
                             .expect("Failed to construct Stage3D")
+                            .as_object()
+                            .expect("Stage3D is an Object")
                     })
                     .collect();
                 let mut write = self.0.write(activation.gc());
diff --git a/core/src/focus_tracker.rs b/core/src/focus_tracker.rs
index a3cbfa8b298f8..234764fde8cb0 100644
--- a/core/src/focus_tracker.rs
+++ b/core/src/focus_tracker.rs
@@ -1,6 +1,6 @@
 use crate::avm1::Avm1;
 use crate::avm1::Value;
-use crate::avm2::{Activation, Avm2, EventObject, TObject};
+use crate::avm2::{Activation, Avm2, EventObject};
 use crate::context::{RenderContext, UpdateContext};
 pub use crate::display_object::{
     DisplayObject, TDisplayObject, TDisplayObjectContainer, TextSelection,
@@ -253,7 +253,7 @@ impl<'gc> FocusTracker<'gc> {
             EventObject::focus_event(&mut activation, event_type, true, related_object, key_code);
         Avm2::dispatch_event(activation.context, event, target);
 
-        let canceled = event.as_event().unwrap().is_cancelled();
+        let canceled = event.event().is_cancelled();
         canceled
     }
 
diff --git a/core/src/loader.rs b/core/src/loader.rs
index bcb67a94380eb..57f7cf19d2b7c 100644
--- a/core/src/loader.rs
+++ b/core/src/loader.rs
@@ -1587,7 +1587,6 @@ impl<'gc> Loader<'gc> {
                                 .urlvariables
                                 .construct(activation, &[string_value.into()])
                                 .ok()
-                                .map(|o| o.into())
                         }
                     } else {
                         if &data_format != b"text" {
@@ -1624,39 +1623,17 @@ impl<'gc> Loader<'gc> {
 
                         // FIXME - we should fire "progress" events as we receive data, not
                         // just at the end
-                        let progress_evt = activation
-                            .avm2()
-                            .classes()
-                            .progressevent
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "progress".into(),
-                                    false.into(),
-                                    false.into(),
-                                    total_len.into(),
-                                    total_len.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                        let progress_evt = Avm2EventObject::progress_event(
+                            &mut activation,
+                            "progress",
+                            total_len,
+                            total_len,
+                        );
 
                         Avm2::dispatch_event(activation.context, progress_evt, target);
 
-                        let http_status_evt = activation
-                            .avm2()
-                            .classes()
-                            .httpstatusevent
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "httpStatus".into(),
-                                    false.into(),
-                                    false.into(),
-                                    status.into(),
-                                    redirected.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                        let http_status_evt =
+                            Avm2EventObject::http_status_event(&mut activation, status, redirected);
 
                         Avm2::dispatch_event(activation.context, http_status_evt, target);
 
@@ -1683,39 +1660,20 @@ impl<'gc> Loader<'gc> {
                             } else {
                                 (0, false)
                             };
-                        let http_status_evt = activation
-                            .avm2()
-                            .classes()
-                            .httpstatusevent
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "httpStatus".into(),
-                                    false.into(),
-                                    false.into(),
-                                    status_code.into(),
-                                    redirected.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                        let http_status_evt = Avm2EventObject::http_status_event(
+                            &mut activation,
+                            status_code,
+                            redirected,
+                        );
 
                         Avm2::dispatch_event(activation.context, http_status_evt, target);
 
                         // FIXME - Match the exact error message generated by Flash
-
-                        let io_error_evt_cls = activation.avm2().classes().ioerrorevent;
-                        let io_error_evt = io_error_evt_cls
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "ioError".into(),
-                                    false.into(),
-                                    false.into(),
-                                    "Error #2032: Stream Error".into(),
-                                    2032.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                        let io_error_evt = Avm2EventObject::io_error_event(
+                            &mut activation,
+                            "Error #2032: Stream Error".into(),
+                            2032,
+                        );
 
                         Avm2::dispatch_event(uc, io_error_evt, target);
                     }
@@ -1844,21 +1802,12 @@ impl<'gc> Loader<'gc> {
 
                         // FIXME - As in load_url_loader, we should fire "progress" events as we receive data,
                         // not just at the end
-                        let progress_evt = activation
-                            .avm2()
-                            .classes()
-                            .progressevent
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "progress".into(),
-                                    false.into(),
-                                    false.into(),
-                                    total_len.into(),
-                                    total_len.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                        let progress_evt = Avm2EventObject::progress_event(
+                            &mut activation,
+                            "progress",
+                            total_len,
+                            total_len,
+                        );
 
                         Avm2::dispatch_event(activation.context, progress_evt, sound_object);
 
@@ -1872,21 +1821,14 @@ impl<'gc> Loader<'gc> {
                         Avm2::dispatch_event(activation.context, complete_evt, sound_object);
                     }
                     Err(_err) => {
-                        // FIXME: Match the exact error message generated by Flash.
                         let mut activation = Avm2Activation::from_nothing(uc);
-                        let io_error_evt_cls = activation.avm2().classes().ioerrorevent;
-                        let io_error_evt = io_error_evt_cls
-                            .construct(
-                                &mut activation,
-                                &[
-                                    "ioError".into(),
-                                    false.into(),
-                                    false.into(),
-                                    "Error #2032: Stream Error".into(),
-                                    2032.into(),
-                                ],
-                            )
-                            .map_err(|e| Error::Avm2Error(e.to_string()))?;
+
+                        // FIXME: Match the exact error message generated by Flash.
+                        let io_error_evt = Avm2EventObject::io_error_event(
+                            &mut activation,
+                            "Error #2032: Stream Error".into(),
+                            2032,
+                        );
 
                         Avm2::dispatch_event(uc, io_error_evt, sound_object);
                     }
@@ -2227,7 +2169,10 @@ impl<'gc> Loader<'gc> {
                     .classes()
                     .bitmap
                     .construct(&mut activation, &[bitmapdata_avm2.into()])
+                    .unwrap()
+                    .as_object()
                     .unwrap();
+
                 let bitmap_dobj = bitmap_avm2.as_display_object().unwrap();
 
                 if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
@@ -2386,21 +2331,12 @@ impl<'gc> Loader<'gc> {
             MovieLoaderVMData::Avm2 { loader_info, .. } => {
                 let mut activation = Avm2Activation::from_nothing(uc);
 
-                let progress_evt = activation
-                    .avm2()
-                    .classes()
-                    .progressevent
-                    .construct(
-                        &mut activation,
-                        &[
-                            "progress".into(),
-                            false.into(),
-                            false.into(),
-                            cur_len.into(),
-                            total_len.into(),
-                        ],
-                    )
-                    .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                let progress_evt = Avm2EventObject::progress_event(
+                    &mut activation,
+                    "progress",
+                    cur_len,
+                    total_len,
+                );
 
                 Avm2::dispatch_event(uc, progress_evt, loader_info.into());
             }
@@ -2610,39 +2546,13 @@ impl<'gc> Loader<'gc> {
             MovieLoaderVMData::Avm2 { loader_info, .. } => {
                 let mut activation = Avm2Activation::from_nothing(uc);
 
-                let http_status_evt = activation
-                    .avm2()
-                    .classes()
-                    .httpstatusevent
-                    .construct(
-                        &mut activation,
-                        &[
-                            "httpStatus".into(),
-                            false.into(),
-                            false.into(),
-                            status.into(),
-                            redirected.into(),
-                        ],
-                    )
-                    .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                let http_status_evt =
+                    Avm2EventObject::http_status_event(&mut activation, status, redirected);
 
                 Avm2::dispatch_event(activation.context, http_status_evt, loader_info.into());
 
-                // FIXME - Match the exact error message generated by Flash
-
-                let io_error_evt_cls = activation.avm2().classes().ioerrorevent;
-                let io_error_evt = io_error_evt_cls
-                    .construct(
-                        &mut activation,
-                        &[
-                            "ioError".into(),
-                            false.into(),
-                            false.into(),
-                            msg.into(),
-                            0.into(),
-                        ],
-                    )
-                    .map_err(|e| Error::Avm2Error(e.to_string()))?;
+                // FIXME - Match the exact error message and code generated by Flash
+                let io_error_evt = Avm2EventObject::io_error_event(&mut activation, msg, 0);
 
                 Avm2::dispatch_event(uc, io_error_evt, loader_info.into());
             }
@@ -2932,14 +2842,11 @@ impl<'gc> Loader<'gc> {
                                 target_object.into(),
                             );
 
-                            let size = data.len() as u64;
                             let progress_evt = Avm2EventObject::progress_event(
                                 &mut activation,
                                 "progress",
-                                size,
-                                size,
-                                false,
-                                false,
+                                data.len(),
+                                data.len(),
                             );
                             Avm2::dispatch_event(
                                 activation.context,
diff --git a/core/src/net_connection.rs b/core/src/net_connection.rs
index da0d07937a05c..140512d54dae4 100644
--- a/core/src/net_connection.rs
+++ b/core/src/net_connection.rs
@@ -144,7 +144,6 @@ impl<'gc> NetConnections<'gc> {
                 let mut activation = Avm2Activation::from_nothing(context);
                 let event = Avm2EventObject::net_status_event(
                     &mut activation,
-                    "netStatus",
                     vec![
                         ("code", "NetConnection.Connect.Success"),
                         ("level", "status"),
@@ -197,7 +196,6 @@ impl<'gc> NetConnections<'gc> {
                 let mut activation = Avm2Activation::from_nothing(context);
                 let event = Avm2EventObject::net_status_event(
                     &mut activation,
-                    "netStatus",
                     vec![
                         ("code", "NetConnection.Connect.Closed"),
                         ("level", "status"),
@@ -211,7 +209,6 @@ impl<'gc> NetConnections<'gc> {
                     // [NA] I have no idea why, but a NetConnection receives a second and nonsensical event on close
                     let event = Avm2EventObject::net_status_event(
                         &mut activation,
-                        "netStatus",
                         vec![
                             ("code", ""),
                             ("description", ""),
@@ -546,7 +543,6 @@ impl FlashRemoting {
                                     let url = AvmString::new_utf8(activation.gc(), response.url);
                                     let event = Avm2EventObject::net_status_event(
                                         &mut activation,
-                                        "netStatus",
                                         vec![
                                             ("code", "NetConnection.Call.Failed".into()),
                                             ("level", "error".into()),
diff --git a/core/src/player.rs b/core/src/player.rs
index 3c750a216950f..e32bf7373c7cd 100644
--- a/core/src/player.rs
+++ b/core/src/player.rs
@@ -5,7 +5,8 @@ use crate::avm1::SystemProperties;
 use crate::avm1::VariableDumper;
 use crate::avm1::{Activation, ActivationIdentifier};
 use crate::avm1::{TObject, Value};
-use crate::avm2::{Activation as Avm2Activation, Avm2, CallStack, Object as Avm2Object};
+use crate::avm2::object::{EventObject as Avm2EventObject, Object as Avm2Object};
+use crate::avm2::{Activation as Avm2Activation, Avm2, CallStack};
 use crate::backend::ui::FontDefinition;
 use crate::backend::{
     audio::{AudioBackend, AudioManager},
@@ -655,21 +656,18 @@ impl Player {
 
                 if let Some(menu_object) = menu_object {
                     // TODO: contextMenuOwner and mouseTarget might not be the same
-                    let menu_evt = activation
-                        .avm2()
-                        .classes()
-                        .contextmenuevent
-                        .construct(
-                            &mut activation,
-                            &[
-                                "menuSelect".into(),
-                                false.into(),
-                                false.into(),
-                                hit_obj.into(),
-                                hit_obj.into(),
-                            ],
-                        )
-                        .expect("Context menu event should be constructed!");
+                    let context_menu_event_cls = activation.avm2().classes().contextmenuevent;
+                    let menu_evt = Avm2EventObject::from_class_and_args(
+                        &mut activation,
+                        context_menu_event_cls,
+                        &[
+                            "menuSelect".into(),
+                            false.into(),
+                            false.into(),
+                            hit_obj.into(),
+                            hit_obj.into(),
+                        ],
+                    );
 
                     Avm2::dispatch_event(activation.context, menu_evt, menu_object);
                 }
@@ -719,21 +717,19 @@ impl Player {
 
                             if menu_obj.is_some() {
                                 // TODO: contextMenuOwner and mouseTarget might not be the same (see above comment)
-                                let menu_evt = activation
-                                    .avm2()
-                                    .classes()
-                                    .contextmenuevent
-                                    .construct(
-                                        &mut activation,
-                                        &[
-                                            "menuItemSelect".into(),
-                                            false.into(),
-                                            false.into(),
-                                            display_obj.object2(),
-                                            display_obj.object2(),
-                                        ],
-                                    )
-                                    .expect("Context menu event should be constructed!");
+                                let context_menu_event_cls =
+                                    activation.avm2().classes().contextmenuevent;
+                                let menu_evt = Avm2EventObject::from_class_and_args(
+                                    &mut activation,
+                                    context_menu_event_cls,
+                                    &[
+                                        "menuItemSelect".into(),
+                                        false.into(),
+                                        false.into(),
+                                        display_obj.object2(),
+                                        display_obj.object2(),
+                                    ],
+                                );
 
                                 Avm2::dispatch_event(context, menu_evt, menu_item);
                             }
@@ -1157,23 +1153,23 @@ impl Player {
                 // TODO: keyLocation should not be a dummy value.
                 // ctrlKey and controlKey can be different from each other on Mac.
                 // commandKey should be supported.
-                let keyboard_event = keyboardevent_class
-                    .construct(
-                        &mut activation,
-                        &[
-                            event_name_val,                          /* type */
-                            true.into(),                             /* bubbles */
-                            false.into(),                            /* cancelable */
-                            key_char.map_or(0, |c| c as u32).into(), /* charCode */
-                            key_code.value().into(),                 /* keyCode */
-                            0.into(),                                /* keyLocation */
-                            ctrl_key.into(),                         /* ctrlKey */
-                            alt_key.into(),                          /* altKey */
-                            shift_key.into(),                        /* shiftKey */
-                            ctrl_key.into(),                         /* controlKey */
-                        ],
-                    )
-                    .expect("Failed to construct KeyboardEvent");
+                let keyboard_event = Avm2EventObject::from_class_and_args(
+                    &mut activation,
+                    keyboardevent_class,
+                    &[
+                        event_name_val,                          /* type */
+                        true.into(),                             /* bubbles */
+                        false.into(),                            /* cancelable */
+                        key_char.map_or(0, |c| c as u32).into(), /* charCode */
+                        key_code.value().into(),                 /* keyCode */
+                        0.into(),                                /* keyLocation */
+                        ctrl_key.into(),                         /* ctrlKey */
+                        alt_key.into(),                          /* altKey */
+                        shift_key.into(),                        /* shiftKey */
+                        ctrl_key.into(),                         /* controlKey */
+                    ],
+                );
+
                 let target_object = activation
                     .context
                     .focus_tracker
@@ -1184,7 +1180,7 @@ impl Player {
                 if target_object.movie().is_action_script_3() {
                     let target = target_object
                         .object2()
-                        .coerce_to_object(&mut activation)
+                        .as_object()
                         .expect("DisplayObject is not an object!");
 
                     Avm2::dispatch_event(activation.context, keyboard_event, target);
@@ -1880,26 +1876,14 @@ impl Player {
                 if let Some(loader_info) = root.loader_info().filter(|_| !was_root_movie_loaded) {
                     let mut activation = Avm2Activation::from_nothing(context);
 
-                    let progress_evt = activation.avm2().classes().progressevent.construct(
+                    let progress_evt = Avm2EventObject::progress_event(
                         &mut activation,
-                        &[
-                            "progress".into(),
-                            false.into(),
-                            false.into(),
-                            root.compressed_loaded_bytes().into(),
-                            root.compressed_total_bytes().into(),
-                        ],
+                        "progress",
+                        root.compressed_loaded_bytes() as usize,
+                        root.compressed_total_bytes() as usize,
                     );
 
-                    match progress_evt {
-                        Err(e) => tracing::error!(
-                            "Encountered AVM2 error when constructing `progress` event: {}",
-                            e,
-                        ),
-                        Ok(progress_evt) => {
-                            Avm2::dispatch_event(context, progress_evt, loader_info);
-                        }
-                    }
+                    Avm2::dispatch_event(context, progress_evt, loader_info);
                 }
             }
 
diff --git a/core/src/socket.rs b/core/src/socket.rs
index 083d5ab105a29..251c3eede4c29 100644
--- a/core/src/socket.rs
+++ b/core/src/socket.rs
@@ -1,13 +1,13 @@
-use crate::{
-    avm1::{
-        globals::xml_socket::XmlSocket, Activation as Avm1Activation, ActivationIdentifier,
-        ExecutionReason, Object as Avm1Object, TObject as Avm1TObject,
-    },
-    avm2::{object::SocketObject, Activation as Avm2Activation, Avm2, EventObject},
-    backend::navigator::NavigatorBackend,
-    context::UpdateContext,
-    string::AvmString,
+use crate::avm1::{
+    globals::xml_socket::XmlSocket, Activation as Avm1Activation, ActivationIdentifier,
+    ExecutionReason, Object as Avm1Object, TObject as Avm1TObject,
 };
+use crate::avm2::object::{EventObject, SocketObject};
+use crate::avm2::{Activation as Avm2Activation, Avm2};
+use crate::backend::navigator::NavigatorBackend;
+use crate::context::UpdateContext;
+use crate::string::AvmString;
+
 use async_channel::{unbounded, Receiver, Sender as AsyncSender, Sender};
 use gc_arena::Collect;
 use slotmap::{new_key_type, SlotMap};
@@ -258,21 +258,11 @@ impl<'gc> Sockets<'gc> {
                         SocketKind::Avm2(target) => {
                             let mut activation = Avm2Activation::from_nothing(context);
 
-                            let io_error_evt = activation
-                                .avm2()
-                                .classes()
-                                .ioerrorevent
-                                .construct(
-                                    &mut activation,
-                                    &[
-                                        "ioError".into(),
-                                        false.into(),
-                                        false.into(),
-                                        "Error #2031: Socket Error.".into(),
-                                        2031.into(),
-                                    ],
-                                )
-                                .expect("IOErrorEvent should be constructed");
+                            let io_error_evt = EventObject::io_error_event(
+                                &mut activation,
+                                "Error #2031: Socket Error.".into(),
+                                2031,
+                            );
 
                             Avm2::dispatch_event(activation.context, io_error_evt, target.into());
                         }
@@ -306,22 +296,12 @@ impl<'gc> Sockets<'gc> {
                             let bytes_loaded = data.len();
                             target.read_buffer().extend(data);
 
-                            let progress_evt = activation
-                                .avm2()
-                                .classes()
-                                .progressevent
-                                .construct(
-                                    &mut activation,
-                                    &[
-                                        "socketData".into(),
-                                        false.into(),
-                                        false.into(),
-                                        bytes_loaded.into(),
-                                        //NOTE: bytesTotal is not used by socketData event.
-                                        0.into(),
-                                    ],
-                                )
-                                .expect("ProgressEvent should be constructed");
+                            let progress_evt = EventObject::progress_event(
+                                &mut activation,
+                                "socketData",
+                                bytes_loaded,
+                                0, // NOTE: bytesTotal is not used by socketData event.
+                            );
 
                             Avm2::dispatch_event(activation.context, progress_evt, target.into());
                         }
diff --git a/core/src/streams.rs b/core/src/streams.rs
index 514808da2f4ec..1c9ad4b154295 100644
--- a/core/src/streams.rs
+++ b/core/src/streams.rs
@@ -5,10 +5,9 @@ use crate::avm1::{
     ExecutionReason as Avm1ExecutionReason, FlvValueAvm1Ext, ScriptObject as Avm1ScriptObject,
     TObject as Avm1TObject, Value as Avm1Value,
 };
-use crate::avm2::object::TObject as Avm2TObject;
 use crate::avm2::{
     Activation as Avm2Activation, Avm2, Error as Avm2Error, EventObject as Avm2EventObject,
-    FlvValueAvm2Ext, Object as Avm2Object,
+    FlvValueAvm2Ext, Object as Avm2Object, Value as Avm2Value,
 };
 use crate::backend::audio::{
     DecodeError, SoundInstanceHandle, SoundStreamInfo, SoundStreamWrapping,
@@ -1316,8 +1315,7 @@ impl<'gc> NetStream<'gc> {
             Some(AvmObject::Avm2(object)) => {
                 let domain = context.avm2.stage_domain();
                 let mut activation = Avm2Activation::from_domain(context, domain);
-                let net_status_event =
-                    Avm2EventObject::net_status_event(&mut activation, "netStatus", values);
+                let net_status_event = Avm2EventObject::net_status_event(&mut activation, values);
                 Avm2::dispatch_event(activation.context, net_status_event, object);
             }
             None => {}
@@ -1365,7 +1363,7 @@ impl<'gc> NetStream<'gc> {
 
                 let data_object = variable_data.to_avm2_value(&mut activation);
 
-                client_object.call_public_property(
+                Avm2Value::from(client_object).call_public_property(
                     AvmString::new_utf8_bytes(activation.gc(), variable_name),
                     &[data_object],
                     &mut activation,


Test patch:
diff --git a/tests/tests/swfs/avm2/verify_abnormal_loop/output.txt b/tests/tests/swfs/avm2/verify_abnormal_loop/output.txt
index c46babdf92773..da421e24ed767 100644
--- a/tests/tests/swfs/avm2/verify_abnormal_loop/output.txt
+++ b/tests/tests/swfs/avm2/verify_abnormal_loop/output.txt
@@ -1,1 +1,1 @@
-ReferenceError: Error #1069: Property field not found on int and there is no default value.
+ReferenceError: Error #1069: Property field not found on Number and there is no default value.
diff --git a/tests/tests/swfs/from_avmplus/as3/Vector/every/test.toml b/tests/tests/swfs/from_avmplus/as3/Vector/every/test.toml
index 661aaf82f605e..cf6123969a1d6 100644
--- a/tests/tests/swfs/from_avmplus/as3/Vector/every/test.toml
+++ b/tests/tests/swfs/from_avmplus/as3/Vector/every/test.toml
@@ -1,2 +1,1 @@
 num_ticks = 1
-known_failure = true # https://github.com/ruffle-rs/ruffle/issues/12321
diff --git a/tests/tests/swfs/from_avmplus/ecma3/TypeConversion/e9_9_1_rt/test.toml b/tests/tests/swfs/from_avmplus/ecma3/TypeConversion/e9_9_1_rt/test.toml
index 29f3cef790229..cf6123969a1d6 100644
--- a/tests/tests/swfs/from_avmplus/ecma3/TypeConversion/e9_9_1_rt/test.toml
+++ b/tests/tests/swfs/from_avmplus/ecma3/TypeConversion/e9_9_1_rt/test.toml
@@ -1,2 +1,1 @@
 num_ticks = 1
-known_failure = true