avm2: Implement `MovieClip.scenes`.

This commit is contained in:
David Wendt 2020-09-18 23:09:27 -04:00 committed by Mike Welsh
parent c682868205
commit 8152400a39
7 changed files with 218 additions and 53 deletions

View File

@ -11,7 +11,7 @@ use crate::avm2::string::AvmString;
use crate::avm2::traits::Trait; use crate::avm2::traits::Trait;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::Error; use crate::avm2::Error;
use crate::display_object::{MovieClip, TDisplayObject}; use crate::display_object::{MovieClip, Scene, TDisplayObject};
use gc_arena::{GcCell, MutationContext}; use gc_arena::{GcCell, MutationContext};
/// Implements `flash.display.MovieClip`'s instance constructor. /// Implements `flash.display.MovieClip`'s instance constructor.
@ -66,8 +66,13 @@ pub fn current_frame<'gc>(
.and_then(|o| o.as_display_object()) .and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip()) .and_then(|dobj| dobj.as_movie_clip())
{ {
if let Some((_scene, scene_basis, _length)) = mc.current_scene() { if let Some(Scene {
return Ok(((mc.current_frame() + 1) - scene_basis).into()); name: _,
start,
length: _,
}) = mc.current_scene()
{
return Ok(((mc.current_frame() + 1) - start).into());
} else { } else {
return Ok(mc.current_frame().into()); return Ok(mc.current_frame().into());
} }
@ -122,16 +127,24 @@ pub fn current_label<'gc>(
Ok(Value::Undefined) Ok(Value::Undefined)
} }
/// Given a scene, produce it's name, length, and a list of frame labels.
///
/// The intended purpose of this output is to be sent directly into the
/// constructor of `flash.display.Scene`.
fn labels_for_scene<'gc>( fn labels_for_scene<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
mc: MovieClip<'gc>, mc: MovieClip<'gc>,
scene: &Scene,
) -> Result<(String, u16, Object<'gc>), Error> { ) -> Result<(String, u16, Object<'gc>), Error> {
let (scene_name, scene_start, scene_length) = let Scene {
mc.current_scene().unwrap_or(("".to_string(), 0, u16::MAX)); name: scene_name,
start: scene_start,
length: scene_length,
} = scene;
let mut frame_labels = Vec::new(); let mut frame_labels = Vec::new();
let frame_label_proto = activation.context.avm2.prototypes().framelabel; let frame_label_proto = activation.context.avm2.prototypes().framelabel;
for (name, frame) in mc.labels_in_range(scene_start, scene_start + scene_length) { for (name, frame) in mc.labels_in_range(*scene_start, scene_start + scene_length) {
let name: Value<'gc> = AvmString::new(activation.context.gc_context, name).into(); let name: Value<'gc> = AvmString::new(activation.context.gc_context, name).into();
let local_frame = frame - scene_start + 1; let local_frame = frame - scene_start + 1;
let args = [name, local_frame.into()]; let args = [name, local_frame.into()];
@ -143,8 +156,8 @@ fn labels_for_scene<'gc>(
} }
Ok(( Ok((
scene_name, scene_name.to_string(),
scene_length, *scene_length,
ArrayObject::from_array( ArrayObject::from_array(
ArrayStorage::from_storage(frame_labels), ArrayStorage::from_storage(frame_labels),
activation.context.avm2.prototypes().array, activation.context.avm2.prototypes().array,
@ -163,7 +176,8 @@ pub fn current_labels<'gc>(
.and_then(|o| o.as_display_object()) .and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip()) .and_then(|dobj| dobj.as_movie_clip())
{ {
return Ok(labels_for_scene(activation, mc)?.2.into()); let scene = mc.current_scene().unwrap_or_else(Default::default);
return Ok(labels_for_scene(activation, mc, &scene)?.2.into());
} }
Ok(Value::Undefined) Ok(Value::Undefined)
@ -179,7 +193,8 @@ pub fn current_scene<'gc>(
.and_then(|o| o.as_display_object()) .and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip()) .and_then(|dobj| dobj.as_movie_clip())
{ {
let (scene_name, scene_length, scene_labels) = labels_for_scene(activation, mc)?; let scene = mc.current_scene().unwrap_or_else(Default::default);
let (scene_name, scene_length, scene_labels) = labels_for_scene(activation, mc, &scene)?;
let scene_proto = activation.context.avm2.prototypes().scene; let scene_proto = activation.context.avm2.prototypes().scene;
let args = [ let args = [
AvmString::new(activation.context.gc_context, scene_name).into(), AvmString::new(activation.context.gc_context, scene_name).into(),
@ -197,6 +212,46 @@ pub fn current_scene<'gc>(
Ok(Value::Undefined) Ok(Value::Undefined)
} }
/// Implements `scenes`.
pub fn scenes<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(mc) = this
.and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip())
{
let mut scene_objects = Vec::new();
for scene in mc.scenes() {
let (scene_name, scene_length, scene_labels) =
labels_for_scene(activation, mc, &scene)?;
let scene_proto = activation.context.avm2.prototypes().scene;
let args = [
AvmString::new(activation.context.gc_context, scene_name).into(),
scene_labels.into(),
scene_length.into(),
];
let scene = scene_proto.construct(activation, &args)?;
scene::instance_init(activation, Some(scene), &args)?;
scene_objects.push(Some(scene.into()));
}
return Ok(ArrayObject::from_array(
ArrayStorage::from_storage(scene_objects),
activation.context.avm2.prototypes().array,
activation.context.gc_context,
)
.into());
}
Ok(Value::Undefined)
}
/// Implements `framesLoaded`. /// Implements `framesLoaded`.
pub fn frames_loaded<'gc>( pub fn frames_loaded<'gc>(
_activation: &mut Activation<'_, 'gc, '_>, _activation: &mut Activation<'_, 'gc, '_>,
@ -398,8 +453,13 @@ pub fn prev_scene<'gc>(
.and_then(|o| o.as_display_object()) .and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip()) .and_then(|dobj| dobj.as_movie_clip())
{ {
if let Some((_scene, target_frame, _length)) = mc.previous_scene() { if let Some(Scene {
mc.goto_frame(&mut activation.context, target_frame, false); name: _,
start,
length: _,
}) = mc.previous_scene()
{
mc.goto_frame(&mut activation.context, start, false);
} }
} }
@ -416,8 +476,13 @@ pub fn next_scene<'gc>(
.and_then(|o| o.as_display_object()) .and_then(|o| o.as_display_object())
.and_then(|dobj| dobj.as_movie_clip()) .and_then(|dobj| dobj.as_movie_clip())
{ {
if let Some((_scene, target_frame, _length)) = mc.next_scene() { if let Some(Scene {
mc.goto_frame(&mut activation.context, target_frame, false); name: _,
start,
length: _,
}) = mc.next_scene()
{
mc.goto_frame(&mut activation.context, start, false);
} }
} }
@ -466,6 +531,11 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
Method::from_builtin(current_scene), Method::from_builtin(current_scene),
)); ));
write.define_instance_trait(Trait::from_getter(
QName::new(Namespace::package(""), "scenes"),
Method::from_builtin(scenes),
));
write.define_instance_trait(Trait::from_getter( write.define_instance_trait(Trait::from_getter(
QName::new(Namespace::package(""), "framesLoaded"), QName::new(Namespace::package(""), "framesLoaded"),
Method::from_builtin(frames_loaded), Method::from_builtin(frames_loaded),

View File

@ -30,7 +30,7 @@ pub use button::Button;
pub use edit_text::{AutoSizeMode, EditText}; pub use edit_text::{AutoSizeMode, EditText};
pub use graphic::Graphic; pub use graphic::Graphic;
pub use morph_shape::{MorphShape, MorphShapeStatic}; pub use morph_shape::{MorphShape, MorphShapeStatic};
pub use movie_clip::MovieClip; pub use movie_clip::{MovieClip, Scene};
pub use text::Text; pub use text::Text;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@ -554,9 +554,6 @@ impl<'gc> MovieClip<'gc> {
static_data: &mut MovieClipStatic, static_data: &mut MovieClipStatic,
) -> DecodeResult { ) -> DecodeResult {
let mut sfl_data = reader.read_define_scene_and_frame_label_data()?; let mut sfl_data = reader.read_define_scene_and_frame_label_data()?;
sfl_data
.frame_labels
.sort_unstable_by(|s1, s2| s1.frame_num.cmp(&s2.frame_num));
sfl_data sfl_data
.scenes .scenes
.sort_unstable_by(|s1, s2| s1.frame_num.cmp(&s2.frame_num)); .sort_unstable_by(|s1, s2| s1.frame_num.cmp(&s2.frame_num));
@ -640,60 +637,136 @@ impl<'gc> MovieClip<'gc> {
self.0.read().current_frame self.0.read().current_frame
} }
pub fn current_scene(self) -> Option<(String, FrameNumber, FrameNumber)> { /// Return the current scene.
pub fn current_scene(self) -> Option<Scene> {
let current_frame = self.0.read().current_frame(); let current_frame = self.0.read().current_frame();
self.filter_scenes(|best, (_scene, frame, _end)| { self.filter_scenes(
frame <= current_frame && best.map(|v| frame >= v.1).unwrap_or(true) |best,
}) Scene {
name: _,
start,
length: _,
}| {
*start <= current_frame
&& best
.map(
|Scene {
name: _,
start: best_start,
length: _,
}| start >= best_start,
)
.unwrap_or(true)
},
)
} }
pub fn previous_scene(self) -> Option<(String, FrameNumber, FrameNumber)> { /// Return the previous scene.
pub fn previous_scene(self) -> Option<Scene> {
let current_frame = self let current_frame = self
.current_scene() .current_scene()
.map(|v| v.1) .map(
|Scene {
name: _,
start,
length: _,
}| start,
)
.unwrap_or_else(|| self.current_frame()); .unwrap_or_else(|| self.current_frame());
self.filter_scenes(|best, (_scene, frame, _end)| { self.filter_scenes(
frame < current_frame && best.map(|v| frame >= v.1).unwrap_or(true) |best,
}) Scene {
name: _,
start,
length: _,
}| {
*start < current_frame
&& best
.map(
|Scene {
name: _,
start: best_start,
length: _,
}| start >= best_start,
)
.unwrap_or(true)
},
)
} }
pub fn next_scene(self) -> Option<(String, FrameNumber, FrameNumber)> { /// Return the next scene.
pub fn next_scene(self) -> Option<Scene> {
let current_frame = self.0.read().current_frame(); let current_frame = self.0.read().current_frame();
self.filter_scenes(|best, (_scene, frame, _end)| { self.filter_scenes(
frame > current_frame && best.map(|v| frame <= v.1).unwrap_or(true) |best,
}) Scene {
name: _,
start,
length: _,
}| {
*start > current_frame
&& best
.map(
|Scene {
name: _,
start: best_start,
length: _,
}| start <= best_start,
)
.unwrap_or(true)
},
)
} }
pub fn filter_scenes<F>(self, mut cond: F) -> Option<(String, FrameNumber, FrameNumber)> /// Return all scenes in the movie.
///
/// Scenes will be sorted in playback order.
pub fn scenes(self) -> Vec<Scene> {
let read = self.0.read();
let mut out = Vec::new();
for (_, scene) in read.static_data.scene_labels.iter() {
out.push(scene.clone());
}
out.sort_unstable_by(
|Scene {
name: _,
start: a,
length: _,
},
Scene {
name: _,
start: b,
length: _,
}| a.cmp(b),
);
out
}
/// Scan through the list of scenes and yield the best one, if available,
/// according to a given criterion function.
fn filter_scenes<F>(self, mut cond: F) -> Option<Scene>
where where
F: FnMut( F: FnMut(Option<&Scene>, &Scene) -> bool,
Option<(&str, FrameNumber, FrameNumber)>,
(&str, FrameNumber, FrameNumber),
) -> bool,
{ {
let read = self.0.read(); let read = self.0.read();
let mut best: Option<(&str, FrameNumber, FrameNumber)> = None; let mut best: Option<&Scene> = None;
for ( for (_, scene) in read.static_data.scene_labels.iter() {
_, if cond(best, scene) {
Scene { best = Some(scene);
name,
start,
length,
},
) in read.static_data.scene_labels.iter()
{
if cond(best, (name, *start, *length)) {
best = Some((name, *start, *length));
} }
} }
best.map(|(s, fnum, len)| (s.to_string(), fnum, len)) best.cloned()
} }
/// Yield the current frame label as a tuple of string and frame number.
pub fn current_label(self) -> Option<(String, FrameNumber)> { pub fn current_label(self) -> Option<(String, FrameNumber)> {
let read = self.0.read(); let read = self.0.read();
let current_frame = read.current_frame(); let current_frame = read.current_frame();
@ -2863,10 +2936,20 @@ impl<'gc, 'a> MovieClip<'gc> {
} }
#[derive(Clone)] #[derive(Clone)]
struct Scene { pub struct Scene {
name: String, pub name: String,
start: FrameNumber, pub start: FrameNumber,
length: FrameNumber, pub length: FrameNumber,
}
impl Default for Scene {
fn default() -> Self {
Scene {
name: "".to_string(),
start: 0,
length: u16::MAX,
}
}
} }
/// Static data shared between all instances of a movie clip. /// Static data shared between all instances of a movie clip.

View File

@ -397,6 +397,7 @@ swf_tests! {
(as3_movieclip_currentlabels, "avm2/movieclip_currentlabels", 5), (as3_movieclip_currentlabels, "avm2/movieclip_currentlabels", 5),
(as3_scene_constr, "avm2/scene_constr", 5), (as3_scene_constr, "avm2/scene_constr", 5),
(as3_movieclip_currentscene, "avm2/movieclip_currentscene", 5), (as3_movieclip_currentscene, "avm2/movieclip_currentscene", 5),
(as3_movieclip_scenes, "avm2/movieclip_scenes", 5),
} }
// TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.

View File

@ -0,0 +1,11 @@
//(contents of this.scenes)
Scene 1
3
1
frame1
3
frame3
Scene 2
1
1
frame4

Binary file not shown.

Binary file not shown.