From 359a37d54d1821a15bdf943680ba5f4f2702dfe1 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 28 Mar 2026 20:11:36 +0100 Subject: [PATCH 01/16] Evaluate script parameters at start time rather than at load time This fixes Supersonic RC's title screen only working some of the time --- vm-rust/src/director/chunks/score.rs | 33 ++------- vm-rust/src/player/score.rs | 107 +++++++++++++++++++++------ 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/vm-rust/src/director/chunks/score.rs b/vm-rust/src/director/chunks/score.rs index 32c298bf7..e8b05d656 100644 --- a/vm-rust/src/director/chunks/score.rs +++ b/vm-rust/src/director/chunks/score.rs @@ -2,11 +2,8 @@ use binary_reader::{BinaryReader, Endian}; use log::{debug, error, warn}; use crate::io::reader::DirectorExt; - -use crate::player::datum_ref::DatumRef; use crate::player::eval::eval_lingo_expr_static; - #[allow(dead_code)] pub struct ScoreFrameDelta { offset: u32, @@ -932,7 +929,7 @@ pub struct FrameIntervalSecondary { pub cast_lib: u16, pub cast_member: u16, pub initializer_index: u32, - pub parameter: Vec, + pub parameter: Vec, } impl FrameIntervalSecondary { @@ -998,7 +995,8 @@ impl ScoreChunkHeader { pub struct SpriteBehavior { pub cast_lib: u16, pub cast_member: u16, - pub parameter: Vec, + /// Raw Lingo expression strings (e.g. "[#pName: \"bg\"]") evaluated fresh at attachment time. + pub parameter: Vec, } /// Sprite detail info parsed from sprite detail offset (D6+) @@ -1190,16 +1188,7 @@ impl ScoreChunk { if let Ok(proplist_string) = String::from_utf8(entries[initializer_idx as usize].clone()) { let clean = proplist_string.trim_end_matches('\0'); if clean.starts_with('[') { - match eval_lingo_expr_static(clean.to_owned()) { - Ok(proplist) => { - parameter.push(proplist); - } - Err(e) => { - log::warn!( - "Failed to parse sprite detail initializer: {}", e.message - ); - } - } + parameter.push(clean.to_owned()); } } } @@ -1594,17 +1583,9 @@ impl ScoreChunk { .trim_end_matches('\0'); debug!("parsed param string: {}", clean); if clean.starts_with('[') { - // TODO: Replace `eval_lingo` with a parser - match eval_lingo_expr_static(clean.to_owned()) { - Ok(proplist) => { - debug!("eval_lingo_expr_static succeeded"); - secondary.parameter.push(proplist); - debug!("parameter vector now has {} items", secondary.parameter.len()); - } - Err(e) => { - error!("eval_lingo_expr_static ERROR: {}", e.message); - } - } + // Store raw expression; evaluate fresh at attachment + // time to avoid stale DatumRef after a movie rewind. + secondary.parameter.push(clean.to_owned()); } } } diff --git a/vm-rust/src/player/score.rs b/vm-rust/src/player/score.rs index 86d2d8c11..f3ce5a165 100644 --- a/vm-rust/src/player/score.rs +++ b/vm-rust/src/player/score.rs @@ -5,6 +5,7 @@ use log::debug; use wasm_bindgen::JsValue; use std::collections::{HashMap, HashSet}; +use crate::player::eval::eval_lingo_expr_static; use crate::{ console_warn, director::{ @@ -75,7 +76,8 @@ impl SpriteChannel { pub struct ScoreBehaviorReference { pub cast_lib: u16, pub cast_member: u16, - pub parameter: Vec, + /// Raw Lingo expression strings evaluated fresh at attachment time. + pub parameter: Vec, } #[derive(Clone)] @@ -1214,11 +1216,25 @@ impl Score { ); }); - // Parameter setup + // Parameter setup — evaluate raw Lingo expressions fresh so DatumRefs + // are live in the current allocator (avoids stale refs after movie rewind). if !behavior_ref.parameter.is_empty() { + let evaluated_params: Vec = behavior_ref.parameter.iter() + .filter_map(|expr| { + debug!("🔧 Evaluating parameter expression: {:?}", expr); + // TODO: Replace `eval_lingo` with a parser + match eval_lingo_expr_static(expr.clone()) { + Ok(r) => Some(r), + Err(e) => { + debug!(" ⚠️ Failed to evaluate parameter: {}", e.message); + None + } + } + }) + .collect(); reserve_player_mut(|player| { debug!("🔧 Applying {} saved parameters", behavior_ref.parameter.len()); - for param_ref in &behavior_ref.parameter { + for param_ref in &evaluated_params { let param_datum = player.get_datum(param_ref); debug!(" Parameter type: {:?}", param_datum.type_enum()); if let Datum::PropList(props, _) = param_datum { @@ -1405,12 +1421,28 @@ impl Score { ); }); - // Apply behavior parameters from initializer data + // Apply behavior parameters — evaluate raw Lingo expressions fresh so + // DatumRefs are live in the current allocator (avoids stale refs after rewind). if !behavior.parameter.is_empty() { - debug!("🔧 [sprite_details] Applying {} saved parameters for behavior cast {}/{}", + debug!("🔧 [sprite_details] Applying {} saved parameters for behavior cast {}/{}", behavior.parameter.len(), behavior.cast_lib, behavior.cast_member); + let evaluated_params: Vec = behavior.parameter.iter() + .filter_map(|expr| { + debug!(" [sprite_details] Evaluating: {:?}", expr); + match eval_lingo_expr_static(expr.clone()) { + Ok(r) => { + debug!(" [sprite_details] ✅ Evaluated ok"); + Some(r) + } + Err(e) => { + debug!(" [sprite_details] ⚠️ Eval failed: {}", e.message); + None + } + } + }) + .collect(); reserve_player_mut(|player| { - for param_ref in &behavior.parameter { + for param_ref in &evaluated_params { let param_datum = player.get_datum(param_ref); debug!(" [sprite_details] Parameter type: {:?}", param_datum.type_enum()); if let Datum::PropList(props, _) = param_datum { @@ -1676,26 +1708,36 @@ impl Score { ); }); - // Apply behavior parameters + // Apply behavior parameters — evaluate raw Lingo expressions fresh so + // DatumRefs are live in the current allocator (avoids stale refs after rewind). if !behavior.parameter.is_empty() { - debug!("🔧 [delta-data] Applying {} saved parameters for behavior cast {}/{}", + debug!("🔧 [delta-data] Applying {} saved parameters for behavior cast {}/{}", behavior.parameter.len(), behavior.cast_lib, behavior.cast_member); + let evaluated_params: Vec = behavior.parameter.iter() + .filter_map(|expr| { + debug!(" [delta-data] Evaluating: {:?}", expr); + match eval_lingo_expr_static(expr.clone()) { + Ok(r) => { + debug!(" [delta-data] ✅ Evaluated ok"); + Some(r) + } + Err(e) => { + debug!(" [delta-data] ⚠️ Eval failed: {}", e.message); + None + } + } + }) + .collect(); reserve_player_mut(|player| { - for param_ref in &behavior.parameter { + for param_ref in &evaluated_params { let param_datum = player.get_datum(param_ref); - debug!(" [delta-data] Parameter type: {:?}", param_datum.type_enum()); if let Datum::PropList(props, _) = param_datum { let props_to_set: Vec<(String, DatumRef)> = props.iter() .filter_map(|(key_ref, value_ref)| { let key = player.get_datum(key_ref); if let Datum::Symbol(key_name) = key { let value = player.get_datum(value_ref); - debug!(" [delta-data] prop: {} type: {:?}", key_name, value.type_enum()); - match value { - Datum::String(s) => debug!(" [delta-data] value: {:?}", s), - Datum::Int(n) => debug!(" [delta-data] value: {}", n), - _ => debug!(" [delta-data] value: <{:?}>", value.type_enum()), - } + debug!(" [delta-data] prop: {} = {:?}", key_name, value.type_enum()); Some((key_name.clone(), value_ref.clone())) } else { None @@ -1703,7 +1745,6 @@ impl Score { }) .collect(); for (prop_name, value_ref) in props_to_set { - debug!(" [delta-data] Setting property {} on script instance", prop_name); let result = script_set_prop( player, &actual_instance_ref, @@ -1712,9 +1753,9 @@ impl Score { false, ); if let Err(e) = result { - debug!(" [delta-data] ⚠️ Failed to set property {}: {}", prop_name, e.message); + debug!(" [delta-data] ⚠️ Failed to set '{}': {}", prop_name, e.message); } else { - debug!(" [delta-data] ✅ Successfully set property {}", prop_name); + debug!(" [delta-data] ✅ Set '{}'", prop_name); } } } @@ -1821,8 +1862,24 @@ impl Score { // Apply behavior parameters if !behavior.parameter.is_empty() { + let evaluated_params: Vec = behavior.parameter.iter() + .filter_map(|expr| { + debug!(" [delta-data] Evaluating: {:?}", expr); + match eval_lingo_expr_static(expr.clone()) { + Ok(r) => { + debug!(" [delta-data] ✅ Evaluated ok"); + Some(r) + } + Err(e) => { + debug!(" [delta-data] ⚠️ Eval failed: {}", e.message); + None + } + } + }) + .collect(); + reserve_player_mut(|player| { - for param_ref in &behavior.parameter { + for param_ref in &evaluated_params { let param_datum = player.get_datum(param_ref); if let Datum::PropList(props, _) = param_datum { let props_to_set: Vec<(String, DatumRef)> = props.iter() @@ -1942,12 +1999,14 @@ impl Score { ); }); - // Apply behavior parameters + // Apply behavior parameters — evaluate raw Lingo expressions fresh so + // DatumRefs are live in the current allocator (avoids stale refs after rewind). if !behavior_ref.parameter.is_empty() { + let evaluated_params: Vec = behavior_ref.parameter.iter() + .filter_map(|expr| eval_lingo_expr_static(expr.clone()).ok()) + .collect(); reserve_player_mut(|player| { - debug!(" Applying {} parameters", behavior_ref.parameter.len()); - - for param_ref in &behavior_ref.parameter { + for param_ref in &evaluated_params { let param_datum = player.get_datum(param_ref); if let Datum::PropList(props, _) = param_datum { From f40fa5ecad9dcf7b106a6a2f091366582d728a48 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 15:45:10 +0100 Subject: [PATCH 02/16] Make get_cast_mut fallible --- vm-rust/src/player/cast_lib.rs | 2 +- vm-rust/src/player/cast_manager.rs | 22 +++++++++---------- vm-rust/src/player/commands.rs | 4 ++-- .../datum_handlers/cast_member_ref.rs | 2 +- vm-rust/src/player/handlers/types.rs | 2 +- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/vm-rust/src/player/cast_lib.rs b/vm-rust/src/player/cast_lib.rs index af4c47b85..498b5e2ec 100644 --- a/vm-rust/src/player/cast_lib.rs +++ b/vm-rust/src/player/cast_lib.rs @@ -485,7 +485,7 @@ pub async fn player_cast_lib_set_prop( let player = unsafe { PLAYER_OPT.as_mut().unwrap() }; let cast_manager = &mut player.movie.cast_manager; - let cast_lib_obj = cast_manager.get_cast_mut(cast_lib as u32); + let cast_lib_obj = cast_manager.get_cast_mut(cast_lib as u32)?; if prop_name == "fileName" { log_i( diff --git a/vm-rust/src/player/cast_manager.rs b/vm-rust/src/player/cast_manager.rs index dd16b7688..edf6fe065 100644 --- a/vm-rust/src/player/cast_manager.rs +++ b/vm-rust/src/player/cast_manager.rs @@ -241,16 +241,14 @@ impl CastManager { return self.casts.get(number as usize - 1); } - pub fn get_cast_mut(&mut self, number: u32) -> &mut CastLib { - let n_casts = self.casts.len(); - match self.casts.get_mut(number as usize - 1) { - Some(cast) => cast, - None => panic!( - "Cast index out of bounds: {} (# casts={})", - number, - n_casts - ), - } + pub fn get_cast_mut(&mut self, number: u32) -> Result<&mut CastLib, ScriptError> { + return self + .get_cast_mut_or_null(number) + .ok_or_else(|| ScriptError::new(format!("Cast not found: {}", number))); + } + + pub fn get_cast_mut_or_null(&mut self, number: u32) -> Option<&mut CastLib> { + self.casts.get_mut(number as usize - 1) } pub fn get_cast_by_name(&self, name: &str) -> Option<&CastLib> { @@ -464,7 +462,7 @@ impl CastManager { if member_ref.cast_lib <= 0 || member_ref.cast_lib > self.casts.len() as i32 { return None; } - self.get_cast_mut(member_ref.cast_lib as u32) + self.get_cast_mut_or_null(member_ref.cast_lib as u32)? .find_mut_member_by_number(member_ref.cast_member as u32) } @@ -526,7 +524,7 @@ impl CastManager { "Cannot remove member with invalid cast lib".to_string(), )); } - let cast = self.get_cast_mut(member_ref.cast_lib as u32); + let cast = self.get_cast_mut(member_ref.cast_lib as u32)?; cast.remove_member(member_ref.cast_member as u32); Ok(()) } diff --git a/vm-rust/src/player/commands.rs b/vm-rust/src/player/commands.rs index 02bd8c3b6..7fae7aa18 100644 --- a/vm-rust/src/player/commands.rs +++ b/vm-rust/src/player/commands.rs @@ -416,7 +416,7 @@ pub async fn run_player_command(command: PlayerVMCommand) -> Result Result Date: Wed, 11 Mar 2026 16:21:25 +0100 Subject: [PATCH 03/16] Check for possibly wrong values and fix member_ref_from_slot_number --- vm-rust/src/lib.rs | 5 +- vm-rust/src/player/bitmap/bitmap.rs | 2 +- vm-rust/src/player/cast_lib.rs | 3 ++ vm-rust/src/player/cast_manager.rs | 32 +++--------- vm-rust/src/player/cast_member.rs | 20 +++---- vm-rust/src/player/eval.rs | 10 +--- vm-rust/src/player/events.rs | 12 ++--- .../handlers/datum_handlers/cast_lib.rs | 16 ++---- .../datum_handlers/cast_member/bitmap.rs | 5 +- .../datum_handlers/cast_member_ref.rs | 7 +-- vm-rust/src/player/mod.rs | 12 ++--- vm-rust/src/player/score.rs | 52 ++++--------------- vm-rust/src/rendering.rs | 16 ++---- 13 files changed, 48 insertions(+), 144 deletions(-) diff --git a/vm-rust/src/lib.rs b/vm-rust/src/lib.rs index f568792da..6e51dacd1 100644 --- a/vm-rust/src/lib.rs +++ b/vm-rust/src/lib.rs @@ -191,10 +191,7 @@ pub fn trigger_timeout(name: &str) { #[wasm_bindgen] pub fn player_print_member_bitmap_hex(cast_lib: i32, cast_member: i32) { - player_dispatch(PlayerVMCommand::PrintMemberBitmapHex(CastMemberRef { - cast_lib, - cast_member, - })); + player_dispatch(PlayerVMCommand::PrintMemberBitmapHex(cast_member_ref(cast_lib, cast_member))); } #[wasm_bindgen] diff --git a/vm-rust/src/player/bitmap/bitmap.rs b/vm-rust/src/player/bitmap/bitmap.rs index 230c5a3b8..6a8138bca 100644 --- a/vm-rust/src/player/bitmap/bitmap.rs +++ b/vm-rust/src/player/bitmap/bitmap.rs @@ -14,7 +14,7 @@ use crate::{ }, }; use num::FromPrimitive; - +use crate::player::cast_lib::cast_member_ref; use super::{ mask::BitmapMask, palette::{ diff --git a/vm-rust/src/player/cast_lib.rs b/vm-rust/src/player/cast_lib.rs index 498b5e2ec..8217e2009 100644 --- a/vm-rust/src/player/cast_lib.rs +++ b/vm-rust/src/player/cast_lib.rs @@ -464,6 +464,9 @@ pub const NULL_CAST_MEMBER_REF: CastMemberRef = CastMemberRef { }; pub fn cast_member_ref(cast_lib: i32, cast_member: i32) -> CastMemberRef { + if cast_lib == 65535 { + panic!("cast_lib cannot be 65535 (treated as -1)"); + } CastMemberRef { cast_lib, cast_member, diff --git a/vm-rust/src/player/cast_manager.rs b/vm-rust/src/player/cast_manager.rs index edf6fe065..7f768941f 100644 --- a/vm-rust/src/player/cast_manager.rs +++ b/vm-rust/src/player/cast_manager.rs @@ -16,7 +16,7 @@ use crate::{ js_api::JsApi, player::cast_lib::CastLib, }; - +use crate::player::cast_lib::cast_member_ref; use crate::player::FontManager; use crate::player::font::FontRef; @@ -177,10 +177,7 @@ impl CastManager { target_member, cast.number ); if let Some(bitmap) = bitmap_manager.get_bitmap_mut(bitmap_ref) { - bitmap.palette_ref = PaletteRef::Member(CastMemberRef { - cast_lib: cast.number as i32, - cast_member: target_member, - }); + bitmap.palette_ref = PaletteRef::Member(cast_member_ref(cast.number as i32, target_member)); } found = true; break; @@ -263,10 +260,7 @@ impl CastManager { || CastMemberRefHandlers::get_cast_slot_number(cast.number, member.number) == number { - return Some(CastMemberRef { - cast_lib: cast.number as i32, - cast_member: member.number as i32, - }); + return Some(cast_member_ref(cast.number as i32, member.number as i32)); } } } @@ -305,10 +299,7 @@ impl CastManager { pub fn find_member_ref_by_name(&self, name: &str) -> Option { for cast in &self.casts { if let Some(member) = cast.find_member_by_name(name) { - return Some(CastMemberRef { - cast_lib: cast.number as i32, - cast_member: member.number as i32, - }); + return Some(cast_member_ref(cast.number as i32, member.number as i32)); } } None @@ -357,10 +348,7 @@ impl CastManager { let member_ref = match (&member_name_or_num, cast_lib.as_ref()) { (Datum::String(name), Some(cast_lib)) => { cast_lib.find_member_by_name(name).map(|member| { - Ok(Some(CastMemberRef { - cast_lib: cast_lib.number as i32, - cast_member: member.number as i32, - })) + Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) }) } (Datum::String(name), None) => self @@ -368,10 +356,7 @@ impl CastManager { .map(|member_ref| Ok(Some(member_ref))), (Datum::Int(num), Some(cast_lib)) => { cast_lib.find_member_by_number(*num as u32).map(|member| { - Ok(Some(CastMemberRef { - cast_lib: cast_lib.number as i32, - cast_member: member.number as i32, - })) + Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) }) } (Datum::Int(num), None) => self @@ -379,10 +364,7 @@ impl CastManager { .map(|member_ref| Ok(Some(member_ref))), (Datum::Float(num), Some(cast_lib)) => { cast_lib.find_member_by_number(*num as u32).map(|member| { - Ok(Some(CastMemberRef { - cast_lib: cast_lib.number as i32, - cast_member: member.number as i32, - })) + Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) }) } (Datum::Float(num), None) => self diff --git a/vm-rust/src/player/cast_member.rs b/vm-rust/src/player/cast_member.rs index 3af69f063..8bccae27c 100644 --- a/vm-rust/src/player/cast_member.rs +++ b/vm-rust/src/player/cast_member.rs @@ -22,6 +22,7 @@ use crate::director::{ }, lingo::script::ScriptContext, }; +use crate::player::cast_lib::cast_member_ref; use crate::player::handlers::datum_handlers::cast_member::font::{StyledSpan, TextAlignment}; #[derive(Clone)] @@ -1938,7 +1939,7 @@ impl CastMember { .map(|info| info.header.script_id) .unwrap_or(0); let member_script_ref = if script_id > 0 { - Some(CastMemberRef { cast_lib: cast_lib as i32, cast_member: script_id as i32 }) + Some(cast_member_ref(cast_lib as i32, script_id as i32)) } else { None }; debug!("Flash member {} is a Shape (via shape_info), script_id={}", number, script_id); return CastMember { @@ -1974,7 +1975,7 @@ impl CastMember { .map(|info| info.header.script_id) .unwrap_or(0); let member_script_ref = if script_id > 0 { - Some(CastMemberRef { cast_lib: cast_lib as i32, cast_member: script_id as i32 }) + Some(cast_member_ref(cast_lib as i32, script_id as i32)) } else { None }; debug!("Flash member {} is actually a Shape! script_id={}", number, script_id); return CastMember { @@ -2053,10 +2054,7 @@ impl CastMember { let script_chunk = &lctx.as_ref().unwrap().scripts[&script_id]; // Create the behavior script reference - Some(CastMemberRef { - cast_lib: cast_lib as i32, - cast_member: script_id as i32, - }) + Some(cast_member_ref(cast_lib as i32, script_id as i32)) } else { None }; @@ -2170,10 +2168,7 @@ impl CastMember { .unwrap_or(0); let member_script_ref = if script_id > 0 { - Some(CastMemberRef { - cast_lib: cast_lib as i32, - cast_member: script_id as i32, - }) + Some(cast_member_ref(cast_lib as i32, script_id as i32)) } else { None }; @@ -2444,10 +2439,7 @@ impl CastMember { let behavior_script_ref = if script_id > 0 { let _script_chunk = &lctx.as_ref().unwrap().scripts[&script_id]; - Some(CastMemberRef { - cast_lib: cast_lib as i32, - cast_member: script_id as i32, - }) + Some(cast_member_ref(cast_lib as i32, script_id as i32)) } else { None }; diff --git a/vm-rust/src/player/eval.rs b/vm-rust/src/player/eval.rs index 02c7a7580..fffb9ac6f 100644 --- a/vm-rust/src/player/eval.rs +++ b/vm-rust/src/player/eval.rs @@ -314,10 +314,7 @@ pub fn eval_lingo_pair_static(pair: Pair) -> Result ))), }; let member_num = member_id_datum.int_value().unwrap_or(0); - super::cast_lib::CastMemberRef { - cast_lib: cast_lib_num, - cast_member: member_num, - } + super::cast_lib::cast_member_ref(cast_lib_num, member_num) } else { INVALID_CAST_MEMBER_REF } @@ -1465,10 +1462,7 @@ pub async fn eval_lingo_expr_ast_runtime(expr: &LingoExpr) -> Result = vec![]; if let Some(frame_script) = frame_script { - let script_ref = CastMemberRef { - cast_lib: frame_script.cast_lib.into(), - cast_member: frame_script.cast_member.into(), - }; + let script_ref = cast_member_ref(frame_script.cast_lib.into(), frame_script.cast_member.into()); active_static_scripts.push(script_ref); } for movie_script in movie_scripts { diff --git a/vm-rust/src/player/handlers/datum_handlers/cast_lib.rs b/vm-rust/src/player/handlers/datum_handlers/cast_lib.rs index 3eb74d67d..4974a2d63 100644 --- a/vm-rust/src/player/handlers/datum_handlers/cast_lib.rs +++ b/vm-rust/src/player/handlers/datum_handlers/cast_lib.rs @@ -5,6 +5,7 @@ use crate::{ ScriptErrorCode, }, }; +use crate::player::cast_lib::cast_member_ref; pub struct CastLibDatumHandlers {} @@ -60,16 +61,10 @@ impl CastLibDatumHandlers { let member_ref = match &member_name_or_num { Datum::String(name) => { - cast.find_member_by_name(name).map(|member| CastMemberRef { - cast_lib: cast_lib_num as i32, - cast_member: member.number as i32, - }) + cast.find_member_by_name(name).map(|member| cast_member_ref(cast_lib_num as i32, member.number as i32)) } Datum::Int(num) => { - cast.find_member_by_number(*num as u32).map(|member| CastMemberRef { - cast_lib: cast_lib_num as i32, - cast_member: member.number as i32, - }) + cast.find_member_by_number(*num as u32).map(|member| cast_member_ref(cast_lib_num as i32, member.number as i32)) } _ => { return Err(ScriptError::new(format!( @@ -83,10 +78,7 @@ impl CastLibDatumHandlers { Some(mr) => Ok(player.alloc_datum(Datum::CastMember(mr))), None => { // Return an invalid member ref (member 0) for non-existent members - Ok(player.alloc_datum(Datum::CastMember(CastMemberRef { - cast_lib: cast_lib_num as i32, - cast_member: 0, - }))) + Ok(player.alloc_datum(Datum::CastMember(cast_member_ref(cast_lib_num as i32, 0)))) } } } diff --git a/vm-rust/src/player/handlers/datum_handlers/cast_member/bitmap.rs b/vm-rust/src/player/handlers/datum_handlers/cast_member/bitmap.rs index 6020fb008..b0c26a131 100644 --- a/vm-rust/src/player/handlers/datum_handlers/cast_member/bitmap.rs +++ b/vm-rust/src/player/handlers/datum_handlers/cast_member/bitmap.rs @@ -43,10 +43,7 @@ impl BitmapMemberHandlers { match palette { PaletteRef::BuiltIn(builtin) => Ok(Datum::Symbol(builtin.symbol_string())), PaletteRef::Member(member_ref) => { - let member_ref = CastMemberRef { - cast_member: member_ref.cast_member, - cast_lib: member_ref.cast_lib, - }; + let member_ref = crate::cast_member_ref(member_ref.cast_lib, member_ref.cast_member); Ok(Datum::CastMember(member_ref)) } PaletteRef::Default => Ok(Datum::PaletteRef(PaletteRef::Default)), diff --git a/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs b/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs index 374ae3394..b59e75983 100644 --- a/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs +++ b/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs @@ -14,7 +14,7 @@ use crate::{ sprite::ColorRef, }, }; - +use crate::player::cast_lib::cast_member_ref; use super::cast_member::{ bitmap::BitmapMemberHandlers, button::ButtonMemberHandlers, field::FieldMemberHandlers, film_loop::FilmLoopMemberHandlers, font::FontMemberHandlers, sound::SoundMemberHandlers, @@ -49,10 +49,7 @@ impl CastMemberRefHandlers { } pub fn member_ref_from_slot_number(slot_number: u32) -> CastMemberRef { - CastMemberRef { - cast_lib: (slot_number >> 16) as i32, - cast_member: (slot_number & 0xFFFF) as i32, - } + cast_member_ref((slot_number >> 16) as i16 as i32, (slot_number & 0xFFFF) as i16 as i32) } pub fn call( diff --git a/vm-rust/src/player/mod.rs b/vm-rust/src/player/mod.rs index 189195e02..09d8a8985 100644 --- a/vm-rust/src/player/mod.rs +++ b/vm-rust/src/player/mod.rs @@ -112,7 +112,7 @@ use crate::{ rendering::with_renderer_mut, utils::{get_base_url, get_elapsed_ticks}, }; - +use crate::player::cast_lib::cast_member_ref; use self::{ bitmap::manager::BitmapRef, bytecode::handler_manager::StaticBytecodeHandlerManager, @@ -2142,10 +2142,7 @@ pub async fn player_call_script_handler_raw_args( let is_frame_script = reserve_player_ref(|player| { let frame_script = player.movie.score.get_script_in_frame(player.movie.current_frame); if let Some(fs) = frame_script { - let frame_script_ref = CastMemberRef { - cast_lib: fs.cast_lib.into(), - cast_member: fs.cast_member.into(), - }; + let frame_script_ref = cast_member_ref(fs.cast_lib.into(), fs.cast_member.into()); script_member_ref == &frame_script_ref } else { false @@ -3331,10 +3328,7 @@ fn get_active_static_script_refs<'a>( active_script_refs.push(script.member_ref.clone()); } if let Some(frame_script) = frame_script { - active_script_refs.push(CastMemberRef { - cast_lib: frame_script.cast_lib.into(), - cast_member: frame_script.cast_member.into(), - }); + active_script_refs.push(cast_member_ref(frame_script.cast_lib.into(), frame_script.cast_member.into())); } for global in globals.values() { if let Datum::VarRef(VarRef::Script(script_ref)) = global { diff --git a/vm-rust/src/player/score.rs b/vm-rust/src/player/score.rs index f3ce5a165..378a736f5 100644 --- a/vm-rust/src/player/score.rs +++ b/vm-rust/src/player/score.rs @@ -241,10 +241,7 @@ impl Score { cast_lib }; - let mut script_ref = CastMemberRef { - cast_lib: resolved_cast_lib, - cast_member, - }; + let mut script_ref = cast_member_ref(resolved_cast_lib, cast_member); // Check if the script exists in the resolved cast library // For cast 65535 (relative cast reference), we only use the filmloop's own cast. @@ -590,13 +587,10 @@ impl Score { } else if data.cast_lib == 0 { 1 } else { - data.cast_lib as i32 + data.cast_lib as i16 as i32 }; - let member = CastMemberRef { - cast_lib: resolved_cast_lib, - cast_member: data.cast_member as i32, - }; + let member = cast_member_ref(resolved_cast_lib, data.cast_member as i32); // For Stage sprites, use sprite_set_prop which handles intrinsic size // initialization and other side effects. For FilmLoop sprites, set @@ -831,10 +825,7 @@ impl Score { data.cast_lib as i32 }; - let member = CastMemberRef { - cast_lib: resolved_cast_lib, - cast_member: data.cast_member as i32, - }; + let member = cast_member_ref(resolved_cast_lib, data.cast_member as i32); // Update member if changed (skip if auto-puppeted) let current_member = self.get_sprite(sprite_num).and_then(|s| s.member.clone()); @@ -926,10 +917,7 @@ impl Score { } else { data.cast_lib as i32 }; - let member = CastMemberRef { - cast_lib: resolved_cast_lib, - cast_member: data.cast_member as i32, - }; + let member = cast_member_ref(resolved_cast_lib, data.cast_member as i32); match &score_ref { ScoreRef::Stage => { @@ -1061,10 +1049,7 @@ impl Score { let sound_member_opt = match &score_ref { ScoreRef::FilmLoop(filmloop_member_ref) => { // Look up sound in the film loop's cast library - let cast_member_ref = CastMemberRef { - cast_lib: filmloop_member_ref.cast_lib, - cast_member: sound_data.cast_member as i32, - }; + let cast_member_ref = cast_member_ref(filmloop_member_ref.cast_lib, sound_data.cast_member as i32); player.movie.cast_manager.find_member_by_ref(&cast_member_ref) .map(|m| (m, cast_member_ref)) } @@ -1074,10 +1059,7 @@ impl Score { .find_member_by_slot_number(sound_data.cast_member as u32) .map(|m| { let ref_ = CastMemberRefHandlers::member_ref_from_slot_number(m.number); - (m, CastMemberRef { - cast_lib: ref_.cast_lib as i32, - cast_member: ref_.cast_member as i32, - }) + (m, cast_member_ref(ref_.cast_lib as i32, ref_.cast_member as i32)) }) } }; @@ -1529,10 +1511,7 @@ impl Score { } else { data.cast_lib as i32 }; - let sprite_member = CastMemberRef { - cast_lib: sprite_cast_lib, - cast_member: data.cast_member as i32, - }; + let sprite_member = cast_member_ref(sprite_cast_lib, data.cast_member as i32); debug!( "D5 sprite ch={}: scriptId=({},{}), sprite_member=({},{})", channel_num, resolved_cast_lib, script_member, @@ -1982,10 +1961,7 @@ impl Score { }); // Create the CastMemberRef for later use - let cast_member_ref = CastMemberRef { - cast_lib: behavior_ref.cast_lib as i32, - cast_member: behavior_ref.cast_member as i32, - }; + let cast_member_ref = cast_member_ref(behavior_ref.cast_lib as i32, behavior_ref.cast_member as i32); // Set spriteNum property reserve_player_mut(|player| { @@ -2144,10 +2120,7 @@ impl Score { data.cast_lib as i32 }; - let member = CastMemberRef { - cast_lib: resolved_cast_lib, - cast_member: data.cast_member as i32, - }; + let member = cast_member_ref(resolved_cast_lib, data.cast_member as i32); // Set member directly on the sprite instead of using sprite_set_prop, // because sprite_set_prop always writes to main stage score, @@ -3072,10 +3045,7 @@ impl Score { PaletteRef::from(*member, *cast_lib, 0) } else if *member > 0 { // Positive member = cast member palette - PaletteRef::Member(CastMemberRef { - cast_lib: *cast_lib as i32, - cast_member: *member as i32, - }) + PaletteRef::Member(cast_member_ref(*cast_lib as i32, *member as i32)) } else { // member == 0: use system default PaletteRef::BuiltIn(get_system_default_palette()) diff --git a/vm-rust/src/rendering.rs b/vm-rust/src/rendering.rs index f398a646f..92664b343 100644 --- a/vm-rust/src/rendering.rs +++ b/vm-rust/src/rendering.rs @@ -40,6 +40,7 @@ use crate::player::font::FontManager; use crate::player::font::bitmap_font_copy_char; use crate::player::handlers::datum_handlers::cast_member::font::{FontMemberHandlers, TextAlignment, StyledSpan, HtmlStyle}; use crate::director::lingo::datum::Datum; +use crate::player::cast_lib::cast_member_ref; use crate::player::score_keyframes::SpritePathKeyframes; use crate::rendering_gpu::{DynamicRenderer, Renderer}; @@ -542,10 +543,7 @@ pub fn compute_filmloop_initial_rect_with_members( } else { data.cast_lib as i32 }; - let sprite_member_ref = CastMemberRef { - cast_lib: sprite_cast_lib, - cast_member: data.cast_member as i32, - }; + let sprite_member_ref = cast_member_ref(sprite_cast_lib, data.cast_member as i32); // Get actual bitmap dimensions and registration point from the cast member. // The registration point is the anchor used for positioning - sprite_left = pos_x - reg_x. @@ -750,10 +748,7 @@ fn render_filmloop_from_channel_data( // Build member ref from channel data // cast_lib 65535 means "use the filmloop's cast library" - let sprite_member_ref = CastMemberRef { - cast_lib: if data.cast_lib == 65535 { filmloop_cast_lib } else { data.cast_lib as i32 }, - cast_member: data.cast_member as i32, - }; + let sprite_member_ref = cast_member_ref(if data.cast_lib == 65535 { filmloop_cast_lib } else { data.cast_lib as i32 }, data.cast_member as i32); let member = player.movie.cast_manager.find_member_by_ref(&sprite_member_ref); if member.is_none() { @@ -2789,10 +2784,7 @@ pub fn player_set_preview_member_ref(cast_lib: i32, cast_num: i32) -> Result<(), use crate::rendering_gpu::Renderer; with_renderer_mut(|renderer_lock| { if let Some(dynamic) = renderer_lock { - dynamic.set_preview_member_ref(Some(CastMemberRef { - cast_lib, - cast_member: cast_num, - })); + dynamic.set_preview_member_ref(Some(cast_member_ref(cast_lib, cast_num))); } }); Ok(()) From b7ab025deb3b5a69e439ffb1edf062adb5152bd9 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 16:38:13 +0100 Subject: [PATCH 04/16] Add member property on cast libraries --- vm-rust/src/player/cast_lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vm-rust/src/player/cast_lib.rs b/vm-rust/src/player/cast_lib.rs index 8217e2009..df8add41b 100644 --- a/vm-rust/src/player/cast_lib.rs +++ b/vm-rust/src/player/cast_lib.rs @@ -213,7 +213,8 @@ impl CastLib { "name" => Ok(Datum::String(self.name.clone())), "number of castMembers" | "number of members" => { Ok(Datum::Int(self.members.len() as i32)) - } + }, + "member" => Ok(Datum::CastMember(cast_member_ref(self.number as i32, -1))), _ => Err(ScriptError::new(format!( "Cannot get castLib property {}", prop From 7d7c48dab207d09bd3384800399e7766f64e02c6 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 17:27:52 +0100 Subject: [PATCH 05/16] Add [] for cast lib member list --- vm-rust/src/player/eval.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vm-rust/src/player/eval.rs b/vm-rust/src/player/eval.rs index fffb9ac6f..633843edb 100644 --- a/vm-rust/src/player/eval.rs +++ b/vm-rust/src/player/eval.rs @@ -17,7 +17,7 @@ use crate::{ DirPlayer, }, }; - +use crate::player::cast_lib::CastMemberRef; use super::{cast_lib::INVALID_CAST_MEMBER_REF, datum_formatting::format_datum, sprite::ColorRef, DatumRef, ScriptError}; #[derive(Parser)] @@ -1362,6 +1362,15 @@ pub async fn eval_lingo_expr_ast_runtime(expr: &LingoExpr) -> Result { + let index_num = index_datum.int_value()? as i16; + Ok(reserve_player_mut(|p| p.alloc_datum(if index_num < 0 { + Datum::Void + } else { + Datum::CastMember(CastMemberRef { cast_lib: *cast_lib, cast_member: index_num as i32 }) + }))) + } _ => Err(ScriptError::new(format!( "Cannot index non-list type: {:?}", list_datum.type_enum() From 57680f6f6480856f4188217a7f6cf6d3fbbedde0 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 17:47:07 +0100 Subject: [PATCH 06/16] Fix panics in Put window by introducing get_current_scope --- vm-rust/src/js_api.rs | 2 +- vm-rust/src/player/bytecode/get_set.rs | 6 +++++- vm-rust/src/player/handlers/manager.rs | 15 +++++++++------ vm-rust/src/player/handlers/movie.rs | 6 +++--- vm-rust/src/player/handlers/types.rs | 3 +-- vm-rust/src/player/mod.rs | 20 +++++++++++++------- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/vm-rust/src/js_api.rs b/vm-rust/src/js_api.rs index ae3e4cc0d..5ad276a05 100644 --- a/vm-rust/src/js_api.rs +++ b/vm-rust/src/js_api.rs @@ -1481,7 +1481,7 @@ impl JsApi { .unwrap_or(false); let data: js_sys::Map = - if let Some(current_scope) = player.scopes.get(player.current_scope_ref()) { + if let Some(current_scope) = player.get_current_scope() { let cast_lib = player .movie .cast_manager diff --git a/vm-rust/src/player/bytecode/get_set.rs b/vm-rust/src/player/bytecode/get_set.rs index 77aca619e..29e1eee26 100644 --- a/vm-rust/src/player/bytecode/get_set.rs +++ b/vm-rust/src/player/bytecode/get_set.rs @@ -37,7 +37,11 @@ impl GetSetUtils { ) -> Result { match prop_name { "paramCount" => Ok(player.alloc_datum(Datum::Int( - player.scopes.get(ctx.scope_ref).unwrap().args.len() as i32, + if let Some(scope) = player.scopes.get(ctx.scope_ref) { + scope.args.len() as i32 + } else { + 0 + } ))), "result" => Ok(player.last_handler_result.clone()), "pi" => Ok(player.alloc_datum(Datum::Float(std::f64::consts::PI))), diff --git a/vm-rust/src/player/handlers/manager.rs b/vm-rust/src/player/handlers/manager.rs index 7c1d6b707..db248b691 100644 --- a/vm-rust/src/player/handlers/manager.rs +++ b/vm-rust/src/player/handlers/manager.rs @@ -30,11 +30,15 @@ pub struct BuiltInHandlerManager {} impl BuiltInHandlerManager { fn param(args: &Vec) -> Result { - reserve_player_ref(|player| { + reserve_player_mut(|player| { let param_number = player.get_datum(&args[0]).int_value()?; - let scope_ref = player.current_scope_ref(); - let scope = player.scopes.get(scope_ref).unwrap(); - Ok(scope.args[(param_number - 1) as usize].clone()) + if let Some(scope) = player.get_current_scope() && + let Some(arg) = scope.args.get(param_number.overflowing_sub(1).0 as usize) { + Ok(arg.clone()) + } else { + // outside a function, Director returns 0 + return Ok(player.alloc_datum(Datum::Int(0))); + } }) } @@ -1183,8 +1187,7 @@ impl BuiltInHandlerManager { fn dont_pass_event(_args: &Vec) -> Result { reserve_player_mut(|player| { - let scope_ref = player.current_scope_ref(); - if let Some(scope) = player.scopes.get_mut(scope_ref) { + if let Some(scope) = player.get_current_scope_mut() { scope.passed = false; // Set passed to false to stop propagation } Ok(DatumRef::Void) diff --git a/vm-rust/src/player/handlers/movie.rs b/vm-rust/src/player/handlers/movie.rs index fc5a8392d..f0e1b039a 100644 --- a/vm-rust/src/player/handlers/movie.rs +++ b/vm-rust/src/player/handlers/movie.rs @@ -702,9 +702,9 @@ impl MovieHandlers { pub fn pass(_: &Vec) -> Result { reserve_player_mut(|player| { - let scope_ref = player.current_scope_ref(); - let scope = player.scopes.get_mut(scope_ref).unwrap(); - scope.passed = true; + if let Some(scope) = player.get_current_scope_mut() { + scope.passed = true; + } Ok(DatumRef::Void) }) } diff --git a/vm-rust/src/player/handlers/types.rs b/vm-rust/src/player/handlers/types.rs index f81251e32..e4cbc58f3 100644 --- a/vm-rust/src/player/handlers/types.rs +++ b/vm-rust/src/player/handlers/types.rs @@ -1392,8 +1392,7 @@ impl TypeHandlers { let handler_name = player.get_datum(&args[0]).string_value()?; // Get the current scope's script_ref to determine which script we're currently in - let current_scope_ref = player.current_scope_ref(); - let current_script_ref = player.scopes.get(current_scope_ref) + let current_script_ref = player.get_current_scope() .map(|scope| scope.script_ref.clone()); let list_or_script_instance = player.get_datum(&args[1]); diff --git a/vm-rust/src/player/mod.rs b/vm-rust/src/player/mod.rs index 09d8a8985..e14307454 100644 --- a/vm-rust/src/player/mod.rs +++ b/vm-rust/src/player/mod.rs @@ -1318,8 +1318,7 @@ impl DirPlayer { "currentSpriteNum" => { // TODO: this can also be called by a static script let script_instance_ref = self - .scopes - .get(self.current_scope_ref()) + .get_current_scope() .and_then(|scope| scope.receiver.clone()); if let Some(script_instance_ref) = script_instance_ref { @@ -1620,8 +1619,17 @@ impl DirPlayer { self.scope_count -= 1; } - pub fn current_scope_ref(&self) -> ScopeRef { - (self.scope_count - 1) as ScopeRef + pub fn current_scope_ref(&self) -> Option { + self.scope_count.checked_sub(1).map(|c| c as ScopeRef) + } + + pub fn get_current_scope(&self) -> Option<&Scope> { + self.scopes.get(self.current_scope_ref()?) + } + + pub fn get_current_scope_mut(&mut self) -> Option<&mut Scope> { + let scope = self.current_scope_ref()?; + self.scopes.get_mut(scope) } // Lingo: sound(channelNum) @@ -1959,9 +1967,7 @@ impl ScriptError { pub fn player_handle_scope_return(scope: &ScopeResult) { if scope.passed { reserve_player_mut(|player| { - let scope_ref = player.current_scope_ref(); - let last_scope = player.scopes.get_mut(scope_ref); - if let Some(last_scope) = last_scope { + if let Some(last_scope) = player.get_current_scope_mut() { last_scope.passed = true; } }); From 5e623be9dc3fdbfaea4c43bccb13eecd1e84f7ef Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 18:21:10 +0100 Subject: [PATCH 07/16] Allow searching for handlers --- src/components/CastList/index.tsx | 10 ++++++---- src/vm/index.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/components/CastList/index.tsx b/src/components/CastList/index.tsx index 2c42e3e6f..2b7157c2d 100644 --- a/src/components/CastList/index.tsx +++ b/src/components/CastList/index.tsx @@ -1,5 +1,8 @@ import { useState, useMemo } from "react" -import { ICastMemberIdentifier, castMemberIdentifier, CastSnapshot, CastMemberRecord } from "../../vm" +import { + ICastMemberIdentifier, castMemberIdentifier, CastSnapshot, CastMemberRecord, IScriptSnapshot, + IScriptMemberSnapshot, memberMatches +} from "../../vm" import classNames from "classnames" import styles from './styles.module.css' import _ from "lodash" @@ -63,9 +66,8 @@ function CastListItem({ number, name, members, selectedMemberId, onSelectMember, const filteredMembers = useMemo(() => { if (!filterText) return Object.entries(members); - const lowerFilter = filterText.toLowerCase(); return Object.entries(members).filter(([, member]) => - member.name.toLowerCase().includes(lowerFilter) + memberMatches(member, filterText) ); }, [members, filterText]); @@ -130,7 +132,7 @@ export default function CastList({ castNames, castSnapshots, selectedMemberId, o // When searching, check if this cast has any matching members const hasMatchingMembers = isSearching && Object.values(members).some( - (member) => member.name.toLowerCase().includes(searchQuery.toLowerCase()) + (member) => memberMatches(member, searchQuery) ); // Skip casts with no matching members when searching diff --git a/src/vm/index.ts b/src/vm/index.ts index 95d9f1f2d..afd010b52 100644 --- a/src/vm/index.ts +++ b/src/vm/index.ts @@ -240,3 +240,22 @@ export interface ScoreSnapshot { } export type MemberSnapshot = IBaseMemberSnapshot & (IFieldMemberSnapshot | IScriptMemberSnapshot | IBitmapMemberSnapshot | IPaletteMemberSnapshot | IFontMemberSnapshot | IUnknownMemberSnapshot | IFilmLoopMemberSnapshot) + +export function memberMatches(record: CastMemberRecord, query: string): boolean { + const lowerQuery = query.toLowerCase(); + + if (record.name.toLowerCase().includes(lowerQuery)) { + return true; + } + + switch (record.type) { + case 'field': + return (record.snapshot as IFieldMemberSnapshot)?.text.toLowerCase().includes(lowerQuery) ?? false; + case 'script': + return (record.snapshot as IScriptMemberSnapshot)?.script.handlers.some(handler => + handler.name.toLowerCase().includes(lowerQuery) + ) ?? false; + default: + return false; + } +} \ No newline at end of file From acf2c8e80eb5886b7a35b0bd1173901825919b98 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 20:43:23 +0100 Subject: [PATCH 08/16] Add repr() function on Datum --- vm-rust/src/director/lingo/datum.rs | 10 ++++++++++ vm-rust/src/player/handlers/manager.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/vm-rust/src/director/lingo/datum.rs b/vm-rust/src/director/lingo/datum.rs index 1e67070fa..b2c90544b 100644 --- a/vm-rust/src/director/lingo/datum.rs +++ b/vm-rust/src/director/lingo/datum.rs @@ -6,6 +6,7 @@ use num_derive::FromPrimitive; use crate::player::{ DirPlayer, ScriptError, bitmap::{bitmap::PaletteRef, manager::BitmapRef, mask::BitmapMask}, cast_lib::CastMemberRef, cast_member::Media, datum_ref::DatumRef, script_ref::ScriptInstanceRef, sprite::{ColorRef, CursorRef} }; +use crate::player::handlers::manager::BuiltInHandlerManager; #[allow(dead_code)] #[derive(Clone, PartialEq, Debug)] @@ -271,6 +272,11 @@ impl Datum { } // TODO(zdimension): this should really return a Cow instead of allocating a String + // also.. this seems to be used for many things: + // - stringification + // - string operators (starts) which should only work in strings (i.e. "123" starts "12" should return false, it currently returns true) + // - ? + // it should be replace by repr() in some places pub fn string_value(&self) -> Result { match self { Datum::String(s) => Ok(s.clone()), @@ -308,6 +314,10 @@ impl Datum { } } + pub fn repr(&self, player: &DirPlayer) -> String { + BuiltInHandlerManager::format_for_put(self, player) + } + pub fn symbol_value(&self) -> Result { match self { Datum::Symbol(s) => Ok(s.clone()), diff --git a/vm-rust/src/player/handlers/manager.rs b/vm-rust/src/player/handlers/manager.rs index db248b691..68a22773e 100644 --- a/vm-rust/src/player/handlers/manager.rs +++ b/vm-rust/src/player/handlers/manager.rs @@ -290,7 +290,7 @@ impl BuiltInHandlerManager { Ok(DatumRef::Void) } - fn format_for_put(datum: &Datum, player: &DirPlayer) -> String { + pub fn format_for_put(datum: &Datum, player: &DirPlayer) -> String { match datum { // Strings are output with quotes Datum::String(s) => format!("\"{}\"", s), From 8292250a4449bbd294f8f1dac0c38976e3dd1aa6 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 20:49:42 +0100 Subject: [PATCH 09/16] Add find_cast_lib_by_identifier --- vm-rust/src/player/cast_manager.rs | 80 +++++++++++++++++------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/vm-rust/src/player/cast_manager.rs b/vm-rust/src/player/cast_manager.rs index 7f768941f..b33ae2a74 100644 --- a/vm-rust/src/player/cast_manager.rs +++ b/vm-rust/src/player/cast_manager.rs @@ -17,7 +17,7 @@ use crate::{ player::cast_lib::CastLib, }; use crate::player::cast_lib::cast_member_ref; -use crate::player::FontManager; +use crate::player::{reserve_player_ref, FontManager}; use crate::player::font::FontRef; use super::{ @@ -312,39 +312,12 @@ impl CastManager { datums: &DatumAllocator, ) -> Result, ScriptError> { // --- Determine cast library --- - let cast_lib = if cast_name_or_num.is_none() - || cast_name_or_num.is_some_and(|x| matches!(x, Datum::Void)) - { - None - } else if cast_name_or_num.is_some_and(|x| x.is_string()) { - if let Ok(cast_name) = cast_name_or_num.unwrap().string_value() { - self.get_cast_by_name(&cast_name) - } else { - warn!( - "Invalid cast name: {}", - cast_name_or_num.unwrap().type_str() - ); - None - } - } else if cast_name_or_num.is_some_and(|x| x.is_number()) { - let int_val = cast_name_or_num.unwrap().int_value().unwrap_or(-1); - if int_val > 0 { - self.get_cast_or_null(int_val as u32) - } else { - None - } - } else if let Some(Datum::CastLib(cast_lib)) = cast_name_or_num { - self.get_cast_or_null(*cast_lib as u32) - } else { - warn!( - "Cast number or name invalid: {}", - cast_name_or_num - .map(|x| x.type_str()) - .unwrap_or("None") - ); - None - }; + let cast_lib = self.find_cast_lib_by_identifier(cast_name_or_num); + + self.find_member_ref_in_cast_by_identifier(member_name_or_num, cast_lib) + } + pub fn find_member_ref_in_cast_by_identifier(&self, member_name_or_num: &Datum, cast_lib: Option<&CastLib>) -> Result, ScriptError> { let member_ref = match (&member_name_or_num, cast_lib.as_ref()) { (Datum::String(name), Some(cast_lib)) => { cast_lib.find_member_by_name(name).map(|member| { @@ -372,8 +345,9 @@ impl CastManager { .map(|member_ref| Ok(Some(member_ref))), (Datum::CastMember(member_ref), _) => Some(Ok(Some(member_ref.clone()))), _ => Some(Err(ScriptError::new(format!( - "Member number or name type invalid: {}", - member_name_or_num.type_str() + "Member number or name type invalid: {} ({})", + member_name_or_num.type_str(), + reserve_player_ref(|p| member_name_or_num.repr(p)) )))), }; @@ -385,6 +359,42 @@ impl CastManager { } } + pub(crate) fn find_cast_lib_by_identifier(&self, cast_name_or_num: Option<&Datum>) -> Option<&CastLib> { + let cast_lib = if cast_name_or_num.is_none() + || cast_name_or_num.is_some_and(|x| matches!(x, Datum::Void)) + { + None + } else if cast_name_or_num.is_some_and(|x| x.is_string()) { + if let Ok(cast_name) = cast_name_or_num.unwrap().string_value() { + self.get_cast_by_name(&cast_name) + } else { + warn!( + "Invalid cast name: {}", + cast_name_or_num.unwrap().type_str() + ); + None + } + } else if cast_name_or_num.is_some_and(|x| x.is_number()) { + let int_val = cast_name_or_num.unwrap().int_value().unwrap_or(-1); + if int_val > 0 { + self.get_cast_or_null(int_val as u32) + } else { + None + } + } else if let Some(Datum::CastLib(cast_lib)) = cast_name_or_num { + self.get_cast_or_null(*cast_lib as u32) + } else { + warn!( + "Cast number or name invalid: {}", + cast_name_or_num + .map(|x| x.type_str()) + .unwrap_or("None") + ); + None + }; + cast_lib + } + pub fn find_member_by_identifiers( &self, member_name_or_num: &Datum, From a4753963644e820cf7d51bf2a95763222d612b53 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 23:14:18 +0100 Subject: [PATCH 10/16] Change invalid member type from string to symbol --- vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs b/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs index b59e75983..3f8de8368 100644 --- a/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs +++ b/vm-rust/src/player/handlers/datum_handlers/cast_member_ref.rs @@ -264,7 +264,7 @@ impl CastMemberRefHandlers { match prop { "name" => Ok(Datum::String("".to_string())), "number" => Ok(Datum::Int(-1)), - "type" => Ok(Datum::String("empty".to_string())), + "type" => Ok(Datum::Symbol("empty".to_string())), "castLibNum" => Ok(Datum::Int(-1)), "memberNum" => Ok(Datum::Int(-1)), "width" | "height" | "rect" | "duration" => Ok(Datum::Void), From 02c18a1b4d7e73cd256cdbd8f924dd2d94d443c2 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Wed, 11 Mar 2026 23:30:30 +0100 Subject: [PATCH 11/16] Fix panic on invalid member ref --- vm-rust/src/player/cast_manager.rs | 40 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/vm-rust/src/player/cast_manager.rs b/vm-rust/src/player/cast_manager.rs index b33ae2a74..577a188d3 100644 --- a/vm-rust/src/player/cast_manager.rs +++ b/vm-rust/src/player/cast_manager.rs @@ -235,7 +235,7 @@ impl CastManager { } pub fn get_cast_or_null(&self, number: u32) -> Option<&CastLib> { - return self.casts.get(number as usize - 1); + return self.casts.get((number as usize).wrapping_sub(1)); } pub fn get_cast_mut(&mut self, number: u32) -> Result<&mut CastLib, ScriptError> { @@ -245,7 +245,7 @@ impl CastManager { } pub fn get_cast_mut_or_null(&mut self, number: u32) -> Option<&mut CastLib> { - self.casts.get_mut(number as usize - 1) + self.casts.get_mut((number as usize).wrapping_sub(1)) } pub fn get_cast_by_name(&self, name: &str) -> Option<&CastLib> { @@ -321,42 +321,38 @@ impl CastManager { let member_ref = match (&member_name_or_num, cast_lib.as_ref()) { (Datum::String(name), Some(cast_lib)) => { cast_lib.find_member_by_name(name).map(|member| { - Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) + cast_member_ref(cast_lib.number as i32, member.number as i32) }) } (Datum::String(name), None) => self .find_member_ref_by_name(name) - .map(|member_ref| Ok(Some(member_ref))), + .map(|member_ref| member_ref), + (Datum::Int(num), Some(cast_lib)) => { - cast_lib.find_member_by_number(*num as u32).map(|member| { - Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) - }) + Some(cast_member_ref(cast_lib.number as i32, *num as i32)) } - (Datum::Int(num), None) => self + (Datum::Int(num), None) => Some(self .find_member_ref_by_number(*num as u32) - .map(|member_ref| Ok(Some(member_ref))), + .unwrap_or(cast_member_ref(1, *num))), (Datum::Float(num), Some(cast_lib)) => { - cast_lib.find_member_by_number(*num as u32).map(|member| { - Ok(Some(cast_member_ref(cast_lib.number as i32, member.number as i32))) - }) + Some(cast_member_ref(cast_lib.number as i32, *num as i32)) } - (Datum::Float(num), None) => self + (Datum::Float(num), None) => Some(self .find_member_ref_by_number(*num as u32) - .map(|member_ref| Ok(Some(member_ref))), - (Datum::CastMember(member_ref), _) => Some(Ok(Some(member_ref.clone()))), - _ => Some(Err(ScriptError::new(format!( + .unwrap_or(cast_member_ref(1, *num as i32))), + (Datum::CastMember(member_ref), _) => Some(member_ref.clone()), + _ => return Err(ScriptError::new(format!( "Member number or name type invalid: {} ({})", member_name_or_num.type_str(), reserve_player_ref(|p| member_name_or_num.repr(p)) - )))), + ))), }; - match member_ref { - None => Ok(None), - Some(Ok(None)) => Ok(None), - Some(Ok(Some(member_ref))) => Ok(Some(member_ref)), - Some(Err(err)) => Err(err), + if let Some(CastMemberRef { cast_member, .. }) = member_ref && cast_member < 0 { + return Ok(None); } + + Ok(member_ref) } pub(crate) fn find_cast_lib_by_identifier(&self, cast_name_or_num: Option<&Datum>) -> Option<&CastLib> { From 08e104ffc8b59df23f9d79cc340ef6441114a859 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Mon, 6 Apr 2026 16:39:21 +0200 Subject: [PATCH 12/16] Replace overflowing_sub by wrapping_sub since only value is needed --- vm-rust/src/player/handlers/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm-rust/src/player/handlers/manager.rs b/vm-rust/src/player/handlers/manager.rs index 68a22773e..43e3e5ebf 100644 --- a/vm-rust/src/player/handlers/manager.rs +++ b/vm-rust/src/player/handlers/manager.rs @@ -33,7 +33,7 @@ impl BuiltInHandlerManager { reserve_player_mut(|player| { let param_number = player.get_datum(&args[0]).int_value()?; if let Some(scope) = player.get_current_scope() && - let Some(arg) = scope.args.get(param_number.overflowing_sub(1).0 as usize) { + let Some(arg) = scope.args.get(param_number.wrapping_sub(1) as usize) { Ok(arg.clone()) } else { // outside a function, Director returns 0 From b178cbcad328eab18fca55ce9cef039a3760819f Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Mon, 6 Apr 2026 16:45:02 +0200 Subject: [PATCH 13/16] Homogeneify member behavior --- vm-rust/src/player/eval.rs | 98 ++++------------------------ vm-rust/src/player/handlers/movie.rs | 34 +++++----- 2 files changed, 29 insertions(+), 103 deletions(-) diff --git a/vm-rust/src/player/eval.rs b/vm-rust/src/player/eval.rs index 633843edb..0ff55328c 100644 --- a/vm-rust/src/player/eval.rs +++ b/vm-rust/src/player/eval.rs @@ -18,6 +18,7 @@ use crate::{ }, }; use crate::player::cast_lib::CastMemberRef; +use crate::player::handlers::movie::MovieHandlers; use super::{cast_lib::INVALID_CAST_MEMBER_REF, datum_formatting::format_datum, sprite::ColorRef, DatumRef, ScriptError}; #[derive(Parser)] @@ -279,50 +280,12 @@ pub fn eval_lingo_pair_static(pair: Pair) -> Result let member_id_ref = eval_lingo_pair_static(member_expr)?; // Optional: "of castLib X" - let cast_lib_ref = if let Some(castlib_expr) = inner.next() { - Some(eval_lingo_pair_static(castlib_expr)?) + if let Some(castlib_expr) = inner.next() { + let cast_lib_ref = eval_lingo_pair_static(castlib_expr)?; + MovieHandlers::member(&[member_id_ref, cast_lib_ref]) } else { - None - }; - - reserve_player_mut(|player| { - let member_id_datum = player.get_datum(&member_id_ref).clone(); - - // Get cast lib datum if specified - let cast_lib_datum = cast_lib_ref.as_ref().map(|r| player.get_datum(r).clone()); - - // Use find_member_ref_by_identifiers for proper member lookup - // This handles both string names and numeric member IDs - let member_result = player.movie.cast_manager.find_member_ref_by_identifiers( - &member_id_datum, - cast_lib_datum.as_ref(), - &player.allocator, - )?; - - let member_ref = match member_result { - Some(r) => r, - None => { - // If cast_lib was specified, create a ref with the specified values - // Otherwise return invalid ref - if let Some(cast_datum) = cast_lib_datum { - let cast_lib_num = match cast_datum { - Datum::Int(num) => num, - Datum::CastLib(num) => num as i32, - _ => return Err(ScriptError::new(format!( - "Expected int or castLib, got {:?}", - cast_datum.type_enum() - ))), - }; - let member_num = member_id_datum.int_value().unwrap_or(0); - super::cast_lib::cast_member_ref(cast_lib_num, member_num) - } else { - INVALID_CAST_MEMBER_REF - } - } - }; - - Ok(player.alloc_datum(Datum::CastMember(member_ref))) - }) + MovieHandlers::member(&[member_id_ref]) + } } Rule::castlib_ref => { let mut inner = pair.into_inner(); @@ -1435,51 +1398,12 @@ pub async fn eval_lingo_expr_ast_runtime(expr: &LingoExpr) -> Result r, - None => { - // If cast_lib was specified, create a ref with member 0 - // Otherwise return invalid ref - if let Some(cast_datum) = cast_lib_datum { - let cast_lib_num = match cast_datum { - Datum::Int(num) => num, - Datum::CastLib(num) => num as i32, - _ => return Err(ScriptError::new(format!( - "Expected int or castLib, got {:?}", - cast_datum.type_enum() - ))), - }; - // Try to get member number for explicit reference - let member_num = member_id_datum.int_value().unwrap_or(0); - super::cast_lib::cast_member_ref(cast_lib_num, member_num) - } else { - super::cast_lib::INVALID_CAST_MEMBER_REF - } - } - }; - - Ok(player.alloc_datum(Datum::CastMember(member_ref))) - }) + MovieHandlers::member(&[member_id_ref]) + } } LingoExpr::Identifier(ident_name) => { reserve_player_mut(|player| get_eval_top_level_prop(player, ident_name)) diff --git a/vm-rust/src/player/handlers/movie.rs b/vm-rust/src/player/handlers/movie.rs index f0e1b039a..8a3d8c36c 100644 --- a/vm-rust/src/player/handlers/movie.rs +++ b/vm-rust/src/player/handlers/movie.rs @@ -16,6 +16,7 @@ use crate::{ }, utils::{log_i}, }; +use crate::player::cast_lib::{cast_member_ref, CastLib}; pub struct MovieHandlers {} @@ -61,29 +62,30 @@ impl MovieHandlers { }) } - pub fn member(args: &Vec) -> Result { + pub fn member(args: &[DatumRef]) -> Result { reserve_player_mut(|player| { - if args.len() > 2 { - return Err(ScriptError::new( - "Too many arguments for member".to_string(), - )); - } - let member_name_or_num_ref = args.get(0).unwrap(); + let (member_name_or_num_ref, cast_name_or_num_ref) = match args { + [m, c] => (m, Some(c)), + [m] => (m, None), + _ => return Err(ScriptError::new( + "Invalid arguments for member".to_string(), + )), + }; let member_name_or_num = player.get_datum(member_name_or_num_ref); if let Datum::CastMember(_) = &member_name_or_num { return Ok(member_name_or_num_ref.clone()); } - let cast_name_or_num = args.get(1).map(|x| player.get_datum(x)); - let member = player.movie.cast_manager.find_member_ref_by_identifiers( + let cast_name_or_num = cast_name_or_num_ref.map(|x| player.get_datum(x)); + let cast = player.movie.cast_manager.find_cast_lib_by_identifier(cast_name_or_num); + let member = player.movie.cast_manager.find_member_ref_in_cast_by_identifier( member_name_or_num, - cast_name_or_num, - &player.allocator, + cast, )?; - if let Some(member) = member { - Ok(player.alloc_datum(Datum::CastMember(member.to_owned()))) - } else { - Ok(player.alloc_datum(Datum::CastMember(INVALID_CAST_MEMBER_REF))) - } + + Ok(player.alloc_datum(match member { + Some(r) => Datum::CastMember(r), + None => Datum::Void + })) }) } From eb2204f6439235a4c6bdacb675c39ae1f15f8447 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 28 Mar 2026 20:21:29 +0100 Subject: [PATCH 14/16] Add "browse" on web version --- src/views/LoadMovie/index.tsx | 204 ++++++++++++++++++++++++++++------ vm-rust/src/utils.rs | 2 +- 2 files changed, 172 insertions(+), 34 deletions(-) diff --git a/src/views/LoadMovie/index.tsx b/src/views/LoadMovie/index.tsx index 962e2b911..dbdcb9136 100644 --- a/src/views/LoadMovie/index.tsx +++ b/src/views/LoadMovie/index.tsx @@ -13,11 +13,34 @@ type RecentMovie = { url: string; params: ExternalParam[]; timestamp: number; + fileHandle?: BrowserFileHandle; }; -const RECENT_MOVIES_KEY = 'recentMovies'; +type BrowserFileHandle = { + kind: 'file'; + name: string; + queryPermission?: (descriptor?: { mode?: 'read' | 'readwrite' }) => Promise<'granted' | 'denied' | 'prompt'>; + requestPermission?: (descriptor?: { mode?: 'read' | 'readwrite' }) => Promise<'granted' | 'denied' | 'prompt'>; + getFile: () => Promise; +}; + +type ShowOpenFilePicker = (options?: { + multiple?: boolean; + excludeAcceptAllOption?: boolean; + types?: Array<{ + description?: string; + accept: Record; + }>; +}) => Promise; + +const RECENT_MOVIES_DB_NAME = 'dirplayer'; +const RECENT_MOVIES_STORE_NAME = 'recentMovies'; +const RECENT_MOVIES_DB_VERSION = 1; +const RECENT_MOVIES_RECORD_ID = 'movies'; const MAX_RECENT_MOVIES = 50; +let recentMoviesDbPromise: Promise | null = null; + function paramsArrayToRecord(params: ExternalParam[]): Record { const record: Record = {}; for (const p of params) { @@ -28,30 +51,74 @@ function paramsArrayToRecord(params: ExternalParam[]): Record { return record; } -function loadRecentMovies(): RecentMovie[] { +function openRecentMoviesDb(): Promise { + if (recentMoviesDbPromise) { + return recentMoviesDbPromise; + } + + recentMoviesDbPromise = new Promise((resolve, reject) => { + const request = window.indexedDB.open(RECENT_MOVIES_DB_NAME, RECENT_MOVIES_DB_VERSION); + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(RECENT_MOVIES_STORE_NAME)) { + db.createObjectStore(RECENT_MOVIES_STORE_NAME, { keyPath: 'id' }); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB')); + }); + + return recentMoviesDbPromise; +} + +async function loadRecentMoviesIndexedDb(): Promise { try { - const raw = window.localStorage.getItem(RECENT_MOVIES_KEY); - return raw ? JSON.parse(raw) : []; + const db = await openRecentMoviesDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(RECENT_MOVIES_STORE_NAME, 'readonly'); + const store = tx.objectStore(RECENT_MOVIES_STORE_NAME); + const request = store.get(RECENT_MOVIES_RECORD_ID); + + request.onsuccess = () => { + const result = request.result as { id: string; movies: RecentMovie[] } | undefined; + resolve(result?.movies ?? []); + }; + request.onerror = () => reject(request.error ?? new Error('Failed to read recent movies')); + }); } catch { return []; } } -function saveRecentMovie(url: string, params: ExternalParam[]): RecentMovie[] { - const existing = loadRecentMovies().filter(m => m.url !== url); - const updated = [{ url, params, timestamp: Date.now() }, ...existing].slice(0, MAX_RECENT_MOVIES); - window.localStorage.setItem(RECENT_MOVIES_KEY, JSON.stringify(updated)); +async function saveRecentMoviesIndexedDb(movies: RecentMovie[]): Promise { + const db = await openRecentMoviesDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(RECENT_MOVIES_STORE_NAME, 'readwrite'); + const store = tx.objectStore(RECENT_MOVIES_STORE_NAME); + const request = store.put({ id: RECENT_MOVIES_RECORD_ID, movies }); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error ?? new Error('Failed to write recent movies')); + }); +} + +async function saveRecentMovieIndexedDb(url: string, params: ExternalParam[], fileHandle?: BrowserFileHandle): Promise { + const existing = (await loadRecentMoviesIndexedDb()).filter(m => m.url !== url); + const updated = [{ url, params, timestamp: Date.now(), fileHandle }, ...existing].slice(0, MAX_RECENT_MOVIES); + await saveRecentMoviesIndexedDb(updated); return updated; } -function removeRecentMovie(url: string): RecentMovie[] { - const updated = loadRecentMovies().filter(m => m.url !== url); - window.localStorage.setItem(RECENT_MOVIES_KEY, JSON.stringify(updated)); +async function removeRecentMovieIndexedDb(url: string): Promise { + const updated = (await loadRecentMoviesIndexedDb()).filter(m => m.url !== url); + await saveRecentMoviesIndexedDb(updated); return updated; } -function clearRecentMovies(): RecentMovie[] { - window.localStorage.removeItem(RECENT_MOVIES_KEY); +async function clearRecentMoviesIndexedDb(): Promise { + await saveRecentMoviesIndexedDb([]); return []; } @@ -62,9 +129,24 @@ export default function LoadMovie() { const [hasError, setHasError] = useState(false); const [autoPlay, setAutoPlay] = useState(process.env.REACT_APP_MOVIE_AUTO_PLAY === 'true'); const [externalParams, setExternalParams] = useState([]); - const [recentMovies, setRecentMovies] = useState(() => loadRecentMovies()); + const [recentMovies, setRecentMovies] = useState([]); + const [selectedFileHandle, setSelectedFileHandle] = useState(undefined); const [paramsExpanded, setParamsExpanded] = useState(false); const isInElectron = isElectron(); + const showOpenFilePickerFn = (window as Window & { showOpenFilePicker?: ShowOpenFilePicker }).showOpenFilePicker; + const supportsWebBrowse = !isInElectron && typeof showOpenFilePickerFn === 'function'; + + const persistRecentMovie = useCallback(async (url: string, params: ExternalParam[], fileHandle?: BrowserFileHandle) => { + return saveRecentMovieIndexedDb(url, params, fileHandle); + }, []); + + const removeRecentMoviePersisted = useCallback(async (url: string) => { + return removeRecentMovieIndexedDb(url); + }, []); + + const clearRecentMoviesPersisted = useCallback(async () => { + return clearRecentMoviesIndexedDb(); + }, []); const addParam = useCallback(() => { setExternalParams(prev => [...prev, { key: '', value: '' }]); @@ -94,50 +176,100 @@ export default function LoadMovie() { } }, [autoPlay, externalParams]); + const loadMovieFromFileHandle = useCallback(async (fileHandle: BrowserFileHandle, params?: ExternalParam[]) => { + const currentPermission = await fileHandle.queryPermission?.({ mode: 'read' }); + if (currentPermission !== 'granted') { + const requestedPermission = await fileHandle.requestPermission?.({ mode: 'read' }); + if (requestedPermission !== 'granted') { + console.warn('[LoadMovie] Read permission was not granted for selected file handle'); + return; + } + } + + const file = await fileHandle.getFile(); + const objectUrl = URL.createObjectURL(file); + const urlForUi = file.name; + const updated = await persistRecentMovie(urlForUi, params ?? externalParams, fileHandle); + setRecentMovies(updated); + await loadMovieFile(objectUrl, params); + }, [externalParams, loadMovieFile, persistRecentMovie]); + const onLoadClick = useCallback(async () => { + if (!isInElectron && selectedFileHandle) { + await loadMovieFromFileHandle(selectedFileHandle, externalParams); + return; + } + if (!movieUrl.trim()) { setHasError(true); return; } - const updated = saveRecentMovie(movieUrl, externalParams); + const updated = await persistRecentMovie(movieUrl, externalParams); setRecentMovies(updated); await loadMovieFile(movieUrl); - }, [movieUrl, externalParams, loadMovieFile]); + }, [externalParams, isInElectron, loadMovieFile, loadMovieFromFileHandle, movieUrl, persistRecentMovie, selectedFileHandle]); const onBrowseClick = useCallback(async () => { - if (!isInElectron) return; try { - const filePath = await openFileDialog(); - if (filePath) { - setMovieUrl(`file://${filePath}`); + if (isInElectron) { + const filePath = await openFileDialog(); + if (filePath) { + setSelectedFileHandle(undefined); + setMovieUrl(`file://${filePath}`); + } + return; + } + + if (!showOpenFilePickerFn) { + return; + } + + const [fileHandle] = await showOpenFilePickerFn({ + multiple: false, + }); + + if (fileHandle) { + setSelectedFileHandle(fileHandle); + setMovieUrl(fileHandle.name); } } catch (e) { console.error('[LoadMovie] Failed to open file dialog', e); } - }, [isInElectron]); + }, [isInElectron, showOpenFilePickerFn]); - const onLoadRecent = useCallback((movie: RecentMovie) => { + const onLoadRecent = useCallback(async (movie: RecentMovie) => { setMovieUrl(movie.url); setExternalParams(movie.params); - const updated = saveRecentMovie(movie.url, movie.params); + if (!isInElectron && movie.fileHandle) { + setSelectedFileHandle(movie.fileHandle); + await loadMovieFromFileHandle(movie.fileHandle, movie.params); + return; + } + + setSelectedFileHandle(undefined); + const updated = await persistRecentMovie(movie.url, movie.params, movie.fileHandle); setRecentMovies(updated); - loadMovieFile(movie.url, movie.params); - }, [loadMovieFile]); + await loadMovieFile(movie.url, movie.params); + }, [isInElectron, loadMovieFile, loadMovieFromFileHandle, persistRecentMovie]); const onEditRecent = useCallback((movie: RecentMovie) => { setMovieUrl(movie.url); setExternalParams(movie.params); + setSelectedFileHandle(movie.fileHandle); if (movie.params.length > 0) { setParamsExpanded(true); } }, []); - const onRemoveRecent = useCallback((url: string) => { - setRecentMovies(removeRecentMovie(url)); - }, []); + const onRemoveRecent = useCallback(async (url: string) => { + setRecentMovies(await removeRecentMoviePersisted(url)); + }, [removeRecentMoviePersisted]); - const onClearRecent = useCallback(() => { - setRecentMovies(clearRecentMovies()); - }, []); + const onClearRecent = useCallback(async () => { + setRecentMovies(await clearRecentMoviesPersisted()); + }, [clearRecentMoviesPersisted]); useMountEffect(async () => { + const initialRecentMovies = await loadRecentMoviesIndexedDb(); + setRecentMovies(initialRecentMovies); + if (movieUrl && process.env.REACT_APP_MOVIE_AUTO_LOAD === 'true' && !isDebugSession()) { await loadMovieFile(movieUrl); } @@ -165,10 +297,16 @@ export default function LoadMovie() { className={`${styles.input} ${hasError ? styles.inputError : ''}`} placeholder={isInElectron ? '/path/to/movie.dcr' : 'https://example.com/movie.dcr'} value={movieUrl} - onChange={e => { setMovieUrl(e.currentTarget.value); setHasError(false); }} + onChange={e => { + setMovieUrl(e.currentTarget.value); + if (!isInElectron) { + setSelectedFileHandle(undefined); + } + setHasError(false); + }} disabled={isLoading} /> - {isInElectron && ( + {(isInElectron || supportsWebBrowse) && (