core: Allow playhead to advance past SWF head frame count

The frame count declared in the SWF header is not required to
match the actual number of frames present in the tag stream.
Flash Player will execute frames until it reaches the end of
the tag stream (reporting a `currentFrame` larger both `totalFrames`
and `framesLoaded`), and *then* jump back to the start.

We now inspect the tag stream in `determine_next_frame` - if we've
reached the end (either `TagCode::End` or we've run out of bytes),
we jump back to the beginning. The declared SWF frame count is ignored.

I haven't addressed gotos yet, which are more complicated due to
the 'frame clamping' behavior.
This commit is contained in:
Aaron Hill 2024-07-06 18:22:30 -04:00 committed by TÖRÖK Attila
parent 9f8b89168c
commit d8bc28f29e
9 changed files with 86 additions and 20 deletions

View File

@ -2322,7 +2322,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
} else {
self.target_clip()
.and_then(|dobj| dobj.as_movie_clip())
.map(|mc| mc.frames_loaded() >= min(frame_num, mc.total_frames()) as i32)
.map(|mc| mc.real_frames_loaded() >= min(frame_num, mc.total_frames()) as i32)
.unwrap_or(true)
};
@ -2365,7 +2365,9 @@ impl<'a, 'gc> Activation<'a, 'gc> {
// `ifFrameLoaded(_framesloaded + 1)` always evaluates to true (off-by-one).
self.target_clip()
.and_then(|dobj| dobj.as_movie_clip())
.map(|mc| mc.frames_loaded() + 1 >= min(frame_num as u16, mc.total_frames()) as i32)
.map(|mc| {
mc.real_frames_loaded() + 1 >= min(frame_num as u16, mc.total_frames()) as i32
})
.unwrap_or(true)
};

View File

@ -658,7 +658,7 @@ fn frames_loaded<'gc>(
this: DisplayObject<'gc>,
) -> Value<'gc> {
this.as_movie_clip()
.map(MovieClip::frames_loaded)
.map(MovieClip::frames_loaded_for_avm)
.map_or(Value::Undefined, Value::from)
}

View File

@ -278,7 +278,7 @@ pub fn get_frames_loaded<'gc>(
.as_display_object()
.and_then(|dobj| dobj.as_movie_clip())
{
return Ok(mc.frames_loaded().into());
return Ok(mc.frames_loaded_for_avm().into());
}
Ok(Value::Undefined)

View File

