Flash/Core/Avm1.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Flash;
/// <summary>A value thrown by ActionScript (throw x;).</summary>
public sealed class AsThrow : Exception
{
public readonly Value Thrown;
public AsThrow( Value v ) : base( "ActionScript exception" ) { Thrown = v; }
}
public enum ScopeClass : byte { Global, Target, Local, With }
public sealed class Scope
{
public readonly AsObject Values;
public readonly ScopeClass Class;
public readonly Scope Parent;
public Scope( AsObject values, ScopeClass cls, Scope parent ) { Values = values; Class = cls; Parent = parent; }
}
/// <summary>The ActionScript 1/2 virtual machine (AVM1).</summary>
public sealed partial class Avm1
{
public readonly Player Player;
public int SwfVersion = 11;
public AsObject Global;
public AsObject ObjectProto, FunctionProto, ArrayProto, StringProto, NumberProto, BooleanProto, DateProto;
public AsObject MovieClipProto, TextFieldProto, ButtonProto, SoundProto, ColorProto, SharedObjectProto;
public NativeFunction ObjectCtor, FunctionCtor, ArrayCtor;
public Random Rng = new();
public readonly List<string> Log = new();
int callDepth;
const int MaxCallDepth = 256;
const long MaxActionsPerCall = 2_000_000;
long actionBudget;
public Avm1( Player player )
{
Player = player;
Builtins.Install( this );
}
public void Trace( string s )
{
Log.Add( s );
if ( Log.Count > 500 ) Log.RemoveAt( 0 );
Player.Host?.Trace( s );
}
// ======================================================================= object helpers
public AsObject NewObject( AsObject proto = null ) => new AsObject( proto ?? ObjectProto );
public AsArray NewArray( IEnumerable<Value> items = null )
{
var a = new AsArray( ArrayProto );
if ( items != null ) a.Items.AddRange( items );
return a;
}
public NativeFunction Native( string name, NativeCall call ) => new NativeFunction( FunctionProto, name, call );
public void Method( AsObject target, string name, NativeCall call ) => target.DefineHidden( name, Value.From( Native( name, call ) ) );
/// <summary>Turns a primitive into an object for property access / method calls.</summary>
public AsObject ToObject( Value v )
{
switch ( v.Kind )
{
case ValueKind.Object: return v.Obj;
case ValueKind.String: return new AsBoxed( StringProto, v );
case ValueKind.Number: return new AsBoxed( NumberProto, v );
case ValueKind.Bool: return new AsBoxed( BooleanProto, v );
default: return null;
}
}
public Value GetMember( Value objV, string name )
{
if ( objV.IsNullish ) return Value.Undefined;
if ( objV.IsString && name == "length" ) return objV.Str.Length;
var o = objV.IsObject ? objV.Obj : ToObject( objV );
if ( o is null ) return Value.Undefined;
return o.GetWithThis( name, this, objV.IsObject ? objV.Obj : o );
}
public void SetMember( Value objV, string name, Value v )
{
if ( !objV.IsObject ) return;
objV.Obj.Set( name, v, this );
}
// ======================================================================= calling
public Value CallFunction( AsFunction f, Value thisV, Value[] args )
{
if ( f is null ) return Value.Undefined;
return f.Call( this, thisV, args ?? Array.Empty<Value>() );
}
public Value CallMethod( Value objV, string name, params Value[] args )
{
var f = GetMember( objV, name ).AsObj as AsFunction;
if ( f is null ) return Value.Undefined;
return CallFunction( f, objV, args );
}
public Value Construct( AsFunction ctor, Value[] args )
{
if ( ctor is null ) return Value.Undefined;
return ctor.Construct( this, args );
}
/// <summary>Runs a frame script / clip event / button action on a timeline.</summary>
public void RunScript( DisplayObject clip, byte[] code, int start = 0, int length = -1, Value? thisOverride = null )
{
if ( code is null || code.Length == 0 ) return;
var scope = new Scope( clip, ScopeClass.Target, new Scope( Global, ScopeClass.Global, null ) );
var ctx = new Ctx
{
Code = code, Scope = scope, This = thisOverride ?? Value.From( clip ), Target = clip, BaseClip = clip,
Regs = new Value[4], Pool = Array.Empty<string>(), Version = SwfVersion,
};
actionBudget = MaxActionsPerCall;
try
{
callDepth++;
RunRange( ctx, start, length < 0 ? code.Length : start + length );
}
catch ( AsThrow t )
{
Trace( "Uncaught exception: " + t.Thrown.ToStr( this ) );
}
catch ( ScriptAbort a )
{
Trace( "Script aborted: " + a.Message );
}
finally { callDepth--; }
}
public Value InvokeBytecode( BytecodeFunction f, Value thisV, Value[] args, AsObject superOwner )
{
if ( callDepth >= MaxCallDepth ) throw new ScriptAbort( "stack overflow in " + f.Name );
var activation = new AsObject( null );
var scope = new Scope( activation, ScopeClass.Local, f.Scope );
var ctx = new Ctx
{
Code = f.Code, Scope = scope, This = thisV, BaseClip = f.BaseClip, Target = f.BaseClip, Pool = f.ConstantPool,
Version = f.SwfVersion, Func = f, Args = args,
Regs = new Value[f.IsFunction2 ? Math.Max( f.RegisterCount, 1 ) : 4],
};
if ( ctx.Target != null && ctx.Target.Removed && thisV.Obj is DisplayObject td && !td.Removed ) { ctx.Target = td; ctx.BaseClip = td; }
var argsObj = NewArray( args );
argsObj.DefineHidden( "callee", Value.From( f ) );
argsObj.DefineHidden( "caller", Value.Null );
AsObject superOwnerProto = null;
if ( thisV.IsObject ) superOwnerProto = superOwner ?? thisV.Obj.Proto;
SuperObject superObj = superOwnerProto != null ? new SuperObject( this, thisV, superOwnerProto ) : null;
if ( f.IsFunction2 )
{
ushort fl = f.Flags2;
int r = 1;
// flag bits: 0 preloadThis,1 suppressThis,2 preloadArgs,3 suppressArgs,4 preloadSuper,5 suppressSuper,6 preloadRoot,7 preloadParent,8 preloadGlobal
if ( (fl & 0x01) != 0 ) ctx.Regs[r++] = thisV;
if ( (fl & 0x04) != 0 ) ctx.Regs[r++] = Value.From( argsObj );
if ( (fl & 0x10) != 0 ) ctx.Regs[r++] = superObj != null ? Value.From( superObj ) : Value.Undefined;
if ( (fl & 0x40) != 0 ) ctx.Regs[r++] = Value.From( Player.Root );
if ( (fl & 0x80) != 0 ) ctx.Regs[r++] = f.BaseClip?.Parent != null ? Value.From( f.BaseClip.Parent ) : Value.Undefined;
if ( (fl & 0x100) != 0 ) ctx.Regs[r++] = Value.From( Global );
if ( (fl & 0x02) == 0 ) activation.SetOwn( "this", thisV );
if ( (fl & 0x08) == 0 ) activation.SetOwn( "arguments", Value.From( argsObj ) );
if ( (fl & 0x20) == 0 && superObj != null ) activation.SetOwn( "super", Value.From( superObj ) );
for ( int i = 0; i < f.ParamNames.Length; i++ )
{
var v = i < args.Length ? args[i] : Value.Undefined;
int reg = f.ParamRegisters[i];
if ( reg != 0 && reg < ctx.Regs.Length ) ctx.Regs[reg] = v;
else activation.SetOwn( f.ParamNames[i], v );
}
}
else
{
activation.SetOwn( "this", thisV );
activation.SetOwn( "arguments", Value.From( argsObj ) );
if ( superObj != null ) activation.SetOwn( "super", Value.From( superObj ) );
for ( int i = 0; i < f.ParamNames.Length; i++ )
activation.SetOwn( f.ParamNames[i], i < args.Length ? args[i] : Value.Undefined );
}
callDepth++;
try
{
var c = RunRange( ctx, f.Start, f.Start + f.Length );
return c.Kind == Completion.Return ? c.Value : Value.Undefined;
}
finally { callDepth--; }
}
// ======================================================================= interpreter
sealed class Ctx
{
public byte[] Code;
public Scope Scope;
public Value This;
public DisplayObject Target, BaseClip;
public string[] Pool;
public Value[] Regs;
public int Version;
public BytecodeFunction Func;
public Value[] Args;
public readonly List<Value> Stack = new( 16 );
}
enum Completion { Normal, Return, Jump }
readonly struct Result
{
public readonly Completion Kind; public readonly Value Value; public readonly int JumpTo;
public Result( Completion k, Value v = default, int jump = 0 ) { Kind = k; Value = v; JumpTo = jump; }
}
static Value Pop( Ctx c )
{
var s = c.Stack;
if ( s.Count == 0 ) return Value.Undefined;
var v = s[s.Count - 1];
s.RemoveAt( s.Count - 1 );
return v;
}
static Value Peek( Ctx c ) => c.Stack.Count == 0 ? Value.Undefined : c.Stack[c.Stack.Count - 1];
static void Push( Ctx c, Value v ) => c.Stack.Add( v );
static ushort U16( byte[] b, int i ) => (ushort)(b[i] | (b[i + 1] << 8));
static short S16( byte[] b, int i ) => (short)(b[i] | (b[i + 1] << 8));
static int I32( byte[] b, int i ) => b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24);
static string CStr( byte[] b, ref int i, int end )
{
int s = i;
while ( i < end && b[i] != 0 ) i++;
var str = Encoding.UTF8.GetString( b, s, i - s );
i++; // skip NUL
return str;
}
Value[] PopArgs( Ctx c, int n )
{
n = Math.Clamp( n, 0, c.Stack.Count );
var a = new Value[n];
for ( int i = 0; i < n; i++ ) a[i] = Pop( c );
return a;
}
Result RunRange( Ctx c, int start, int end )
{
var code = c.Code;
int pc = start;
while ( pc < end && pc < code.Length )
{
if ( --actionBudget < 0 ) throw new ScriptAbort( "too many actions (infinite loop?)" );
int op = code[pc];
int len = 0;
int body = pc + 1;
if ( op >= 0x80 )
{
len = U16( code, pc + 1 );
body = pc + 3;
}
int next = body + len;
if ( op == 0 ) return new Result( Completion.Normal );
switch ( op )
{
// -------------------------------------------------- timeline control (SWF 3)
case 0x04: c.Target?.AsClip?.NextFrame(); break;
case 0x05: c.Target?.AsClip?.PrevFrame(); break;
case 0x06: c.Target?.AsClip?.Play(); break;
case 0x07: c.Target?.AsClip?.Stop(); break;
case 0x08: break; // toggle quality
case 0x09: Player.StopAllSounds(); break;
case 0x81: c.Target?.AsClip?.GotoFrame( U16( code, body ) + 1, stop: true ); break;
case 0x8C: { int i = body; var label = CStr( code, ref i, next ); c.Target?.AsClip?.GotoLabel( label, stop: true ); break; }
case 0x8A: break; // WaitForFrame: everything is loaded
case 0x8D: Pop( c ); break; // WaitForFrame2
case 0x8B:
{
int i = body; var t = CStr( code, ref i, next );
c.Target = t.Length == 0 ? c.BaseClip : ResolveTargetPath( c, t ) ?? c.Target;
break;
}
case 0x20:
{
var tv = Pop( c );
if ( tv.IsObject && tv.Obj is DisplayObject d ) c.Target = d;
else
{
var t = tv.ToStr( this );
c.Target = t.Length == 0 ? c.BaseClip : ResolveTargetPath( c, t ) ?? c.BaseClip;
}
break;
}
case 0x83:
{
int i = body; var url = CStr( code, ref i, next ); var win = CStr( code, ref i, next );
Player.GetUrl( url, win );
break;
}
case 0x9A:
{
var win = Pop( c ); var url = Pop( c );
int flags = code[body];
if ( (flags & 0x40) != 0 )
{
// loadMovie / loadVariables into a target
Player.GetUrl( url.ToStr( this ), win.ToStr( this ) );
}
else Player.GetUrl( url.ToStr( this ), win.ToStr( this ) );
break;
}
case 0x9F:
{
int flags = code[body];
int bias = (flags & 0x02) != 0 ? U16( code, body + 1 ) : 0;
bool play = (flags & 0x01) != 0;
var fv = Pop( c );
GotoFrameValue( c, fv, bias, !play );
break;
}
case 0x9E: // Call (frame actions)
{
var fv = Pop( c ).ToStr( this );
CallFrame( c, fv );
break;
}
// -------------------------------------------------- stack / arithmetic
case 0x96: ActionPush( c, body, next ); break;
case 0x17: Pop( c ); break;
case 0x4C: Push( c, Peek( c ) ); break;
case 0x4D: { var a = Pop( c ); var b = Pop( c ); Push( c, a ); Push( c, b ); break; }
case 0x0A: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a + b ); break; }
case 0x0B: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a - b ); break; }
case 0x0C: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a * b ); break; }
case 0x0D:
{
var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this );
if ( b == 0 && c.Version < 5 ) Push( c, "#ERROR#" ); else Push( c, a / b );
break;
}
case 0x3F: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a % b ); break; }
case 0x0E: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a == b ); break; }
case 0x0F: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a < b ); break; }
case 0x10: { var b = Pop( c ).ToBool( c.Version ); var a = Pop( c ).ToBool( c.Version ); Push( c, a && b ); break; }
case 0x11: { var b = Pop( c ).ToBool( c.Version ); var a = Pop( c ).ToBool( c.Version ); Push( c, a || b ); break; }
case 0x12: { var a = Pop( c ).ToBool( c.Version ); Push( c, !a ); break; }
case 0x13: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, a == b ); break; }
case 0x14: Push( c, Pop( c ).ToStr( this ).Length ); break;
case 0x31: Push( c, Pop( c ).ToStr( this ).Length ); break;
case 0x15:
case 0x35:
{
int count = Pop( c ).ToInt32( this ); int index = Pop( c ).ToInt32( this ) - 1; var s = Pop( c ).ToStr( this );
if ( index < 0 ) index = 0;
if ( index >= s.Length ) { Push( c, "" ); break; }
if ( count < 0 || index + count > s.Length ) count = s.Length - index;
Push( c, s.Substring( index, count ) );
break;
}
case 0x21: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, a + b ); break; }
case 0x29: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, string.CompareOrdinal( a, b ) < 0 ); break; }
case 0x68: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, string.CompareOrdinal( a, b ) > 0 ); break; }
case 0x18: Push( c, (double)Value.ToInt32( Math.Truncate( Pop( c ).ToNumber( this ) ) ) ); break;
case 0x32: case 0x36: { var s = Pop( c ).ToStr( this ); Push( c, s.Length > 0 ? s[0] : 0 ); break; }
case 0x33: case 0x37: { int n = Pop( c ).ToInt32( this ); Push( c, n == 0 ? "" : ((char)n).ToString() ); break; }
case 0x30: { int max = Pop( c ).ToInt32( this ); Push( c, max <= 0 ? 0 : Rng.Next( max ) ); break; }
case 0x34: Push( c, Player.TimeMs ); break;
case 0x26: Trace( Pop( c ).ToStr( this ) ); break;
case 0x47:
{
var b = Pop( c ); var a = Pop( c );
var pa = a.IsObject ? a.Obj.ToPrimitive( this, false ) : a;
var pb = b.IsObject ? b.Obj.ToPrimitive( this, false ) : b;
if ( a.IsObject && a.Obj is DisplayObject ) pa = a.ToStr( this );
if ( b.IsObject && b.Obj is DisplayObject ) pb = b.ToStr( this );
if ( pa.IsString || pb.IsString ) Push( c, pa.ToStr( this ) + pb.ToStr( this ) );
else Push( c, pa.ToNumber( this ) + pb.ToNumber( this ) );
break;
}
case 0x48: { var b = Pop( c ); var a = Pop( c ); Push( c, LessThan( a, b ) ); break; }
case 0x67: { var b = Pop( c ); var a = Pop( c ); Push( c, LessThan( b, a ) ); break; }
case 0x49: { var b = Pop( c ); var a = Pop( c ); Push( c, AbstractEquals( a, b ) ); break; }
case 0x66: { var b = Pop( c ); var a = Pop( c ); Push( c, StrictEquals( a, b ) ); break; }
case 0x4A: Push( c, Pop( c ).ToNumber( this ) ); break;
case 0x4B: Push( c, Pop( c ).ToStr( this ) ); break;
case 0x50: Push( c, Pop( c ).ToNumber( this ) + 1 ); break;
case 0x51: Push( c, Pop( c ).ToNumber( this ) - 1 ); break;
case 0x60: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a & b ); break; }
case 0x61: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a | b ); break; }
case 0x62: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a ^ b ); break; }
case 0x63: { var b = Pop( c ).ToInt32( this ) & 31; var a = Pop( c ).ToInt32( this ); Push( c, a << b ); break; }
case 0x64: { var b = Pop( c ).ToInt32( this ) & 31; var a = Pop( c ).ToInt32( this ); Push( c, a >> b ); break; }
case 0x65: { var b = Pop( c ).ToInt32( this ) & 31; var a = (uint)Pop( c ).ToInt32( this ); Push( c, (double)(a >> b) ); break; }
case 0x44: Push( c, Pop( c ).TypeOf() ); break;
case 0x45:
{
var v = Pop( c );
Push( c, v.IsObject && v.Obj is DisplayObject d ? (Value)d.TargetPath : Value.Undefined );
break;
}
// -------------------------------------------------- variables
case 0x1C:
{
var name = Pop( c ).ToStr( this );
Push( c, GetVariable( c, name ) );
break;
}
case 0x1D:
{
var v = Pop( c ); var name = Pop( c ).ToStr( this );
SetVariable( c, name, v );
break;
}
case 0x3C:
{
var v = Pop( c ); var name = Pop( c ).ToStr( this );
DefineLocal( c, name, v );
break;
}
case 0x41:
{
var name = Pop( c ).ToStr( this );
var local = LocalScope( c );
if ( !local.Values.HasOwnProperty( name ) ) local.Values.SetOwn( name, Value.Undefined );
break;
}
case 0x3A:
{
var name = Pop( c ).ToStr( this ); var obj = Pop( c );
Push( c, obj.IsObject && obj.Obj.Delete( name ) );
break;
}
case 0x3B:
{
var name = Pop( c ).ToStr( this );
bool ok = false;
for ( var s = c.Scope; s != null; s = s.Parent )
if ( s.Values.HasOwnProperty( name ) ) { ok = s.Values.Delete( name ); break; }
Push( c, ok );
break;
}
case 0x4E:
{
var nameV = Pop( c ); var obj = Pop( c );
Push( c, GetMember( obj, nameV.ToStr( this ) ) );
break;
}
case 0x4F:
{
var v = Pop( c ); var nameV = Pop( c ); var obj = Pop( c );
SetMember( obj, nameV.ToStr( this ), v );
break;
}
case 0x22:
{
int idx = Pop( c ).ToInt32( this ); var tv = Pop( c );
var d = TargetFromValue( c, tv );
Push( c, d != null ? d.GetIndexedProperty( idx, this ) : Value.Undefined );
break;
}
case 0x23:
{
var v = Pop( c ); int idx = Pop( c ).ToInt32( this ); var tv = Pop( c );
var d = TargetFromValue( c, tv );
d?.SetIndexedProperty( idx, v, this );
break;
}
case 0x87:
{
int r = code[body];
if ( r < c.Regs.Length ) c.Regs[r] = Peek( c );
break;
}
case 0x88:
{
int n = U16( code, body ); int i = body + 2;
var pool = new string[n];
for ( int k = 0; k < n; k++ ) pool[k] = CStr( code, ref i, next );
c.Pool = pool;
break;
}
// -------------------------------------------------- objects
case 0x40:
{
var name = Pop( c ).ToStr( this ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
var ctor = GetVariable( c, name ).AsObj as AsFunction;
Push( c, ctor != null ? Construct( ctor, args ) : Value.Undefined );
break;
}
case 0x53:
{
var nameV = Pop( c ); var obj = Pop( c ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
AsFunction ctor;
if ( nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0) ) ctor = obj.AsObj as AsFunction;
else ctor = GetMember( obj, nameV.ToStr( this ) ).AsObj as AsFunction;
Push( c, ctor != null ? Construct( ctor, args ) : Value.Undefined );
break;
}
case 0x42:
{
int n = Pop( c ).ToInt32( this );
var arr = NewArray();
for ( int i = 0; i < n; i++ ) arr.Items.Add( Pop( c ) );
Push( c, arr );
break;
}
case 0x43:
{
int n = Pop( c ).ToInt32( this );
var obj = NewObject();
for ( int i = 0; i < n; i++ ) { var v = Pop( c ); var k = Pop( c ).ToStr( this ); obj.Set( k, v, this ); }
Push( c, obj );
break;
}
case 0x3D:
{
var name = Pop( c ).ToStr( this ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
var (holder, fv) = ResolveCallable( c, name );
if ( fv.AsObj is AsFunction f )
{
var thisV = holder != null ? Value.From( holder ) : (c.Target != null ? Value.From( c.Target ) : Value.Undefined);
if ( holder != null && LocalScopeHolds( c, holder ) ) thisV = c.Target != null ? Value.From( c.Target ) : Value.Undefined;
Push( c, CallFunction( f, thisV, args ) );
}
else Push( c, Value.Undefined );
if ( c.BaseClip != null && c.BaseClip.Removed && c.Func == null ) return new Result( Completion.Normal );
break;
}
case 0x52:
{
var nameV = Pop( c ); var obj = Pop( c ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
if ( obj.IsNullish ) { Push( c, Value.Undefined ); break; }
if ( (nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0)) && obj.Obj is SuperObject sc )
{
Push( c, sc.CallConstructor( args ) );
break;
}
if ( nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0) )
{
// obj itself is the function
if ( obj.AsObj is AsFunction f0 ) Push( c, CallFunction( f0, Value.Undefined, args ) );
else Push( c, Value.Undefined );
break;
}
var name = nameV.ToStr( this );
if ( obj.IsObject && obj.Obj is SuperObject sup )
{
Push( c, sup.CallMethod( name, args ) );
break;
}
var fv = GetMember( obj, name );
if ( fv.AsObj is AsFunction f )
{
var thisV = obj.IsObject ? obj : Value.From( ToObject( obj ) );
// for prototype methods, super inside should start above the prototype that owns the method
Push( c, f.Call( this, thisV, args, FindOwner( obj, name ) ) );
}
else Push( c, Value.Undefined );
if ( c.BaseClip != null && c.BaseClip.Removed && c.Func == null ) return new Result( Completion.Normal );
break;
}
case 0x3E:
return new Result( Completion.Return, Pop( c ) );
case 0x54:
{
var ctor = Pop( c ); var obj = Pop( c );
Push( c, obj.IsObject && ctor.AsObj is AsFunction && InstanceOf( obj.Obj, ctor.Obj ) );
break;
}
case 0x2B:
{
var obj = Pop( c ); var ctor = Pop( c );
Push( c, obj.IsObject && ctor.AsObj is AsFunction && InstanceOf( obj.Obj, ctor.Obj ) ? obj : Value.Null );
break;
}
case 0x2C:
{
var ctor = Pop( c ); int n = Pop( c ).ToInt32( this ); var ifaces = PopArgs( c, n );
var proto = ctor.AsObj?.Get( "prototype", this ).AsObj;
if ( proto != null ) proto.Native = ifaces;
break;
}
case 0x69:
{
var superCtor = Pop( c ).AsObj; var sub = Pop( c ).AsObj;
if ( sub != null && superCtor != null )
{
var proto = NewObject( superCtor.Get( "prototype", this ).AsObj );
proto.DefineHidden( "__constructor__", Value.From( superCtor ) );
if ( SwfVersion >= 7 ) proto.DefineHidden( "constructor", Value.From( sub ) );
sub.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum );
}
break;
}
case 0x46:
{
var name = Pop( c ).ToStr( this );
var obj = GetVariable( c, name );
Push( c, Value.Undefined );
if ( obj.IsObject ) foreach ( var k in obj.Obj.EnumerableKeys() ) Push( c, k );
break;
}
case 0x55:
{
var obj = Pop( c );
Push( c, Value.Undefined );
if ( obj.IsObject ) foreach ( var k in obj.Obj.EnumerableKeys() ) Push( c, k );
break;
}
case 0x2A:
throw new AsThrow( Pop( c ) );
// -------------------------------------------------- functions & blocks
case 0x9B:
{
int i = body;
var name = CStr( code, ref i, next );
int np = U16( code, i ); i += 2;
var ps = new string[np];
for ( int k = 0; k < np; k++ ) ps[k] = CStr( code, ref i, next );
int size = U16( code, i );
var f = MakeFunction( c, name, ps, null, false, 0, 0, next, size );
next += size;
if ( name.Length > 0 ) SetVariable( c, name, f );
else Push( c, f );
break;
}
case 0x8E:
{
int i = body;
var name = CStr( code, ref i, next );
int np = U16( code, i ); i += 2;
int regCount = code[i++];
ushort flags = (ushort)(code[i] | (code[i + 1] << 8)); i += 2;
var ps = new string[np]; var pr = new byte[np];
for ( int k = 0; k < np; k++ ) { pr[k] = code[i++]; ps[k] = CStr( code, ref i, next ); }
int size = U16( code, i );
var f = MakeFunction( c, name, ps, pr, true, regCount, flags, next, size );
next += size;
if ( name.Length > 0 ) SetVariable( c, name, f );
else Push( c, f );
break;
}
case 0x94:
{
int size = U16( code, body );
var obj = Pop( c );
int blockStart = next;
int blockEnd = next + size;
var withObj = obj.IsObject ? obj.Obj : ToObject( obj );
var saved = c.Scope;
if ( withObj != null ) c.Scope = new Scope( withObj, ScopeClass.With, c.Scope );
Result r;
try { r = RunRange( c, blockStart, blockEnd ); }
finally { c.Scope = saved; }
if ( r.Kind == Completion.Return ) return r;
if ( r.Kind == Completion.Jump )
{
if ( r.JumpTo >= start && r.JumpTo < end ) { pc = r.JumpTo; continue; }
if ( r.JumpTo == end ) return new Result( Completion.Normal );
return r;
}
next = blockEnd;
break;
}
case 0x8F:
{
int i = body;
int flags = code[i++];
int trySize = U16( code, i ); i += 2;
int catchSize = U16( code, i ); i += 2;
int finallySize = U16( code, i ); i += 2;
bool catchInReg = (flags & 4) != 0;
string catchName = null; int catchReg = 0;
if ( catchInReg ) catchReg = code[i++]; else catchName = CStr( code, ref i, next );
int tryStart = next, catchStart = tryStart + trySize, finStart = catchStart + catchSize, after = finStart + finallySize;
Result r = new( Completion.Normal );
try
{
r = RunRange( c, tryStart, catchStart );
}
catch ( AsThrow t ) when ( (flags & 1) != 0 )
{
if ( catchInReg ) { if ( catchReg < c.Regs.Length ) c.Regs[catchReg] = t.Thrown; }
else LocalScope( c ).Values.SetOwn( catchName, t.Thrown );
r = RunRange( c, catchStart, finStart );
}
finally
{
if ( (flags & 2) != 0 && finallySize > 0 )
{
var fr = RunRange( c, finStart, after );
if ( fr.Kind == Completion.Return ) r = fr;
}
}
if ( r.Kind == Completion.Return ) return r;
if ( r.Kind == Completion.Jump )
{
if ( r.JumpTo >= start && r.JumpTo < end ) { pc = r.JumpTo; continue; }
if ( r.JumpTo == end ) return new Result( Completion.Normal );
return r;
}
next = after;
break;
}
case 0x99:
{
int target = next + S16( code, body );
if ( target == end ) return new Result( Completion.Normal );
if ( target < start || target > end ) return new Result( Completion.Jump, default, target );
pc = target;
continue;
}
case 0x9D:
{
int target = next + S16( code, body );
if ( Pop( c ).ToBool( c.Version ) )
{
if ( target == end ) return new Result( Completion.Normal );
if ( target < start || target > end ) return new Result( Completion.Jump, default, target );
pc = target;
continue;
}
break;
}
// -------------------------------------------------- movie clips
case 0x24:
{
int depth = Pop( c ).ToInt32( this ); var tname = Pop( c ).ToStr( this ); var src = Pop( c );
var srcClip = TargetFromValue( c, src );
srcClip?.Duplicate( tname, depth + 0 );
break;
}
case 0x25:
{
var tv = Pop( c );
var d = TargetFromValue( c, tv );
d?.RemoveFromScript();
break;
}
case 0x27:
{
var target = Pop( c ); bool lockCenter = Pop( c ).ToBool( c.Version ); bool constrain = Pop( c ).ToBool( c.Version );
double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
if ( constrain ) { y2 = Pop( c ).ToNumber( this ); x2 = Pop( c ).ToNumber( this ); y1 = Pop( c ).ToNumber( this ); x1 = Pop( c ).ToNumber( this ); }
var d = TargetFromValue( c, target );
if ( d != null ) Player.StartDrag( d, lockCenter, constrain ? new double[] { x1, y1, x2, y2 } : null );
break;
}
case 0x28: Player.StopDrag(); break;
default:
// unknown / unsupported action: skip
break;
}
pc = next;
}
return new Result( Completion.Normal );
}
void ActionPush( Ctx c, int i, int end )
{
var code = c.Code;
while ( i < end )
{
int type = code[i++];
switch ( type )
{
case 0: Push( c, CStr( code, ref i, end ) ); break;
case 1: Push( c, (double)BitConverter.Int32BitsToSingle( I32( code, i ) ) ); i += 4; break;
case 2: Push( c, Value.Null ); break;
case 3: Push( c, Value.Undefined ); break;
case 4: { int r = code[i++]; Push( c, r < c.Regs.Length ? c.Regs[r] : Value.Undefined ); break; }
case 5: Push( c, code[i++] != 0 ); break;
case 6:
{
long hi = (uint)I32( code, i ); long lo = (uint)I32( code, i + 4 );
Push( c, BitConverter.Int64BitsToDouble( (hi << 32) | lo ) );
i += 8;
break;
}
case 7: Push( c, (double)I32( code, i ) ); i += 4; break;
case 8: { int k = code[i++]; Push( c, k < c.Pool.Length ? (Value)c.Pool[k] : Value.Undefined ); break; }
case 9: { int k = U16( code, i ); i += 2; Push( c, k < c.Pool.Length ? (Value)c.Pool[k] : Value.Undefined ); break; }
default: return;
}
}
}
BytecodeFunction MakeFunction( Ctx c, string name, string[] ps, byte[] regs, bool f2, int regCount, ushort flags, int start, int size )
{
var f = new BytecodeFunction( FunctionProto )
{
Name = name, Code = c.Code, Start = start, Length = size, ParamNames = ps, ParamRegisters = regs ?? new byte[ps.Length],
IsFunction2 = f2, RegisterCount = regCount, Flags2 = flags, ConstantPool = c.Pool, Scope = c.Scope,
BaseClip = c.BaseClip ?? Player.Root, SwfVersion = c.Version,
};
var proto = NewObject();
proto.DefineHidden( "constructor", Value.From( f ) );
f.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum );
return f;
}
// ======================================================================= variable resolution
Scope LocalScope( Ctx c )
{
for ( var s = c.Scope; s != null; s = s.Parent )
if ( s.Class == ScopeClass.Local || s.Class == ScopeClass.Target ) return s;
return c.Scope;
}
bool LocalScopeHolds( Ctx c, AsObject holder )
{
for ( var s = c.Scope; s != null; s = s.Parent )
if ( s.Class == ScopeClass.Local && ReferenceEquals( s.Values, holder ) ) return true;
return false;
}
static bool IsPath( string name )
{
foreach ( var ch in name ) if ( ch == '/' || ch == ':' || ch == '.' ) return true;
return false;
}
Value GetVariable( Ctx c, string name )
{
if ( name.Length > 0 && IsPath( name ) && !char.IsDigit( name[0] ) )
{
if ( TryResolvePathVariable( c, name, out var pv ) ) return pv;
}
var (holder, v) = ResolveCallable( c, name );
return v;
}
(AsObject holder, Value value) ResolveCallable( Ctx c, string name )
{
switch ( name )
{
case "this": return (null, c.This);
case "_root": return (null, Value.From( Player.Root ));
case "_global": return (null, Value.From( Global ));
case "_parent":
{
// _parent resolves on the current target unless shadowed by a local
break;
}
}
for ( var s = c.Scope; s != null; s = s.Parent )
{
var o = s.Values;
if ( o is DisplayObject d && d.Removed ) o = c.Target ?? Player.Root;
if ( o.Has( name ) ) return (o, o.Get( name, this ));
}
if ( name == "_parent" ) return (null, c.Target?.Parent != null ? Value.From( c.Target.Parent ) : Value.Undefined);
if ( name.StartsWith( "_level" ) ) return (null, Value.From( Player.Root ));
return (null, Value.Undefined);
}
void SetVariable( Ctx c, string name, Value v )
{
if ( name.Length > 0 && IsPath( name ) && !char.IsDigit( name[0] ) )
{
if ( TrySetPathVariable( c, name, v ) ) return;
}
for ( var s = c.Scope; s != null; s = s.Parent )
{
var o = s.Values;
if ( o is DisplayObject d && d.Removed ) continue;
if ( s.Class == ScopeClass.Target || o.Has( name ) || s.Parent == null )
{
o.Set( name, v, this );
return;
}
}
}
void DefineLocal( Ctx c, string name, Value v )
{
// in a with block, an existing property on the with target wins
for ( var s = c.Scope; s != null; s = s.Parent )
{
if ( s.Class == ScopeClass.With )
{
if ( s.Values.Has( name ) ) { s.Values.Set( name, v, this ); return; }
continue;
}
s.Values.SetOwn( name, v );
return;
}
}
AsObject FindOwner( Value obj, string name )
{
if ( !obj.IsObject ) return null;
var o = obj.Obj; int g = 0;
while ( o != null && g++ < 64 )
{
if ( o.GetOwnProperty( name ) != null ) return ReferenceEquals( o, obj.Obj ) ? null : o;
o = o.Proto;
}
return null;
}
public bool InstanceOf( AsObject obj, AsObject ctor )
{
var proto = ctor.Get( "prototype", this ).AsObj;
if ( proto is null ) return false;
var o = obj.Proto; int g = 0;
while ( o != null && g++ < 128 )
{
if ( ReferenceEquals( o, proto ) ) return true;
if ( o.Native is Value[] ifaces )
foreach ( var i in ifaces ) if ( i.AsObj != null && ReferenceEquals( i.Obj, ctor ) ) return true;
o = o.Proto;
}
return false;
}
// ======================================================================= target paths
DisplayObject TargetFromValue( Ctx c, Value v )
{
if ( v.IsObject && v.Obj is DisplayObject d ) return d;
var s = v.ToStr( this );
if ( s.Length == 0 ) return c.Target;
return ResolveTargetPath( c, s );
}
public DisplayObject ResolveTargetPath( DisplayObject start, string path ) => ResolveTargetPath( new Ctx { Target = start, BaseClip = start, This = Value.From( start ) }, path );
DisplayObject ResolveTargetPath( Ctx c, string path )
{
if ( string.IsNullOrEmpty( path ) ) return c.Target;
DisplayObject cur = c.Target ?? Player.Root;
int i = 0;
if ( path[0] == '/' ) { cur = Player.Root; i = 1; }
var parts = path.Substring( i ).Split( '/', '.' );
bool first = true;
foreach ( var raw in parts )
{
var p = raw;
if ( p.Length == 0 ) continue;
if ( p == ".." || p == "_parent" ) { cur = cur?.Parent; }
else if ( p == "_root" || p.StartsWith( "_level" ) ) cur = Player.Root;
else if ( p == "this" ) { cur = c.This.Obj as DisplayObject ?? cur; }
else if ( first && p != null && c.Scope != null && !(cur is MovieClip mc0 && mc0.ChildByName( p ) != null) )
{
// first segment can be a variable holding a clip
var v = GetVariableNoPath( c, p );
if ( v.IsObject && v.Obj is DisplayObject vd ) cur = vd;
else cur = (cur as MovieClip)?.ChildByName( p );
}
else
{
var mc = cur as MovieClip;
if ( mc is null ) return null;
var child = mc.ChildByName( p );
if ( child == null )
{
var v = mc.Get( p, this );
child = v.Obj as DisplayObject;
}
cur = child;
}
first = false;
if ( cur is null ) return null;
}
return cur;
}
Value GetVariableNoPath( Ctx c, string name ) => ResolveCallable( c, name ).value;
bool TryResolvePathVariable( Ctx c, string name, out Value v )
{
v = Value.Undefined;
int colon = name.LastIndexOf( ':' );
string targetPath, varName;
if ( colon >= 0 ) { targetPath = name.Substring( 0, colon ); varName = name.Substring( colon + 1 ); }
else
{
int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( '/' ) );
if ( dot < 0 ) return false;
targetPath = name.Substring( 0, dot ); varName = name.Substring( dot + 1 );
if ( name.IndexOf( '/' ) >= 0 && name.IndexOf( '.' ) < 0 && name.IndexOf( ':' ) < 0 )
{
// "/a/b" is a clip path
var d = ResolveTargetPath( c, name );
if ( d != null ) { v = Value.From( d ); return true; }
}
}
var start = targetPath.Length == 0 ? c.Target : ResolveTargetPath( c, targetPath );
if ( start == null )
{
// maybe an object path like a.b.c on plain objects
var parts = name.Split( '.' );
var cur = GetVariableNoPath( c, parts[0] );
for ( int k = 1; k < parts.Length; k++ ) cur = GetMember( cur, parts[k] );
v = cur;
return true;
}
v = start.Get( varName, this );
return true;
}
bool TrySetPathVariable( Ctx c, string name, Value v )
{
int colon = name.LastIndexOf( ':' );
string targetPath, varName;
if ( colon >= 0 ) { targetPath = name.Substring( 0, colon ); varName = name.Substring( colon + 1 ); }
else
{
int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( '/' ) );
if ( dot < 0 ) return false;
targetPath = name.Substring( 0, dot ); varName = name.Substring( dot + 1 );
}
var start = targetPath.Length == 0 ? c.Target : ResolveTargetPath( c, targetPath );
if ( start == null )
{
var parts = name.Split( '.' );
var cur = GetVariableNoPath( c, parts[0] );
for ( int k = 1; k < parts.Length - 1; k++ ) cur = GetMember( cur, parts[k] );
if ( cur.IsObject ) { cur.Obj.Set( parts[^1], v, this ); return true; }
return false;
}
start.Set( varName, v, this );
return true;
}
// ======================================================================= frames
void GotoFrameValue( Ctx c, Value fv, int bias, bool stop )
{
var target = c.Target?.AsClip;
if ( fv.IsString )
{
var s = fv.Str;
int colon = s.LastIndexOf( ':' );
if ( colon >= 0 )
{
target = ResolveTargetPath( c, s.Substring( 0, colon ) )?.AsClip;
s = s.Substring( colon + 1 );
}
if ( target == null ) return;
var n = Value.StringToNumber( s, SwfVersion );
if ( !double.IsNaN( n ) ) target.GotoFrame( (int)n + bias, stop );
else target.GotoLabel( s, stop );
return;
}
if ( target == null ) return;
target.GotoFrame( Value.ToInt32( fv.ToNumber( this ) ) + bias, stop );
}
void CallFrame( Ctx c, string fv )
{
var target = c.Target?.AsClip;
var s = fv;
int colon = s.LastIndexOf( ':' );
if ( colon >= 0 ) { target = ResolveTargetPath( c, s.Substring( 0, colon ) )?.AsClip; s = s.Substring( colon + 1 ); }
if ( target == null ) return;
var n = Value.StringToNumber( s, SwfVersion );
int frame = !double.IsNaN( n ) ? (int)n : target.FrameOfLabel( s );
if ( frame <= 0 ) return;
foreach ( var code in target.FrameScripts( frame ) ) RunScript( target, code );
}
// ======================================================================= comparison
public Value LessThan( Value a, Value b )
{
var pa = a.IsObject ? a.Obj.ToPrimitive( this, false ) : a;
var pb = b.IsObject ? b.Obj.ToPrimitive( this, false ) : b;
if ( pa.IsString && pb.IsString ) return string.CompareOrdinal( pa.Str, pb.Str ) < 0;
double x = pa.ToNumber( this ), y = pb.ToNumber( this );
if ( double.IsNaN( x ) || double.IsNaN( y ) ) return Value.Undefined;
return x < y;
}
public bool StrictEquals( Value a, Value b )
{
if ( a.Kind != b.Kind ) return false;
switch ( a.Kind )
{
case ValueKind.Undefined: case ValueKind.Null: return true;
case ValueKind.Number: return a.Num == b.Num;
case ValueKind.Bool: return a.Num == b.Num;
case ValueKind.String: return a.Str == b.Str;
default: return ReferenceEquals( a.Obj, b.Obj );
}
}
public bool AbstractEquals( Value a, Value b )
{
if ( a.Kind == b.Kind ) return StrictEquals( a, b );
if ( a.IsNullish && b.IsNullish ) return true;
if ( a.IsNullish || b.IsNullish ) return false;
if ( a.IsNumber && b.IsString ) return a.Num == b.ToNumber( this );
if ( a.IsString && b.IsNumber ) return a.ToNumber( this ) == b.Num;
if ( a.IsBool ) return AbstractEquals( a.Num, b );
if ( b.IsBool ) return AbstractEquals( a, b.Num );
if ( (a.IsNumber || a.IsString) && b.IsObject ) return AbstractEquals( a, b.Obj.ToPrimitive( this, false ) );
if ( a.IsObject && (b.IsNumber || b.IsString) ) return AbstractEquals( a.Obj.ToPrimitive( this, false ), b );
return false;
}
}
/// <summary>Stops a runaway script.</summary>
public sealed class ScriptAbort : Exception
{
public ScriptAbort( string m ) : base( m ) { }
}
/// <summary>The value of `super` inside a method or constructor.</summary>
public sealed class SuperObject : AsObject
{
readonly Avm1 vm;
readonly Value thisV;
readonly AsObject owner; // prototype that owns the running method (its __constructor__ is the parent class)
public SuperObject( Avm1 vm, Value thisV, AsObject owner ) : base( owner?.Proto ) { this.vm = vm; this.thisV = thisV; this.owner = owner; }
public Value CallMethod( string name, Value[] args )
{
var start = owner?.Proto;
if ( start is null ) return Value.Undefined;
var f = start.Get( name, vm ).AsObj as AsFunction;
if ( f is null ) return Value.Undefined;
AsObject found = start; int g = 0;
while ( found != null && found.GetOwnProperty( name ) == null && g++ < 64 ) found = found.Proto;
return f.Call( vm, thisV, args, found );
}
/// <summary>super(...) — runs the parent class constructor on this.</summary>
public Value CallConstructor( Value[] args )
{
var ctor = owner?.Get( "__constructor__", vm ).AsObj as AsFunction;
if ( ctor is null ) return Value.Undefined;
return ctor.Call( vm, thisV, args, owner.Proto );
}
}