From 3b2cf3a87884806d3b36fa408b7a1a7ec59cee18 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 4 Apr 2026 15:51:16 -0700 Subject: [PATCH 1/2] Dispatch frame script (channel 0) behaviors correctly Channel 0 behavior instances now receive beginSprite via targeted dispatch instead of the frame/movie script path. Also dispatch enterFrame/exitFrame to channel 0 behaviors in the all-behaviors loop, matching ScummVM's script channel handling. Co-Authored-By: Claude Opus 4.6 (1M context) --- vm-rust/src/player/events.rs | 45 +++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/vm-rust/src/player/events.rs b/vm-rust/src/player/events.rs index f19147298..35967de25 100644 --- a/vm-rust/src/player/events.rs +++ b/vm-rust/src/player/events.rs @@ -593,12 +593,20 @@ pub async fn player_dispatch_event_beginsprite( return Ok(Vec::new()); } - if frame_instances.len() > 0 { - let _ = player_invoke_frame_and_movie_scripts( - handler_name, - args, - ) - .await; + // Dispatch beginSprite to channel 0 (frame script) behavior instances directly + for (_channel_number, behavior) in frame_instances { + let receivers = vec![behavior.clone()]; + if let Err(err) = player_invoke_targeted_event(handler_name, args, Some(receivers).as_ref()).await { + if err.code == ScriptErrorCode::Abort { + return Ok(vec![]); + } + web_sys::console::error_1( + &format!("Error in {} for frame script: {}", handler_name, err.message).into() + ); + reserve_player_mut(|player| { + player.on_script_error(&err); + }); + } } // Dispatch to sprite behaviors (number > 0) @@ -862,6 +870,31 @@ pub async fn dispatch_event_to_all_behaviors( player.current_score_context = ScoreRef::Stage; }); } + + // Dispatch to channel 0 (frame script) behaviors + // Per ScummVM, the script channel instance receives frame events (enterFrame/exitFrame) + // but NOT sprite events (beginSprite/endSprite). + for (_channel_number, behaviors) in frame_behaviors { + for behavior in behaviors { + let receivers = vec![behavior.clone()]; + if let Err(err) = player_invoke_event_to_instances(handler_name, args, &receivers).await { + if err.code == ScriptErrorCode::Abort { + reserve_player_mut(|player| { + player.is_dispatching_events = false; + player.current_score_context = ScoreRef::Stage; + }); + return; + } + web_sys::console::error_1( + &format!("Error in {} for frame script: {}", handler_name, err.message).into() + ); + reserve_player_mut(|player| { + player.on_script_error(&err); + }); + } + } + } + // Dispatch event to frame/movie scripts if let Err(err) = player_invoke_frame_and_movie_scripts(handler_name, args).await { if err.code != ScriptErrorCode::Abort { From 56c9bee268be526088e79daa236ba72dd899993d Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 4 Apr 2026 15:52:25 -0700 Subject: [PATCH 2/2] Fix mouseEnter/mouseLeave to only track interactable sprites Use get_interactable_sprite_at instead of get_sprite_at for hover tracking, matching Director behavior where only sprites with mouse handlers participate in hover state. Also clear hovered_sprite and fire mouseLeave when the mouse leaves all interactable sprites. Fixes repeated mouseEnter events when non-scripted sprites overlap scripted ones (e.g. Bionicle Atticmedia character selection flyouts). Co-Authored-By: Claude Opus 4.6 (1M context) --- vm-rust/src/player/commands.rs | 57 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/vm-rust/src/player/commands.rs b/vm-rust/src/player/commands.rs index 7c1c6f26f..f2ea3fc72 100644 --- a/vm-rust/src/player/commands.rs +++ b/vm-rust/src/player/commands.rs @@ -27,7 +27,7 @@ use super::{ keyboard_events::{player_key_down, player_key_up}, player_alloc_datum, player_call_script_handler, player_dispatch_global_event, player_is_playing, reserve_player_mut, reserve_player_ref, - score::{concrete_sprite_hit_test, get_concrete_sprite_rect, get_sprite_at}, + score::{concrete_sprite_hit_test, get_concrete_sprite_rect, get_interactable_sprite_at, get_sprite_at}, PlayerVMExecutionItem, ScriptError, ScriptReceiver, PLAYER_TX, }; @@ -692,33 +692,44 @@ pub async fn run_player_command(command: PlayerVMCommand) -> Result { + let prev = hovered_sprite.unwrap_or(-1); + if prev != sprite_num as i16 { + if prev != -1 { + player_dispatch_event_to_sprite( + &"mouseLeave".to_string(), + &vec![], + prev as u16, + ); + } player_dispatch_event_to_sprite( - &"mouseLeave".to_string(), + &"mouseEnter".to_string(), &vec![], - hovered_sprite as u16, - ) + sprite_num as u16, + ); + } else { + player_dispatch_event_to_sprite( + &"mouseWithin".to_string(), + &vec![], + sprite_num as u16, + ); + } + } + None => { + if let Some(prev) = hovered_sprite { + if prev != -1 { + player_dispatch_event_to_sprite( + &"mouseLeave".to_string(), + &vec![], + prev as u16, + ); + } } - player_dispatch_event_to_sprite( - &"mouseEnter".to_string(), - &vec![], - sprite_num as u16, - ); - } else { - player_dispatch_event_to_sprite( - &"mouseWithin".to_string(), - &vec![], - sprite_num as u16, - ); } } }