@ -55,7 +55,7 @@ use super::BitmapClass;
type FrameNumber = u16;
/// Indication of what frame `run_frame` should jump to next.
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Debug)]
enum NextFrame {
/// Construct and run the next frame in the clip.
Next,
@ -815,9 +815,9 @@ impl<'gc> MovieClip<'gc> {
(reader.get_ref().as_ptr() as u64).saturating_sub(data.data().as_ptr() as u64)
};
write.cur_preload_frame = if is_finished {
// Flag the movie as fully preloaded when we hit the end of the
// tag stream.
static_data.total_frames + 1
// Use `cur_frame` instead of `static_data.total_frames`, since the header
// might declare fewer frames than we actually have
cur_frame + 1
} else {
cur_frame
};
@ -1276,10 +1276,18 @@ impl<'gc> MovieClip<'gc> {
self.0.write(gc_context).current_frame = current_frame;
}
pub fn frames_loaded(self) -> i32 {
// Gets the actual number of frames loaded (ignoring the SWF header)
pub fn real_frames_loaded(self) -> i32 {
self.0.read().frames_loaded()
}
// Gets the number of frames loaded, clamping it to the number of frames
// declared in the SWF header. This is the value we report to AVM code
// to match Flash Player's behavior
pub fn frames_loaded_for_avm(self) -> i32 {
self.real_frames_loaded().min(self.total_frames() as i32)
}
pub fn total_bytes(self) -> i32 {
// For a loaded SWF, returns the uncompressed size of the SWF.
// Otherwise, returns the size of the tag list in the clip's DefineSprite tag.
@ -1470,12 +1478,25 @@ impl<'gc> MovieClip<'gc> {
/// Determine what the clip's next frame should be.
fn determine_next_frame(self) -> NextFrame {
if self.current_frame() < self.total_frames() {
let mc = self.0.read();
let mut reader = mc.static_data.swf.read_from(mc.tag_stream_pos);
// We ignore the frame count from the header, and instead continue
// until we reach the end of the stream or a `TagCode::End`.
// Flash Player ignores the frame count, and just executes the full
// tag stream before returning to the first frame.
if !reader.as_slice().is_empty()
&& reader.read_tag_code().expect("Failed to read tag") != TagCode::End as u16
{
NextFrame::Next
} else if self.total_frames() > 1 {
NextFrame::First
} else {
// The `current_frame` can be larger than `total_frames` if the SWF header
// declared fewer frames than we actually have. We only stop the swf if there
// was *really* at most a single frame (we declared at most 1 frame, and reached the end
// of the stream after executing 0 or 1 frames)
} else if self.total_frames() <= 1 && self.current_frame() <= 1 {
NextFrame::Same
} else {
NextFrame::First
}
}
@ -1507,7 +1528,7 @@ impl<'gc> MovieClip<'gc> {
let mc = self.0.read();
let tag_stream_start = mc.static_data.swf.as_ref().as_ptr() as u64;
let data = mc.static_data.swf.clone();
let data: SwfSlice = mc.static_data.swf.clone();
let mut reader = data.read_from(mc.tag_stream_pos);
drop(mc);
@ -1815,7 +1836,8 @@ impl<'gc> MovieClip<'gc> {
let mut index = 0;
// Sanity; let's make sure we don't seek way too far.
let clamped_frame = frame.min(max(mc.frames_loaded(), 0) as FrameNumber);
// FIXME - properly handle seeking to a frame greater than the SWF header frame count
let clamped_frame = frame.min(max(self.frames_loaded_for_avm(), 0) as FrameNumber);
drop(mc);
let mut removed_frame_scripts: Vec<DisplayObject<'gc>> = vec![];
@ -2592,7 +2614,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
// AVM1 code expects to execute in line with timeline instructions, so
// it's exempted from frame construction.
if self.movie().is_action_script_3()
&& (self.frames_loaded() >= 1 || self.total_frames() == 0)
&& (self.real_frames_loaded() >= 1 || self.total_frames() == 0)
{
let is_load_frame = !self.0.read().initialized();
let needs_construction = if matches!(self.object2(), Avm2Value::Null) {
@ -3395,10 +3417,7 @@ impl<'gc> MovieClipData<'gc> {
}
fn play(&mut self) {
// Can only play clips with multiple frames.
if self.total_frames() > 1 {
self.set_playing(true);
}
self.set_playing(true);
}
fn stop(&mut self, context: &mut UpdateContext<'gc>) {

View File

@ -714,6 +714,10 @@ impl<'a> Reader<'a> {
Ok(tags)
}
pub fn read_tag_code(&mut self) -> Result<u16> {
Ok(self.read_u16()? >> 6)
}
pub fn read_tag_code_and_length(&mut self) -> Result<(u16, usize)> {
let tag_code_and_length = self.read_u16()?;
let tag_code = tag_code_and_length >> 6;

View File

@ -0,0 +1,38 @@
///Parent frame 1
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 1
///End parent frame 1
///Child frame 1
///Parent frame 2
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 2
///Parent frame 3
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 3
///Child frame 3
///End child frame 3
///Parent frame 4
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 4
///Child frame 4
///Parent frame 5
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 5
///End parent frame 5
///Child frame 5
///Parent frame 1
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 1
///End parent frame 1
///Child frame 1
///Parent frame 2
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 2
///Parent frame 3
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 3
///Child frame 3
///End child frame 3
///Parent frame 4
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 4
///Child frame 4
///Parent frame 5
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 5
///End parent frame 5
///Child frame 5
///Parent frame 1
///Parent framesLoaded = 1 totalFrames = 1 currentFrame = 1
///End parent frame 1
///Child frame 1

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
# WARNING - the 'test.swf' file has been manually edited
# in JPEXS to change the frame count
num_ticks = 11