Skip to content

AVM2: Fix a plethora of hitTest issues #8744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/src/avm2/globals/flash/display/displayobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,10 @@ pub fn hit_test_point<'gc>(
.coerce_to_boolean();

if shape_flag {
if !dobj.is_on_stage(&activation.context) {
return Ok(false.into());
}

return Ok(dobj
.hit_test_shape(
&mut activation.context,
Expand Down
37 changes: 32 additions & 5 deletions core/src/display_object/avm2_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,20 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> {
BoundingBox::default()
}

fn bounds_with_transform(&self, matrix: &Matrix) -> BoundingBox {
// Get self bounds
let mut bounds = self.self_bounds().transform(matrix);

// Add the bounds of the child, dictated by current state
let state = self.0.read().state;
if let Some(child) = self.get_state_child(state.into()) {
let child_bounds = child.bounds_with_transform(matrix);
bounds.union(&child_bounds);
}

bounds
}

fn hit_test_shape(
&self,
context: &mut UpdateContext<'_, 'gc, '_>,
Expand All @@ -652,8 +666,14 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> {
if !options.contains(HitTestOptions::SKIP_INVISIBLE) || self.visible() {
let state = self.0.read().state;
if let Some(child) = self.get_state_child(state.into()) {
// hit_area is not actually a child, so transform point into local space before passing it down.
let point = self.global_to_local(point);
//TODO: the if below should probably always be taken, why does the hit area
// sometimes have a parent?
let mut point = point;
if child.parent().is_none() {
// hit_area is not actually a child, so transform point into local space before passing it down.
point = self.global_to_local(point);
}

if child.hit_test_shape(context, point, options) {
return true;
}
Expand Down Expand Up @@ -780,6 +800,7 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> {
ClipEvent::Press => (ButtonState::Down, static_data.over_to_down_sound.as_ref()),
ClipEvent::Release => (ButtonState::Over, static_data.down_to_over_sound.as_ref()),
ClipEvent::ReleaseOutside => (ButtonState::Up, static_data.over_to_up_sound.as_ref()),
ClipEvent::MouseUpInside => (ButtonState::Up, static_data.over_to_up_sound.as_ref()),
ClipEvent::RollOut { .. } => (ButtonState::Up, static_data.over_to_up_sound.as_ref()),
ClipEvent::RollOver { .. } => {
(ButtonState::Over, static_data.up_to_over_sound.as_ref())
Expand Down Expand Up @@ -814,14 +835,20 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> {
.as_interactive()
.and_then(|c| c.mouse_pick(context, point, require_button_mode));
if mouse_pick.is_some() {
return mouse_pick;
// Selecting a child of a button is equivalent to selecting the button itself
return Some((*self).into());
}
}

let hit_area = self.0.read().hit_area;
if let Some(hit_area) = hit_area {
// hit_area is not actually a child, so transform point into local space before passing it down.
let point = self.global_to_local(point);
//TODO: the if below should probably always be taken, why does the hit area
// sometimes have a parent?
let mut point = point;
if hit_area.parent().is_none() {
// hit_area is not actually a child, so transform point into local space before passing it down.
point = self.global_to_local(point);
}
if hit_area.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) {
return Some((*self).into());
}
Expand Down
24 changes: 18 additions & 6 deletions core/src/display_object/movie_clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2491,7 +2491,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
} else {
clip_depth = child.clip_depth();
}
} else if child.depth() > clip_depth
} else if child.depth() >= clip_depth
&& child.hit_test_shape(context, point, options)
{
return true;
Expand Down Expand Up @@ -2744,7 +2744,7 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
point: (Twips, Twips),
require_button_mode: bool,
) -> Option<InteractiveObject<'gc>> {
if self.visible() && self.mouse_enabled() {
if self.visible() {
let this: InteractiveObject<'gc> = (*self).into();

if let Some(masker) = self.masker() {
Expand All @@ -2753,7 +2753,12 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
}
}

if self.world_bounds().contains(point) {
// In AVM2, mouse_enabled should only impact the ability to select the current clip
// but it should still be possible to select any children where child.mouse_enabled() is
// true.
// InteractiveObject.mouseEnabled:
// "Any children of this instance on the display list are not affected."
if self.mouse_enabled() && self.world_bounds().contains(point) {
// This MovieClip operates in "button mode" if it has a mouse handler,
// either via on(..) or via property mc.onRelease, etc.
let is_button_mode = self.is_button_mode(context);
Expand All @@ -2778,6 +2783,11 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
!require_button_mode || matches!(self.object2(), Avm2Value::Object(_));

for child in self.iter_render_list().rev() {
// Clicking static text is ignored
if matches!(child, DisplayObject::Text(_)) {
continue;
}

if child.clip_depth() > 0 {
if result.is_some() && child.clip_depth() >= hit_depth {
if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) {
Expand All @@ -2789,7 +2799,9 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
} else if result.is_none() {
if let Some(child) = child.as_interactive() {
result = child.mouse_pick(context, point, require_button_mode);
} else if check_non_interactive && child.hit_test_shape(context, point, options)
} else if check_non_interactive
&& self.mouse_enabled()
&& child.hit_test_shape(context, point, options)
{
result = Some(this);
}
Expand All @@ -2804,8 +2816,8 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
return result;
}

// Check drawing.
if check_non_interactive {
// Check drawing, because this selects the current clip, it must have mouse enabled
if self.mouse_enabled() && check_non_interactive {
let local_matrix = self.global_to_local_matrix();
let point = local_matrix * point;
if self.0.read().drawing.hit_test(point, &local_matrix) {
Expand Down
1 change: 1 addition & 0 deletions tests/tests/regression_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ swf_tests! {
(as3_boolean_constr, "avm2/boolean_constr", 1),
(as3_boolean_negation, "avm2/boolean_negation", 1),
(as3_boolean_tostring, "avm2/boolean_tostring", 1),
(as3_button_hittest, "avm2/button_hittest", 1),
(as3_bytearray_readobject_amf0, "avm2/bytearray_readobject_amf0", 1),
(as3_bytearray_readobject_amf3, "avm2/bytearray_readobject_amf3", 1),
(as3_bytearray_writeobject, "avm2/bytearray_writeobject", 1),
Expand Down
2 changes: 2 additions & 0 deletions tests/tests/swfs/avm2/button_hittest/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// MP.hitTestObject(Btn1)
true
Binary file added tests/tests/swfs/avm2/button_hittest/test.fla
Binary file not shown.
Binary file added tests/tests/swfs/avm2/button_hittest/test.swf
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/tests/swfs/avm2/displayobject_hittestpoint/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ false
false
//this.symbol1_1.hitTestPoint(50.0, 50.0, true);
false
//this.symbol1.hitTestPoint(50.0, 50.0, true); (after removing from stage)
false
//this.symbol1.hitTestPoint(50.0, 50.0, false); (after removing from stage)
true
Binary file modified tests/tests/swfs/avm2/displayobject_hittestpoint/test.fla
Binary file not shown.
Binary file modified tests/tests/swfs/avm2/displayobject_hittestpoint/test.swf
Binary file not shown.