Flash/Core/AsObject.cs
using System;
using System.Collections.Generic;
namespace Flash;
[Flags]
public enum PropFlags : byte { None = 0, DontEnum = 1, DontDelete = 2, ReadOnly = 4 }
public sealed class Property
{
public Value Value;
public PropFlags Flags;
public AsFunction Getter, Setter;
public bool IsVirtual => Getter != null || Setter != null;
}
/// <summary>A plain ActionScript object (with prototype chain).</summary>
public class AsObject
{
readonly Dictionary<string, Property> props = new();
readonly List<string> order = new();
public AsObject Proto;
/// <summary>Native payload for built-in classes (Date ticks, Sound, Color target...).</summary>
public object Native;
public AsObject() { }
public AsObject( AsObject proto ) { Proto = proto; }
public virtual string ClassName => "Object";
// --------------------------------------------------------------- own property storage
public Property GetOwnProperty( string name ) => props.TryGetValue( name, out var p ) ? p : null;
public bool HasOwnProperty( string name ) => props.ContainsKey( name ) || HasNativeProperty( name );
public void SetOwn( string name, Value v, PropFlags flags = PropFlags.None )
{
if ( props.TryGetValue( name, out var p ) ) { p.Value = v; p.Getter = null; p.Setter = null; p.Flags = flags; return; }
props[name] = new Property { Value = v, Flags = flags };
order.Add( name );
}
public void DefineHidden( string name, Value v ) => SetOwn( name, v, PropFlags.DontEnum );
public void AddVirtual( string name, AsFunction getter, AsFunction setter, PropFlags flags = PropFlags.None )
{
if ( !props.TryGetValue( name, out var p ) ) { p = new Property(); props[name] = p; order.Add( name ); }
p.Getter = getter; p.Setter = setter; p.Flags = flags; p.Value = Value.Undefined;
}
public virtual bool Delete( string name )
{
if ( !props.TryGetValue( name, out var p ) ) return false;
if ( (p.Flags & PropFlags.DontDelete) != 0 ) return false;
props.Remove( name );
order.Remove( name );
return true;
}
public void SetFlags( string name, PropFlags set, PropFlags clear )
{
if ( props.TryGetValue( name, out var p ) ) p.Flags = (p.Flags | set) & ~clear;
}
public IEnumerable<string> OwnKeys => order;
public int OwnCount => order.Count;
// --------------------------------------------------------------- hooks for special objects
/// <summary>Built-in (non-dictionary) properties, e.g. a MovieClip's _x or an array's indices.</summary>
protected virtual bool TryGetNative( string name, Avm1 vm, out Value v ) { v = default; return false; }
protected virtual bool TrySetNative( string name, Value v, Avm1 vm ) => false;
protected virtual bool HasNativeProperty( string name ) => false;
/// <summary>Names to enumerate besides the dictionary (array indices, child clips...).</summary>
public virtual IEnumerable<string> NativeKeys => Array.Empty<string>();
// --------------------------------------------------------------- lookup
public Value Get( string name, Avm1 vm ) => GetWithThis( name, vm, this );
public Value GetWithThis( string name, Avm1 vm, AsObject thisObj )
{
if ( name == "__proto__" ) return Proto is null ? Value.Undefined : Value.From( Proto );
var o = this;
int guard = 0;
while ( o != null && guard++ < 256 )
{
if ( o.props.TryGetValue( name, out var p ) )
{
if ( p.Getter != null ) return vm.CallFunction( p.Getter, Value.From( thisObj ), Array.Empty<Value>() );
if ( p.Setter != null ) return Value.Undefined;
return p.Value;
}
if ( o.TryGetNative( name, vm, out var nv ) ) return nv;
o = o.Proto;
}
return Value.Undefined;
}
public bool Has( string name )
{
if ( name == "__proto__" ) return Proto != null;
var o = this; int guard = 0;
while ( o != null && guard++ < 256 )
{
if ( o.props.ContainsKey( name ) || o.HasNativeProperty( name ) ) return true;
o = o.Proto;
}
return false;
}
public virtual void Set( string name, Value v, Avm1 vm )
{
if ( name == "__proto__" ) { Proto = v.AsObj; return; }
if ( props.TryGetValue( name, out var p ) )
{
if ( p.Setter != null ) { vm.CallFunction( p.Setter, Value.From( this ), new[] { v } ); return; }
if ( p.Getter != null ) return;
if ( (p.Flags & PropFlags.ReadOnly) != 0 ) return;
p.Value = v;
return;
}
if ( TrySetNative( name, v, vm ) ) return;
// inherited virtual property?
var o = Proto; int guard = 0;
while ( o != null && guard++ < 256 )
{
if ( o.props.TryGetValue( name, out var ip ) )
{
if ( ip.Setter != null ) { vm.CallFunction( ip.Setter, Value.From( this ), new[] { v } ); return; }
if ( ip.Getter != null ) return;
break;
}
o = o.Proto;
}
props[name] = new Property { Value = v };
order.Add( name );
}
/// <summary>All enumerable names, own first then inherited (for..in).</summary>
public List<string> EnumerableKeys()
{
var seen = new HashSet<string>();
var list = new List<string>();
var o = this; int guard = 0;
while ( o != null && guard++ < 256 )
{
foreach ( var k in o.NativeKeys )
if ( seen.Add( k ) ) list.Add( k );
for ( int i = o.order.Count - 1; i >= 0; i-- )
{
var k = o.order[i];
if ( !seen.Add( k ) ) continue;
if ( (o.props[k].Flags & PropFlags.DontEnum) == 0 ) list.Add( k );
}
o = o.Proto;
}
return list;
}
public virtual Value ToPrimitive( Avm1 vm, bool hintString )
{
var first = hintString ? "toString" : "valueOf";
var second = hintString ? "valueOf" : "toString";
var f = Get( first, vm ).AsObj as AsFunction;
if ( f != null )
{
var r = vm.CallFunction( f, Value.From( this ), Array.Empty<Value>() );
if ( r.Kind != ValueKind.Object ) return r;
}
f = Get( second, vm ).AsObj as AsFunction;
if ( f != null )
{
var r = vm.CallFunction( f, Value.From( this ), Array.Empty<Value>() );
if ( r.Kind != ValueKind.Object ) return r;
}
return Value.From( this );
}
public bool IsInstanceOf( AsObject ctor, Avm1 vm )
{
var proto = ctor.Get( "prototype", vm ).AsObj;
if ( proto is null ) return false;
var o = Proto; int guard = 0;
while ( o != null && guard++ < 256 )
{
if ( ReferenceEquals( o, proto ) ) return true;
// AS2 "implements": interfaces listed in __implements__
o = o.Proto;
}
return false;
}
}
/// <summary>An ActionScript Array.</summary>
public sealed class AsArray : AsObject
{
public readonly List<Value> Items = new();
public AsArray( AsObject proto ) : base( proto ) { }
public override string ClassName => "Array";
public static bool TryIndex( string name, out int index )
{
index = 0;
if ( string.IsNullOrEmpty( name ) || name.Length > 9 ) return false;
for ( int i = 0; i < name.Length; i++ )
{
char c = name[i];
if ( c < '0' || c > '9' ) return false;
if ( i == 0 && c == '0' && name.Length > 1 ) return false;
index = index * 10 + (c - '0');
}
return true;
}
protected override bool TryGetNative( string name, Avm1 vm, out Value v )
{
if ( name == "length" ) { v = Items.Count; return true; }
if ( TryIndex( name, out int i ) ) { v = i < Items.Count ? Items[i] : Value.Undefined; return true; }
v = default; return false;
}
protected override bool TrySetNative( string name, Value v, Avm1 vm )
{
if ( name == "length" )
{
int n = Math.Max( 0, Value.ToInt32( v.ToNumber( vm ) ) );
if ( n < Items.Count ) Items.RemoveRange( n, Items.Count - n );
else while ( Items.Count < n ) Items.Add( Value.Undefined );
return true;
}
if ( TryIndex( name, out int i ) )
{
while ( Items.Count <= i ) Items.Add( Value.Undefined );
Items[i] = v;
return true;
}
return false;
}
protected override bool HasNativeProperty( string name ) => name == "length" || (TryIndex( name, out int i ) && i < Items.Count);
public override bool Delete( string name )
{
if ( TryIndex( name, out int i ) && i < Items.Count ) { Items[i] = Value.Undefined; return true; }
return base.Delete( name );
}
public override IEnumerable<string> NativeKeys
{
get
{
for ( int i = Items.Count - 1; i >= 0; i-- )
if ( !Items[i].IsUndefined ) yield return i.ToString();
}
}
}
/// <summary>Anything callable.</summary>
public abstract class AsFunction : AsObject
{
public string Name = "";
protected AsFunction( AsObject proto ) : base( proto ) { }
public override string ClassName => "Function";
public abstract Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null );
/// <summary>new F(args)</summary>
public virtual Value Construct( Avm1 vm, Value[] args )
{
var protoV = Get( "prototype", vm );
var obj = vm.NewObject( protoV.AsObj ?? vm.ObjectProto );
obj.DefineHidden( "__constructor__", Value.From( this ) );
if ( vm.SwfVersion < 7 ) obj.DefineHidden( "constructor", Value.From( this ) );
var r = Call( vm, Value.From( obj ), args, null );
if ( r.IsObject ) return r;
return Value.From( obj );
}
}
public delegate Value NativeCall( Avm1 vm, Value thisV, Value[] args );
public sealed class NativeFunction : AsFunction
{
readonly NativeCall call;
/// <summary>Optional: when called with new, builds the instance itself (Array, Date...).</summary>
public NativeCall Constructor;
public NativeFunction( AsObject proto, string name, NativeCall call ) : base( proto ) { Name = name; this.call = call; }
public override Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null ) => call( vm, thisV, args );
public override Value Construct( Avm1 vm, Value[] args )
{
if ( Constructor != null )
{
var r = Constructor( vm, Value.Undefined, args );
if ( r.IsObject && !r.Obj.HasOwnProperty( "__constructor__" ) ) r.Obj.DefineHidden( "__constructor__", Value.From( this ) );
return r;
}
return base.Construct( vm, args );
}
}
/// <summary>A function defined in bytecode (DefineFunction / DefineFunction2).</summary>
public sealed class BytecodeFunction : AsFunction
{
public byte[] Code;
public int Start, Length;
public string[] ParamNames;
public byte[] ParamRegisters; // DefineFunction2 only (0 = no register)
public bool IsFunction2;
public int RegisterCount;
public ushort Flags2;
public string[] ConstantPool;
public Scope Scope; // captured scope chain
public DisplayObject BaseClip; // timeline the function was defined on
public int SwfVersion;
public BytecodeFunction( AsObject proto ) : base( proto ) { }
public override Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null )
=> vm.InvokeBytecode( this, thisV, args, superCtorOwner );
}
/// <summary>Primitive wrapper objects (new String("x"), (5).toString(), ...).</summary>
public sealed class AsBoxed : AsObject
{
public Value Primitive;
public AsBoxed( AsObject proto, Value v ) : base( proto ) { Primitive = v; }
public override string ClassName => Primitive.Kind switch { ValueKind.String => "String", ValueKind.Number => "Number", _ => "Boolean" };
protected override bool TryGetNative( string name, Avm1 vm, out Value v )
{
if ( Primitive.IsString && name == "length" ) { v = Primitive.Str.Length; return true; }
v = default; return false;
}
public override Value ToPrimitive( Avm1 vm, bool hintString ) => Primitive;
}