Flash/Core/MovieClip.cs
using System;
using System.Collections.Generic;
namespace Flash;
/// <summary>One depth on a timeline: what the SWF tags say should be there.</summary>
public sealed class Slot
{
public int CharId;
public Matrix M = Matrix.Identity;
public ColorTransform Cx = ColorTransform.Identity;
public string Name;
public int Ratio;
public int ClipDepth;
public List<Filter> Filters;
public int BlendMode;
public List<ClipActionDef> ClipActions;
public int PlacedFrame;
public int Order; // tag order within its frame (for construction order)
public Slot Clone() => new()
{
CharId = CharId, M = M, Cx = Cx, Name = Name, Ratio = Ratio, ClipDepth = ClipDepth, Filters = Filters,
BlendMode = BlendMode, ClipActions = ClipActions, PlacedFrame = PlacedFrame, Order = Order,
};
}
/// <summary>A movie clip: a timeline with child display objects and an AS object.</summary>
public sealed class MovieClip : DisplayObject
{
public const int TimelineDepthOffset = -16384;
public SpriteDef Sprite;
public int CurrentFrame; // 0 = not yet run
public bool Playing = true;
public readonly List<DisplayObject> Children = new();
Dictionary<int, Slot> slots = new();
public List<ClipActionDef> ClipActions;
public bool Enabled = true, UseHandCursor = true, TrackAsMenu;
public bool Initialized; // load event fired
public MovieClip NextExec; // execution list link
public bool InExecList;
public AsFunction RegisteredClass;
public int InstanceCounter;
public MovieClip( Player p, SpriteDef def ) : base( p, p.Vm.MovieClipProto )
{
Sprite = def ?? new SpriteDef { FrameCount = 1, Frames = new[] { new List<FrameOp>() } };
Def = def;
}
public int TotalFrames => Math.Max( 1, Sprite.FrameCount );
public override bool HasOwnToString => false;
// ================================================================== children
public DisplayObject ChildAtDepth( int depth )
{
foreach ( var c in Children ) if ( c.Depth == depth ) return c;
return null;
}
public DisplayObject ChildByName( string name )
{
if ( string.IsNullOrEmpty( name ) ) return null;
// Flash returns the lowest-depth child with the name
foreach ( var c in Children ) if ( !c.Removed && c.Name == name ) return c;
return null;
}
public void AddChild( DisplayObject d, int depth )
{
var old = ChildAtDepth( depth );
if ( old != null ) RemoveChild( old );
d.Parent = this;
d.Depth = depth;
Player.ApplyLayout( d );
int i = 0;
while ( i < Children.Count && Children[i].Depth < depth ) i++;
Children.Insert( i, d );
}
public void RemoveChild( DisplayObject d )
{
if ( !Children.Remove( d ) ) return;
Player.Unload( d );
}
public void SwapDepths( int depth )
{
if ( Parent == null ) return;
var p = Parent;
if ( depth == Depth ) return;
var other = p.ChildAtDepth( depth );
if ( ReferenceEquals( other, this ) ) return;
int myDepth = Depth;
p.Children.Remove( this );
if ( other != null )
{
p.Children.Remove( other );
other.Depth = myDepth;
other.TimelineDepth = -1;
Insert( p, other );
}
Depth = depth;
TimelineDepth = -1;
Insert( p, this );
}
static void Insert( MovieClip p, DisplayObject d )
{
int i = 0;
while ( i < p.Children.Count && p.Children[i].Depth < d.Depth ) i++;
p.Children.Insert( i, d );
}
public int NextHighestDepth()
{
int max = -1;
foreach ( var c in Children ) if ( c.Depth > max ) max = c.Depth;
return Math.Max( 0, max + 1 );
}
// ================================================================== timeline
public int FrameOfLabel( string label )
{
if ( Sprite.Labels.TryGetValue( label, out var f ) ) return f;
foreach ( var kv in Sprite.Labels )
if ( string.Equals( kv.Key, label, StringComparison.OrdinalIgnoreCase ) ) return kv.Value;
return 0;
}
public IEnumerable<byte[]> FrameScripts( int frame )
{
if ( frame < 1 || frame > Sprite.Frames.Length ) yield break;
foreach ( var op in Sprite.Frames[frame - 1] ) if ( op.Kind == 'A' ) yield return op.Code;
}
public void Play() => Playing = true;
public void Stop() => Playing = false;
public void NextFrame()
{
if ( CurrentFrame < TotalFrames ) Seek( CurrentFrame + 1, true );
Playing = false;
}
public void PrevFrame()
{
if ( CurrentFrame > 1 ) Seek( CurrentFrame - 1, true );
Playing = false;
}
public void GotoFrame( int frame, bool stop )
{
if ( frame < 1 ) frame = 1;
if ( frame > TotalFrames ) frame = TotalFrames;
Playing = !stop;
if ( frame == CurrentFrame ) return;
Seek( frame, true );
}
public void GotoLabel( string label, bool stop )
{
int f = FrameOfLabel( label );
if ( f <= 0 ) { Playing = !stop && Playing; return; }
GotoFrame( f, stop );
}
/// <summary>Advance one frame (called by the player for playing clips).</summary>
public void AdvanceFrame()
{
if ( TotalFrames <= 1 && CurrentFrame >= 1 ) return;
int next = CurrentFrame + 1;
if ( next > TotalFrames ) next = 1;
Seek( next, true );
}
/// <summary>Moves the timeline to a frame: works out the display list the tags describe and makes the children match.</summary>
public void Seek( int target, bool runActions )
{
if ( target < 1 ) target = 1;
Dictionary<int, Slot> ns;
int from;
if ( CurrentFrame >= 1 && target > CurrentFrame )
{
ns = new Dictionary<int, Slot>( slots.Count );
foreach ( var kv in slots ) ns[kv.Key] = kv.Value;
from = CurrentFrame + 1;
}
else
{
ns = new Dictionary<int, Slot>();
from = 1;
}
for ( int f = from; f <= target; f++ ) ApplyOps( ns, f );
// --- reconcile
var created = new List<(DisplayObject obj, Slot slot)>();
// remove / update existing timeline children
for ( int i = Children.Count - 1; i >= 0; i-- )
{
var c = Children[i];
if ( c.TimelineDepth < 0 ) continue;
if ( !ns.TryGetValue( c.TimelineDepth, out var s ) )
{
Children.RemoveAt( i );
Player.Unload( c );
continue;
}
if ( c.Def != null && c.Def.Id == s.CharId && c.PlacedFrame == s.PlacedFrame )
{
if ( !c.ScriptTransformed )
{
c.SetMatrixFromTimeline( s.M );
c.Cx = s.Cx;
}
c.Ratio = s.Ratio;
c.Filters = s.Filters;
c.ClipDepth = s.ClipDepth;
c.BlendMode = s.BlendMode;
continue;
}
Children.RemoveAt( i );
Player.Unload( c );
}
bool rebuild = from == 1;
foreach ( var kv in ns )
{
int depth = kv.Key + TimelineDepthOffset;
var existing = ChildAtDepth( depth );
if ( existing != null && existing.TimelineDepth == kv.Key ) continue;
if ( existing != null )
{
// a script object sits at the timeline depth: Flash keeps the script object
continue;
}
var s = kv.Value;
// when playing forward, only tags in the frames just played create objects
// (an object the script moved away with swapDepths is not re-placed)
if ( !rebuild && s.PlacedFrame < from ) continue;
var obj = Player.Instantiate( s.CharId, this );
if ( obj == null ) continue;
obj.FromTimeline = true;
obj.TimelineDepth = kv.Key;
obj.PlacedFrame = s.PlacedFrame;
obj.SetMatrixFromTimeline( s.M );
obj.Cx = s.Cx;
obj.Ratio = s.Ratio;
obj.Filters = s.Filters;
obj.ClipDepth = s.ClipDepth;
obj.BlendMode = s.BlendMode;
obj.Name = s.Name ?? Player.NextInstanceName();
if ( obj is MovieClip mc ) mc.ClipActions = s.ClipActions;
AddChild( obj, depth );
created.Add( (obj, s) );
}
slots = ns;
CurrentFrame = target;
// construct the new objects, then this frame's scripts, in tag order
created.Sort( ( a, b ) => a.slot.PlacedFrame != b.slot.PlacedFrame ? a.slot.PlacedFrame.CompareTo( b.slot.PlacedFrame ) : a.slot.Order.CompareTo( b.slot.Order ) );
var ops = Sprite.Frames.Length >= target ? Sprite.Frames[target - 1] : null;
int ci = 0;
// objects placed on earlier (skipped) frames come first
while ( ci < created.Count && created[ci].slot.PlacedFrame < target ) Player.Construct( created[ci++].obj );
if ( ops != null )
{
int order = 0;
foreach ( var op in ops )
{
order++;
if ( op.Kind == 'A' ) { if ( runActions ) Player.QueueAction( this, op.Code ); }
else if ( op.Kind == 'S' ) Player.StartSound( op.SoundId, op.SoundStop, op.SoundLoops, this, op.SoundNoMultiple );
else if ( op.Kind == 'P' )
{
while ( ci < created.Count && created[ci].slot.PlacedFrame == target && created[ci].slot.Order <= order ) Player.Construct( created[ci++].obj );
}
}
}
while ( ci < created.Count ) Player.Construct( created[ci++].obj );
}
void ApplyOps( Dictionary<int, Slot> ns, int frame )
{
if ( frame < 1 || frame > Sprite.Frames.Length ) return;
int order = 0;
foreach ( var op in Sprite.Frames[frame - 1] )
{
order++;
switch ( op.Kind )
{
case 'P':
{
ns.TryGetValue( op.Depth, out var old );
Slot s;
if ( op.Move && old != null )
{
// replace the character at this depth, keeping unspecified properties
s = old.Clone();
s.CharId = op.CharId;
s.PlacedFrame = frame;
s.Order = order;
}
else
{
s = new Slot { CharId = op.CharId, PlacedFrame = frame, Order = order };
}
if ( op.M.HasValue ) s.M = op.M.Value;
if ( op.Cx.HasValue ) s.Cx = op.Cx.Value;
if ( op.Name != null ) s.Name = op.Name;
if ( op.Ratio.HasValue ) s.Ratio = op.Ratio.Value;
if ( op.ClipDepth > 0 ) s.ClipDepth = op.ClipDepth;
if ( op.HasFilters ) s.Filters = op.Filters;
if ( op.BlendMode != 0 ) s.BlendMode = op.BlendMode;
if ( op.ClipActions != null ) s.ClipActions = op.ClipActions;
ns[op.Depth] = s;
break;
}
case 'M':
{
if ( !ns.TryGetValue( op.Depth, out var old ) ) break;
var s = old.Clone();
if ( op.M.HasValue ) s.M = op.M.Value;
if ( op.Cx.HasValue ) s.Cx = op.Cx.Value;
if ( op.Ratio.HasValue ) s.Ratio = op.Ratio.Value;
if ( op.HasFilters ) s.Filters = op.Filters;
if ( op.ClipDepth > 0 ) s.ClipDepth = op.ClipDepth;
if ( op.BlendMode != 0 ) s.BlendMode = op.BlendMode;
ns[op.Depth] = s;
break;
}
case 'R':
ns.Remove( op.Depth );
break;
}
}
}
// ================================================================== bounds & hit testing
public override (double x0, double y0, double x1, double y1)? LocalBounds()
{
(double, double, double, double)? r = null;
foreach ( var c in Children )
{
if ( c.Removed ) continue;
var b = c.BoundsIn( c.Matrix );
if ( b is null ) continue;
r = r is null ? b : (Math.Min( r.Value.Item1, b.Value.x0 ), Math.Min( r.Value.Item2, b.Value.y0 ), Math.Max( r.Value.Item3, b.Value.x1 ), Math.Max( r.Value.Item4, b.Value.y1 ));
}
return r;
}
public override bool HitLocal( double lx, double ly, bool shapeOnly )
{
for ( int i = Children.Count - 1; i >= 0; i-- )
{
var c = Children[i];
if ( c.Removed || c.ClipDepth > 0 ) continue;
if ( c.HitChild( lx, ly, shapeOnly ) ) return true;
}
return false;
}
/// <summary>Does this clip act like a button (has mouse handlers)?</summary>
public bool HasButtonHandlers =>
Has( "onRelease" ) || Has( "onPress" ) || Has( "onRollOver" ) || Has( "onRollOut" ) || Has( "onReleaseOutside" ) || Has( "onDragOut" ) || Has( "onDragOver" );
// ================================================================== AS properties
protected override bool TryGetNative( string name, Avm1 vm, out Value v )
{
if ( name.Length > 0 && name[0] == '_' )
{
if ( name == "_currentframe" ) { v = Math.Max( 1, CurrentFrame ); return true; }
if ( name == "_totalframes" || name == "_framesloaded" ) { v = TotalFrames; return true; }
if ( GetDisplayProperty( name, vm, out v ) ) return true;
}
var child = ChildByName( name );
if ( child != null ) { v = Value.From( child ); return true; }
switch ( name )
{
case "enabled": v = Enabled; return true;
case "useHandCursor": v = UseHandCursor; return true;
case "trackAsMenu": v = TrackAsMenu; return true;
}
v = default;
return false;
}
protected override bool TrySetNative( string name, Value v, Avm1 vm )
{
if ( name.Length > 0 && name[0] == '_' && SetDisplayProperty( name, v, vm ) ) return true;
switch ( name )
{
case "enabled": Enabled = v.ToBool( vm.SwfVersion ); return true;
case "useHandCursor": UseHandCursor = v.ToBool( vm.SwfVersion ); return true;
case "trackAsMenu": TrackAsMenu = v.ToBool( vm.SwfVersion ); return true;
}
return false;
}
protected override bool HasNativeProperty( string name ) => IsDisplayProperty( name ) || ChildByName( name ) != null;
public override IEnumerable<string> NativeKeys
{
get
{
for ( int i = Children.Count - 1; i >= 0; i-- )
if ( !Children[i].Removed && !string.IsNullOrEmpty( Children[i].Name ) && (Children[i] is MovieClip || Children[i] is EditText || Children[i] is ButtonObj) )
yield return Children[i].Name;
}
}
// ================================================================== script-side creation
public override void Duplicate( string name, int depth )
{
if ( Parent == null ) return;
var copy = Player.Instantiate( Def?.Id ?? -1, Parent ) as MovieClip ?? new MovieClip( Player, Sprite );
copy.Name = name;
copy.SetMatrixFromTimeline( Matrix );
copy.Cx = Cx;
copy.Filters = Filters;
copy.ClipActions = ClipActions;
copy.Visible = Visible;
Parent.AddChild( copy, depth );
Player.Construct( copy );
}
public override void OnRemoved()
{
base.OnRemoved();
foreach ( var c in Children ) Player.Unload( c, recursiveOnly: true );
}
}