Flash/Core/Builtins.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Flash;
/// <summary>The ActionScript 2 standard library (the parts SS2 uses, plus the obvious neighbours).</summary>
public static class Builtins
{
static Value Arg( Value[] a, int i ) => i < a.Length ? a[i] : Value.Undefined;
public static void Install( Avm1 vm )
{
var objectProto = new AsObject( null );
vm.ObjectProto = objectProto;
var functionProto = new AsObject( objectProto );
vm.FunctionProto = functionProto;
var global = new AsObject( objectProto );
vm.Global = global;
InstallObject( vm );
InstallFunction( vm );
InstallArray( vm );
InstallString( vm );
InstallNumberBoolean( vm );
InstallMath( vm );
InstallDate( vm );
InstallGlobals( vm );
InstallMovieClip( vm );
InstallTextFieldButton( vm );
InstallSound( vm );
InstallColor( vm );
InstallKeyMouseStage( vm );
InstallSharedObject( vm );
InstallBroadcaster( vm );
}
static NativeFunction Ctor( Avm1 vm, string name, AsObject proto, NativeCall call, NativeCall construct = null )
{
var f = vm.Native( name, call );
f.Constructor = construct;
f.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum | PropFlags.DontDelete );
proto.DefineHidden( "constructor", Value.From( f ) );
vm.Global.DefineHidden( name, Value.From( f ) );
return f;
}
// ========================================================================= Object
static void InstallObject( Avm1 vm )
{
var p = vm.ObjectProto;
vm.ObjectCtor = Ctor( vm, "Object", p, ( vm, t, a ) =>
{
var v = Arg( a, 0 );
if ( v.IsObject ) return v;
if ( !v.IsNullish ) return Value.From( vm.ToObject( v ) );
return t.IsObject && !(t.Obj is DisplayObject) ? t : Value.From( vm.NewObject() );
}, ( vm, t, a ) =>
{
var v = Arg( a, 0 );
if ( v.IsObject ) return v;
if ( !v.IsNullish ) return Value.From( vm.ToObject( v ) );
return Value.From( vm.NewObject() );
} );
vm.Method( p, "hasOwnProperty", ( vm, t, a ) => t.IsObject && t.Obj.HasOwnProperty( Arg( a, 0 ).ToStr( vm ) ) );
vm.Method( p, "toString", ( vm, t, a ) => t.IsObject && t.Obj is AsFunction ? "[type Function]" : "[object Object]" );
vm.Method( p, "toLocaleString", ( vm, t, a ) => "[object Object]" );
vm.Method( p, "valueOf", ( vm, t, a ) => t );
vm.Method( p, "isPropertyEnumerable", ( vm, t, a ) =>
{
if ( !t.IsObject ) return false;
var pr = t.Obj.GetOwnProperty( Arg( a, 0 ).ToStr( vm ) );
return pr != null && (pr.Flags & PropFlags.DontEnum) == 0;
} );
vm.Method( p, "isPrototypeOf", ( vm, t, a ) =>
{
var o = Arg( a, 0 ).AsObj;
if ( !t.IsObject || o == null ) return false;
for ( var x = o.Proto; x != null; x = x.Proto ) if ( ReferenceEquals( x, t.Obj ) ) return true;
return false;
} );
vm.Method( p, "addProperty", ( vm, t, a ) =>
{
if ( !t.IsObject ) return false;
var name = Arg( a, 0 ).ToStr( vm );
var g = Arg( a, 1 ).AsObj as AsFunction;
var s = Arg( a, 2 ).AsObj as AsFunction;
if ( name.Length == 0 || g == null ) return false;
t.Obj.AddVirtual( name, g, s );
return true;
} );
vm.Method( p, "watch", ( vm, t, a ) => false );
vm.Method( p, "unwatch", ( vm, t, a ) => false );
var ctor = vm.ObjectCtor;
vm.Method( ctor, "registerClass", ( vm, t, a ) =>
{
var name = Arg( a, 0 ).ToStr( vm );
if ( Arg( a, 1 ).AsObj is AsFunction f ) vm.Player.RegisteredClasses[name] = f;
else vm.Player.RegisteredClasses.Remove( name );
return true;
} );
}
// ========================================================================= Function
static void InstallFunction( Avm1 vm )
{
var p = vm.FunctionProto;
vm.FunctionCtor = Ctor( vm, "Function", p, ( vm, t, a ) => Arg( a, 0 ), ( vm, t, a ) => Arg( a, 0 ).IsObject ? Arg( a, 0 ) : Value.From( vm.NewObject() ) );
vm.Method( p, "call", ( vm, t, a ) =>
{
if ( t.AsObj is not AsFunction f ) return Value.Undefined;
var thisArg = Arg( a, 0 );
var rest = a.Length > 1 ? a[1..] : Array.Empty<Value>();
return f.Call( vm, thisArg.IsNullish ? Value.Undefined : thisArg, rest );
} );
vm.Method( p, "apply", ( vm, t, a ) =>
{
if ( t.AsObj is not AsFunction f ) return Value.Undefined;
var thisArg = Arg( a, 0 );
var args = Array.Empty<Value>();
if ( Arg( a, 1 ).AsObj is AsArray arr ) args = arr.Items.ToArray();
else if ( Arg( a, 1 ).IsObject )
{
int n = Arg( a, 1 ).Obj.Get( "length", vm ).ToInt32( vm );
args = new Value[Math.Max( 0, n )];
for ( int i = 0; i < args.Length; i++ ) args[i] = Arg( a, 1 ).Obj.Get( i.ToString(), vm );
}
return f.Call( vm, thisArg.IsNullish ? Value.Undefined : thisArg, args );
} );
vm.Method( p, "toString", ( vm, t, a ) => "[type Function]" );
}
// ========================================================================= Array
static void InstallArray( Avm1 vm )
{
var p = new AsArray( vm.ObjectProto );
vm.ArrayProto = p;
NativeCall make = ( vm, t, a ) =>
{
var arr = vm.NewArray();
if ( a.Length == 1 && a[0].IsNumber )
{
int n = Value.ToInt32( a[0].Num );
for ( int i = 0; i < n; i++ ) arr.Items.Add( Value.Undefined );
}
else arr.Items.AddRange( a );
return arr;
};
vm.ArrayCtor = Ctor( vm, "Array", p, make, make );
var ctor = vm.ArrayCtor;
ctor.SetOwn( "CASEINSENSITIVE", 1 ); ctor.SetOwn( "DESCENDING", 2 ); ctor.SetOwn( "UNIQUESORT", 4 );
ctor.SetOwn( "RETURNINDEXEDARRAY", 8 ); ctor.SetOwn( "NUMERIC", 16 );
AsArray A( Value t ) => t.AsObj as AsArray;
vm.Method( p, "push", ( vm, t, a ) => { var arr = A( t ); if ( arr == null ) return Value.Undefined; arr.Items.AddRange( a ); return arr.Items.Count; } );
vm.Method( p, "pop", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null || arr.Items.Count == 0 ) return Value.Undefined;
var v = arr.Items[^1]; arr.Items.RemoveAt( arr.Items.Count - 1 ); return v;
} );
vm.Method( p, "shift", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null || arr.Items.Count == 0 ) return Value.Undefined;
var v = arr.Items[0]; arr.Items.RemoveAt( 0 ); return v;
} );
vm.Method( p, "unshift", ( vm, t, a ) => { var arr = A( t ); if ( arr == null ) return Value.Undefined; arr.Items.InsertRange( 0, a ); return arr.Items.Count; } );
vm.Method( p, "reverse", ( vm, t, a ) => { var arr = A( t ); arr?.Items.Reverse(); return t; } );
vm.Method( p, "join", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null ) return "";
var sep = Arg( a, 0 ).IsUndefined ? "," : Arg( a, 0 ).ToStr( vm );
return Join( vm, arr, sep );
} );
vm.Method( p, "toString", ( vm, t, a ) => { var arr = A( t ); return arr == null ? "" : Join( vm, arr, "," ); } );
vm.Method( p, "concat", ( vm, t, a ) =>
{
var arr = A( t );
var r = vm.NewArray( arr?.Items );
foreach ( var x in a ) { if ( x.AsObj is AsArray xa ) r.Items.AddRange( xa.Items ); else r.Items.Add( x ); }
return r;
} );
vm.Method( p, "slice", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null ) return vm.NewArray();
int n = arr.Items.Count;
int s = Clamp( Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToInt32( vm ), n );
int e = Clamp( Arg( a, 1 ).IsUndefined ? n : Arg( a, 1 ).ToInt32( vm ), n );
var r = vm.NewArray();
for ( int i = s; i < e; i++ ) r.Items.Add( arr.Items[i] );
return r;
} );
vm.Method( p, "splice", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null || a.Length == 0 ) return Value.Undefined;
int n = arr.Items.Count;
int s = Clamp( Arg( a, 0 ).ToInt32( vm ), n );
int del = a.Length > 1 ? Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n - s ) : n - s;
var removed = vm.NewArray( arr.Items.GetRange( s, del ) );
arr.Items.RemoveRange( s, del );
if ( a.Length > 2 ) arr.Items.InsertRange( s, a[2..] );
return removed;
} );
vm.Method( p, "sort", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null ) return Value.Undefined;
AsFunction cmp = Arg( a, 0 ).AsObj as AsFunction;
int flags = cmp != null ? Arg( a, 1 ).ToInt32( vm ) : (Arg( a, 0 ).IsNumber ? Arg( a, 0 ).ToInt32( vm ) : 0);
return SortArray( vm, arr, flags, ( x, y ) =>
{
if ( cmp != null ) return Math.Sign( vm.CallFunction( cmp, Value.Undefined, new[] { x, y } ).ToNumber( vm ) );
return CompareDefault( vm, x, y, flags );
} );
} );
vm.Method( p, "sortOn", ( vm, t, a ) =>
{
var arr = A( t ); if ( arr == null ) return Value.Undefined;
var fields = new List<string>();
if ( Arg( a, 0 ).AsObj is AsArray fa ) foreach ( var f in fa.Items ) fields.Add( f.ToStr( vm ) );
else fields.Add( Arg( a, 0 ).ToStr( vm ) );
int flags = Arg( a, 1 ).IsNumber ? Arg( a, 1 ).ToInt32( vm ) : 0;
return SortArray( vm, arr, flags, ( x, y ) =>
{
foreach ( var f in fields )
{
int c = CompareDefault( vm, vm.GetMember( x, f ), vm.GetMember( y, f ), flags );
if ( c != 0 ) return c;
}
return 0;
} );
} );
}
static int Clamp( int i, int n ) => i < 0 ? Math.Max( 0, n + i ) : Math.Min( i, n );
static string Join( Avm1 vm, AsArray arr, string sep )
{
var sb = new StringBuilder();
for ( int i = 0; i < arr.Items.Count; i++ )
{
if ( i > 0 ) sb.Append( sep );
var v = arr.Items[i];
if ( !v.IsNullish ) sb.Append( v.ToStr( vm ) );
else if ( v.IsNull ) sb.Append( "null" );
else sb.Append( "undefined" );
}
return sb.ToString();
}
static int CompareDefault( Avm1 vm, Value x, Value y, int flags )
{
int c;
if ( (flags & 16) != 0 )
{
double a = x.ToNumber( vm ), b = y.ToNumber( vm );
c = a < b ? -1 : a > b ? 1 : 0;
}
else
{
var a = x.ToStr( vm ); var b = y.ToStr( vm );
c = (flags & 1) != 0 ? string.Compare( a, b, StringComparison.OrdinalIgnoreCase ) : string.CompareOrdinal( a, b );
c = Math.Sign( c );
}
return (flags & 2) != 0 ? -c : c;
}
static Value SortArray( Avm1 vm, AsArray arr, int flags, Comparison<Value> cmp )
{
var idx = new List<int>();
for ( int i = 0; i < arr.Items.Count; i++ ) idx.Add( i );
var items = arr.Items;
// stable merge sort via index comparison
idx.Sort( ( i, j ) => { int c = cmp( items[i], items[j] ); return c != 0 ? c : i.CompareTo( j ); } );
if ( (flags & 4) != 0 )
for ( int k = 1; k < idx.Count; k++ ) if ( cmp( items[idx[k - 1]], items[idx[k]] ) == 0 ) return 0;
if ( (flags & 8) != 0 )
{
var r = vm.NewArray();
foreach ( var i in idx ) r.Items.Add( i );
return r;
}
var sorted = new List<Value>();
foreach ( var i in idx ) sorted.Add( items[i] );
arr.Items.Clear();
arr.Items.AddRange( sorted );
return Value.From( arr );
}
// ========================================================================= String
static void InstallString( Avm1 vm )
{
var p = new AsBoxed( vm.ObjectProto, "" );
vm.StringProto = p;
var ctor = Ctor( vm, "String", p, ( vm, t, a ) => a.Length == 0 ? "" : (Value)Arg( a, 0 ).ToStr( vm ),
( vm, t, a ) => Value.From( new AsBoxed( vm.StringProto, a.Length == 0 ? "" : Arg( a, 0 ).ToStr( vm ) ) ) );
vm.Method( ctor, "fromCharCode", ( vm, t, a ) =>
{
var sb = new StringBuilder();
foreach ( var c in a ) sb.Append( (char)c.ToInt32( vm ) );
return sb.ToString();
} );
string S( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToStr( vm ) : t.ToStr( vm );
vm.Method( p, "toString", ( vm, t, a ) => S( vm, t ) );
vm.Method( p, "valueOf", ( vm, t, a ) => S( vm, t ) );
vm.Method( p, "charAt", ( vm, t, a ) => { var s = S( vm, t ); int i = Arg( a, 0 ).ToInt32( vm ); return i >= 0 && i < s.Length ? s[i].ToString() : ""; } );
vm.Method( p, "charCodeAt", ( vm, t, a ) => { var s = S( vm, t ); int i = Arg( a, 0 ).ToInt32( vm ); return i >= 0 && i < s.Length ? s[i] : Value.NaN; } );
vm.Method( p, "concat", ( vm, t, a ) => { var sb = new StringBuilder( S( vm, t ) ); foreach ( var x in a ) sb.Append( x.ToStr( vm ) ); return sb.ToString(); } );
vm.Method( p, "indexOf", ( vm, t, a ) =>
{
var s = S( vm, t ); var f = Arg( a, 0 ).ToStr( vm );
int start = Arg( a, 1 ).IsUndefined ? 0 : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, s.Length );
return s.IndexOf( f, start, StringComparison.Ordinal );
} );
vm.Method( p, "lastIndexOf", ( vm, t, a ) =>
{
var s = S( vm, t ); var f = Arg( a, 0 ).ToStr( vm );
int start = Arg( a, 1 ).IsUndefined ? s.Length : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, s.Length );
if ( f.Length == 0 ) return Math.Min( start, s.Length );
int from = Math.Min( start + f.Length - 1, s.Length - 1 );
return from < 0 ? -1 : s.LastIndexOf( f, from, StringComparison.Ordinal );
} );
vm.Method( p, "slice", ( vm, t, a ) =>
{
var s = S( vm, t ); int n = s.Length;
int st = Clamp( Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToInt32( vm ), n );
int e = Clamp( Arg( a, 1 ).IsUndefined ? n : Arg( a, 1 ).ToInt32( vm ), n );
return e > st ? s.Substring( st, e - st ) : "";
} );
vm.Method( p, "substring", ( vm, t, a ) =>
{
var s = S( vm, t ); int n = s.Length;
int st = Math.Clamp( Arg( a, 0 ).ToInt32( vm ), 0, n );
int e = Arg( a, 1 ).IsUndefined ? n : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n );
if ( st > e ) (st, e) = (e, st);
return s.Substring( st, e - st );
} );
vm.Method( p, "substr", ( vm, t, a ) =>
{
var s = S( vm, t ); int n = s.Length;
int st = Clamp( Arg( a, 0 ).ToInt32( vm ), n );
int len = Arg( a, 1 ).IsUndefined ? n - st : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n - st );
return s.Substring( st, len );
} );
vm.Method( p, "split", ( vm, t, a ) =>
{
var s = S( vm, t );
var r = vm.NewArray();
if ( Arg( a, 0 ).IsUndefined ) { r.Items.Add( s ); return r; }
var sep = Arg( a, 0 ).ToStr( vm );
int limit = Arg( a, 1 ).IsUndefined ? int.MaxValue : Arg( a, 1 ).ToInt32( vm );
if ( sep.Length == 0 ) { foreach ( var ch in s ) { if ( r.Items.Count >= limit ) break; r.Items.Add( ch.ToString() ); } return r; }
foreach ( var part in s.Split( sep ) ) { if ( r.Items.Count >= limit ) break; r.Items.Add( part ); }
return r;
} );
vm.Method( p, "toLowerCase", ( vm, t, a ) => S( vm, t ).ToLowerInvariant() );
vm.Method( p, "toUpperCase", ( vm, t, a ) => S( vm, t ).ToUpperInvariant() );
}
// ========================================================================= Number / Boolean
static void InstallNumberBoolean( Avm1 vm )
{
var np = new AsBoxed( vm.ObjectProto, 0 );
vm.NumberProto = np;
var nc = Ctor( vm, "Number", np, ( vm, t, a ) => a.Length == 0 ? 0 : Arg( a, 0 ).ToNumber( vm ),
( vm, t, a ) => Value.From( new AsBoxed( vm.NumberProto, a.Length == 0 ? 0 : Arg( a, 0 ).ToNumber( vm ) ) ) );
nc.SetOwn( "MAX_VALUE", double.MaxValue ); nc.SetOwn( "MIN_VALUE", double.Epsilon ); nc.SetOwn( "NaN", double.NaN );
nc.SetOwn( "POSITIVE_INFINITY", double.PositiveInfinity ); nc.SetOwn( "NEGATIVE_INFINITY", double.NegativeInfinity );
double N( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToNumber( vm ) : t.ToNumber( vm );
vm.Method( np, "valueOf", ( vm, t, a ) => N( vm, t ) );
vm.Method( np, "toString", ( vm, t, a ) =>
{
double d = N( vm, t );
int radix = Arg( a, 0 ).IsUndefined ? 10 : Arg( a, 0 ).ToInt32( vm );
if ( radix == 10 || radix < 2 || radix > 36 || double.IsNaN( d ) || double.IsInfinity( d ) ) return Value.NumberToString( d );
long n = (long)d;
bool neg = n < 0; if ( neg ) n = -n;
var sb = new StringBuilder();
do { int dg = (int)(n % radix); sb.Insert( 0, (char)(dg < 10 ? '0' + dg : 'a' + dg - 10) ); n /= radix; } while ( n > 0 );
if ( neg ) sb.Insert( 0, '-' );
return sb.ToString();
} );
var bp = new AsBoxed( vm.ObjectProto, false );
vm.BooleanProto = bp;
Ctor( vm, "Boolean", bp, ( vm, t, a ) => Arg( a, 0 ).ToBool( vm.SwfVersion ),
( vm, t, a ) => Value.From( new AsBoxed( vm.BooleanProto, Arg( a, 0 ).ToBool( vm.SwfVersion ) ) ) );
bool B( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToBool( vm.SwfVersion ) : t.ToBool( vm.SwfVersion );
vm.Method( bp, "valueOf", ( vm, t, a ) => B( vm, t ) );
vm.Method( bp, "toString", ( vm, t, a ) => B( vm, t ) ? "true" : "false" );
}
// ========================================================================= Math
static void InstallMath( Avm1 vm )
{
var m = vm.NewObject();
vm.Global.DefineHidden( "Math", Value.From( m ) );
double D( Avm1 vm, Value[] a, int i ) => Arg( a, i ).ToNumber( vm );
vm.Method( m, "abs", ( vm, t, a ) => Math.Abs( D( vm, a, 0 ) ) );
vm.Method( m, "acos", ( vm, t, a ) => Math.Acos( D( vm, a, 0 ) ) );
vm.Method( m, "asin", ( vm, t, a ) => Math.Asin( D( vm, a, 0 ) ) );
vm.Method( m, "atan", ( vm, t, a ) => Math.Atan( D( vm, a, 0 ) ) );
vm.Method( m, "atan2", ( vm, t, a ) => Math.Atan2( D( vm, a, 0 ), D( vm, a, 1 ) ) );
vm.Method( m, "ceil", ( vm, t, a ) => Math.Ceiling( D( vm, a, 0 ) ) );
vm.Method( m, "cos", ( vm, t, a ) => Math.Cos( D( vm, a, 0 ) ) );
vm.Method( m, "exp", ( vm, t, a ) => Math.Exp( D( vm, a, 0 ) ) );
vm.Method( m, "floor", ( vm, t, a ) => Math.Floor( D( vm, a, 0 ) ) );
vm.Method( m, "log", ( vm, t, a ) => Math.Log( D( vm, a, 0 ) ) );
vm.Method( m, "max", ( vm, t, a ) =>
{
if ( a.Length == 0 ) return double.NegativeInfinity;
double r = double.NegativeInfinity;
foreach ( var x in a ) { var d = x.ToNumber( vm ); if ( double.IsNaN( d ) ) return double.NaN; r = Math.Max( r, d ); }
return r;
} );
vm.Method( m, "min", ( vm, t, a ) =>
{
if ( a.Length == 0 ) return double.PositiveInfinity;
double r = double.PositiveInfinity;
foreach ( var x in a ) { var d = x.ToNumber( vm ); if ( double.IsNaN( d ) ) return double.NaN; r = Math.Min( r, d ); }
return r;
} );
vm.Method( m, "pow", ( vm, t, a ) => Math.Pow( D( vm, a, 0 ), D( vm, a, 1 ) ) );
vm.Method( m, "random", ( vm, t, a ) => vm.Rng.NextDouble() );
vm.Method( m, "round", ( vm, t, a ) => Math.Floor( D( vm, a, 0 ) + 0.5 ) );
vm.Method( m, "sin", ( vm, t, a ) => Math.Sin( D( vm, a, 0 ) ) );
vm.Method( m, "sqrt", ( vm, t, a ) => Math.Sqrt( D( vm, a, 0 ) ) );
vm.Method( m, "tan", ( vm, t, a ) => Math.Tan( D( vm, a, 0 ) ) );
m.DefineHidden( "PI", Math.PI ); m.DefineHidden( "E", Math.E ); m.DefineHidden( "LN2", Math.Log( 2 ) ); m.DefineHidden( "LN10", Math.Log( 10 ) );
m.DefineHidden( "LOG2E", 1 / Math.Log( 2 ) ); m.DefineHidden( "LOG10E", 1 / Math.Log( 10 ) ); m.DefineHidden( "SQRT1_2", Math.Sqrt( 0.5 ) ); m.DefineHidden( "SQRT2", Math.Sqrt( 2 ) );
}
// ========================================================================= Date
static void InstallDate( Avm1 vm )
{
var p = vm.NewObject();
vm.DateProto = p;
NativeCall make = ( vm, t, a ) =>
{
var o = new AsObject( vm.DateProto );
DateTime dt;
if ( a.Length == 0 ) dt = DateTime.Now;
else if ( a.Length == 1 ) dt = new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ).AddMilliseconds( Arg( a, 0 ).ToNumber( vm ) ).ToLocalTime();
else
{
int y = Arg( a, 0 ).ToInt32( vm ); if ( y < 100 ) y += 1900;
dt = new DateTime( y, 1, 1 ).AddMonths( Arg( a, 1 ).ToInt32( vm ) ).AddDays( (a.Length > 2 ? Arg( a, 2 ).ToInt32( vm ) : 1) - 1 )
.AddHours( Arg( a, 3 ).IsUndefined ? 0 : Arg( a, 3 ).ToInt32( vm ) ).AddMinutes( Arg( a, 4 ).IsUndefined ? 0 : Arg( a, 4 ).ToInt32( vm ) )
.AddSeconds( Arg( a, 5 ).IsUndefined ? 0 : Arg( a, 5 ).ToInt32( vm ) );
}
o.Native = dt;
return Value.From( o );
};
Ctor( vm, "Date", p, ( vm, t, a ) => DateTime.Now.ToString( "ddd MMM d HH:mm:ss yyyy", CultureInfo.InvariantCulture ), make );
DateTime Dt( Value t ) => t.AsObj?.Native is DateTime d ? d : DateTime.Now;
vm.Method( p, "getFullYear", ( vm, t, a ) => Dt( t ).Year );
vm.Method( p, "getYear", ( vm, t, a ) => Dt( t ).Year - 1900 );
vm.Method( p, "getMonth", ( vm, t, a ) => Dt( t ).Month - 1 );
vm.Method( p, "getDate", ( vm, t, a ) => Dt( t ).Day );
vm.Method( p, "getDay", ( vm, t, a ) => (int)Dt( t ).DayOfWeek );
vm.Method( p, "getHours", ( vm, t, a ) => Dt( t ).Hour );
vm.Method( p, "getMinutes", ( vm, t, a ) => Dt( t ).Minute );
vm.Method( p, "getSeconds", ( vm, t, a ) => Dt( t ).Second );
vm.Method( p, "getMilliseconds", ( vm, t, a ) => Dt( t ).Millisecond );
vm.Method( p, "getTime", ( vm, t, a ) => (Dt( t ).ToUniversalTime() - new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc )).TotalMilliseconds );
vm.Method( p, "valueOf", ( vm, t, a ) => (Dt( t ).ToUniversalTime() - new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc )).TotalMilliseconds );
vm.Method( p, "getTimezoneOffset", ( vm, t, a ) => -(Dt( t ) - Dt( t ).ToUniversalTime()).TotalMinutes );
vm.Method( p, "toString", ( vm, t, a ) => Dt( t ).ToString( "ddd MMM d HH:mm:ss yyyy", CultureInfo.InvariantCulture ) );
}
// ========================================================================= global functions
static void InstallGlobals( Avm1 vm )
{
var g = vm.Global;
vm.Method( g, "trace", ( vm, t, a ) => { vm.Trace( Arg( a, 0 ).ToStr( vm ) ); return Value.Undefined; } );
vm.Method( g, "isNaN", ( vm, t, a ) => double.IsNaN( Arg( a, 0 ).ToNumber( vm ) ) );
vm.Method( g, "isFinite", ( vm, t, a ) => { var d = Arg( a, 0 ).ToNumber( vm ); return !double.IsNaN( d ) && !double.IsInfinity( d ); } );
vm.Method( g, "parseInt", ( vm, t, a ) =>
{
var s = Arg( a, 0 ).ToStr( vm ).Trim();
int radix = Arg( a, 1 ).IsUndefined ? 0 : Arg( a, 1 ).ToInt32( vm );
bool neg = false; int i = 0;
if ( i < s.Length && (s[i] == '-' || s[i] == '+') ) { neg = s[i] == '-'; i++; }
if ( radix == 0 )
{
if ( i + 1 < s.Length && s[i] == '0' && (s[i + 1] == 'x' || s[i + 1] == 'X') ) { radix = 16; i += 2; }
else if ( i + 1 < s.Length && s[i] == '0' ) radix = 8;
else radix = 10;
}
else if ( radix == 16 && i + 1 < s.Length && s[i] == '0' && (s[i + 1] == 'x' || s[i + 1] == 'X') ) i += 2;
double r = 0; bool any = false;
for ( ; i < s.Length; i++ )
{
char c = char.ToLowerInvariant( s[i] );
int d = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'z' ? c - 'a' + 10 : 99;
if ( d >= radix ) break;
r = r * radix + d; any = true;
}
return any ? (neg ? -r : r) : double.NaN;
} );
vm.Method( g, "parseFloat", ( vm, t, a ) =>
{
var s = Arg( a, 0 ).ToStr( vm ).TrimStart();
int i = 0;
if ( i < s.Length && (s[i] == '-' || s[i] == '+') ) i++;
while ( i < s.Length && char.IsDigit( s[i] ) ) i++;
if ( i < s.Length && s[i] == '.' ) { i++; while ( i < s.Length && char.IsDigit( s[i] ) ) i++; }
if ( i < s.Length && (s[i] == 'e' || s[i] == 'E') )
{
int j = i + 1; if ( j < s.Length && (s[j] == '-' || s[j] == '+') ) j++;
if ( j < s.Length && char.IsDigit( s[j] ) ) { i = j; while ( i < s.Length && char.IsDigit( s[i] ) ) i++; }
}
return double.TryParse( s.Substring( 0, i ), NumberStyles.Float, CultureInfo.InvariantCulture, out var r ) ? r : double.NaN;
} );
vm.Method( g, "escape", ( vm, t, a ) =>
{
var s = Arg( a, 0 ).ToStr( vm ); var sb = new StringBuilder();
foreach ( var b in Encoding.UTF8.GetBytes( s ) )
{
char c = (char)b;
if ( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "@*_+-./".IndexOf( c ) >= 0 ) sb.Append( c );
else sb.Append( '%' ).Append( b.ToString( "X2" ) );
}
return sb.ToString();
} );
vm.Method( g, "unescape", ( vm, t, a ) =>
{
var s = Arg( a, 0 ).ToStr( vm ); var bytes = new List<byte>();
for ( int i = 0; i < s.Length; i++ )
{
if ( s[i] == '%' && i + 2 < s.Length && int.TryParse( s.Substring( i + 1, 2 ), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h ) ) { bytes.Add( (byte)h ); i += 2; }
else if ( s[i] == '+' ) bytes.Add( (byte)' ' );
else bytes.AddRange( Encoding.UTF8.GetBytes( s[i].ToString() ) );
}
return Encoding.UTF8.GetString( bytes.ToArray() );
} );
vm.Method( g, "getTimer", ( vm, t, a ) => Math.Floor( vm.Player.TimeMs ) );
vm.Method( g, "getVersion", ( vm, t, a ) => "WIN 11,0,1,152" );
vm.Method( g, "updateAfterEvent", ( vm, t, a ) => Value.Undefined );
vm.Method( g, "random", ( vm, t, a ) => { int n = Arg( a, 0 ).ToInt32( vm ); return n <= 0 ? 0 : vm.Rng.Next( n ); } );
vm.Method( g, "int", ( vm, t, a ) => (double)Arg( a, 0 ).ToInt32( vm ) );
vm.Method( g, "targetPath", ( vm, t, a ) => Arg( a, 0 ).AsObj is DisplayObject d ? (Value)d.TargetPath : Value.Undefined );
vm.Method( g, "setInterval", ( vm, t, a ) =>
{
if ( Arg( a, 0 ).AsObj is AsFunction f ) return vm.Player.SetInterval( f, Value.Undefined, Arg( a, 1 ).ToNumber( vm ), a.Length > 2 ? a[2..] : Array.Empty<Value>() );
if ( Arg( a, 0 ).IsObject )
{
var obj = Arg( a, 0 );
var fm = vm.GetMember( obj, Arg( a, 1 ).ToStr( vm ) ).AsObj as AsFunction;
if ( fm != null ) return vm.Player.SetInterval( fm, obj, Arg( a, 2 ).ToNumber( vm ), a.Length > 3 ? a[3..] : Array.Empty<Value>() );
}
return Value.Undefined;
} );
vm.Method( g, "clearInterval", ( vm, t, a ) => { vm.Player.ClearInterval( Arg( a, 0 ).ToInt32( vm ) ); return Value.Undefined; } );
vm.Method( g, "setTimeout", ( vm, t, a ) =>
{
if ( Arg( a, 0 ).AsObj is AsFunction f ) return vm.Player.SetInterval( f, Value.Undefined, Arg( a, 1 ).ToNumber( vm ), a.Length > 2 ? a[2..] : Array.Empty<Value>(), true );
return Value.Undefined;
} );
vm.Method( g, "clearTimeout", ( vm, t, a ) => { vm.Player.ClearInterval( Arg( a, 0 ).ToInt32( vm ) ); return Value.Undefined; } );
vm.Method( g, "ASSetPropFlags", ( vm, t, a ) =>
{
var o = Arg( a, 0 ).AsObj; if ( o == null ) return Value.Undefined;
int set = Arg( a, 2 ).ToInt32( vm ), clear = Arg( a, 3 ).ToInt32( vm );
var names = new List<string>();
if ( Arg( a, 1 ).IsNull ) names.AddRange( o.OwnKeys );
else if ( Arg( a, 1 ).AsObj is AsArray arr ) foreach ( var x in arr.Items ) names.Add( x.ToStr( vm ) );
else names.AddRange( Arg( a, 1 ).ToStr( vm ).Split( ',' ) );
foreach ( var n in names ) o.SetFlags( n, (PropFlags)(set & 7), (PropFlags)(clear & 7) );
return Value.Undefined;
} );
g.DefineHidden( "NaN", double.NaN );
g.DefineHidden( "Infinity", double.PositiveInfinity );
var system = vm.NewObject();
var security = vm.NewObject();
vm.Method( security, "allowDomain", ( vm, t, a ) => Value.Undefined );
vm.Method( security, "allowInsecureDomain", ( vm, t, a ) => Value.Undefined );
system.SetOwn( "security", Value.From( security ) );
var caps = vm.NewObject();
caps.SetOwn( "version", "WIN 11,0,1,152" ); caps.SetOwn( "os", "Windows" ); caps.SetOwn( "playerType", "StandAlone" );
caps.SetOwn( "screenResolutionX", 1920 ); caps.SetOwn( "screenResolutionY", 1080 ); caps.SetOwn( "language", "en" );
system.SetOwn( "capabilities", Value.From( caps ) );
system.SetOwn( "useCodepage", false );
g.DefineHidden( "System", Value.From( system ) );
// TextFormat (plain property bag)
var tfp = vm.NewObject();
Ctor( vm, "TextFormat", tfp, ( vm, t, a ) =>
{
if ( !t.IsObject ) return Value.Undefined;
string[] names = { "font", "size", "color", "bold", "italic", "underline", "url", "target", "align", "leftMargin", "rightMargin", "indent", "leading" };
for ( int i = 0; i < names.Length; i++ ) t.Obj.Set( names[i], i < a.Length ? a[i] : Value.Null, vm );
return Value.Undefined;
} );
}
// ========================================================================= MovieClip
static MovieClip Clip( Value t ) => t.AsObj as MovieClip;
static DisplayObject Dobj( Value t ) => t.AsObj as DisplayObject;
static void InstallMovieClip( Avm1 vm )
{
var p = vm.NewObject();
vm.MovieClipProto = p;
Ctor( vm, "MovieClip", p, ( vm, t, a ) => Value.Undefined );
vm.Method( p, "play", ( vm, t, a ) => { Clip( t )?.Play(); return Value.Undefined; } );
vm.Method( p, "stop", ( vm, t, a ) => { Clip( t )?.Stop(); return Value.Undefined; } );
vm.Method( p, "nextFrame", ( vm, t, a ) => { Clip( t )?.NextFrame(); return Value.Undefined; } );
vm.Method( p, "prevFrame", ( vm, t, a ) => { Clip( t )?.PrevFrame(); return Value.Undefined; } );
NativeCall gotoFn( bool stop ) => ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
var f = Arg( a, 0 );
if ( f.IsString )
{
var n = Value.StringToNumber( f.Str, vm.SwfVersion );
if ( !double.IsNaN( n ) ) mc.GotoFrame( (int)n, stop ); else mc.GotoLabel( f.Str, stop );
}
else mc.GotoFrame( f.ToInt32( vm ), stop );
return Value.Undefined;
};
vm.Method( p, "gotoAndPlay", gotoFn( false ) );
vm.Method( p, "gotoAndStop", gotoFn( true ) );
vm.Method( p, "attachMovie", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
var id = Arg( a, 0 ).ToStr( vm ); var name = Arg( a, 1 ).ToStr( vm ); int depth = Arg( a, 2 ).ToInt32( vm );
var def = vm.Player.Lib.GetExport( id );
if ( def == null ) return Value.Undefined;
var obj = vm.Player.Instantiate( def.Id, mc );
if ( obj == null ) return Value.Undefined;
obj.Name = name;
mc.AddChild( obj, depth );
vm.Player.Construct( obj, Arg( a, 3 ).AsObj, byScript: true );
return Value.From( obj );
} );
vm.Method( p, "createEmptyMovieClip", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
var c = new MovieClip( vm.Player, null ) { Name = Arg( a, 0 ).ToStr( vm ) };
mc.AddChild( c, Arg( a, 1 ).ToInt32( vm ) );
vm.Player.Construct( c, null, true );
return Value.From( c );
} );
vm.Method( p, "duplicateMovieClip", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null || mc.Parent == null ) return Value.Undefined;
var name = Arg( a, 0 ).ToStr( vm ); int depth = Arg( a, 1 ).ToInt32( vm );
mc.Duplicate( name, depth );
var copy = mc.Parent.ChildAtDepth( depth );
if ( copy != null && Arg( a, 2 ).AsObj is AsObject init )
foreach ( var k in init.EnumerableKeys() ) copy.Set( k, init.Get( k, vm ), vm );
return copy != null ? Value.From( copy ) : Value.Undefined;
} );
vm.Method( p, "removeMovieClip", ( vm, t, a ) => { Dobj( t )?.RemoveFromScript(); return Value.Undefined; } );
vm.Method( p, "unloadMovie", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
foreach ( var c in mc.Children.ToArray() ) mc.RemoveChild( c );
return Value.Undefined;
} );
vm.Method( p, "loadMovie", ( vm, t, a ) => Value.Undefined );
vm.Method( p, "swapDepths", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null || mc.Parent == null ) return Value.Undefined;
var x = Arg( a, 0 );
if ( x.AsObj is DisplayObject other ) { if ( other.Parent == mc.Parent ) mc.SwapDepths( other.Depth ); }
else mc.SwapDepths( x.ToInt32( vm ) );
return Value.Undefined;
} );
vm.Method( p, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
vm.Method( p, "getNextHighestDepth", ( vm, t, a ) => Clip( t ) is { } mc ? mc.NextHighestDepth() : Value.Undefined );
vm.Method( p, "getInstanceAtDepth", ( vm, t, a ) => Clip( t )?.ChildAtDepth( Arg( a, 0 ).ToInt32( vm ) ) is { } c ? Value.From( c ) : Value.Undefined );
vm.Method( p, "getBytesLoaded", ( vm, t, a ) => 7586504 );
vm.Method( p, "getBytesTotal", ( vm, t, a ) => 7586504 );
vm.Method( p, "getSWFVersion", ( vm, t, a ) => vm.SwfVersion );
vm.Method( p, "getURL", ( vm, t, a ) => { vm.Player.GetUrl( Arg( a, 0 ).ToStr( vm ), Arg( a, 1 ).ToStr( vm ) ); return Value.Undefined; } );
vm.Method( p, "getBounds", ( vm, t, a ) =>
{
var d = Dobj( t ); if ( d == null ) return Value.Undefined;
var space = Arg( a, 0 ).AsObj as DisplayObject ?? d;
var m = space.WorldMatrix.Inverse().Mul( d.WorldMatrix );
var b = d.BoundsIn( m );
var r = vm.NewObject();
if ( b == null ) { r.Set( "xMin", 6710886.35, vm ); r.Set( "yMin", 6710886.35, vm ); r.Set( "xMax", 6710886.35, vm ); r.Set( "yMax", 6710886.35, vm ); return r; }
r.Set( "xMin", b.Value.x0, vm ); r.Set( "yMin", b.Value.y0, vm ); r.Set( "xMax", b.Value.x1, vm ); r.Set( "yMax", b.Value.y1, vm );
return r;
} );
vm.Method( p, "localToGlobal", ( vm, t, a ) =>
{
var d = Dobj( t ); var pt = Arg( a, 0 ).AsObj; if ( d == null || pt == null ) return Value.Undefined;
var (x, y) = d.WorldMatrix.Apply( pt.Get( "x", vm ).ToNumber( vm ), pt.Get( "y", vm ).ToNumber( vm ) );
pt.Set( "x", x, vm ); pt.Set( "y", y, vm );
return Value.Undefined;
} );
vm.Method( p, "globalToLocal", ( vm, t, a ) =>
{
var d = Dobj( t ); var pt = Arg( a, 0 ).AsObj; if ( d == null || pt == null ) return Value.Undefined;
var (x, y) = d.WorldMatrix.Inverse().Apply( pt.Get( "x", vm ).ToNumber( vm ), pt.Get( "y", vm ).ToNumber( vm ) );
pt.Set( "x", x, vm ); pt.Set( "y", y, vm );
return Value.Undefined;
} );
vm.Method( p, "hitTest", ( vm, t, a ) =>
{
var d = Dobj( t ); if ( d == null ) return false;
if ( a.Length >= 2 )
{
double x = Arg( a, 0 ).ToNumber( vm ), y = Arg( a, 1 ).ToNumber( vm );
bool shape = Arg( a, 2 ).ToBool( vm.SwfVersion );
if ( shape ) return d.HitTestPoint( x, y, true );
var b = d.BoundsIn( d.WorldMatrix );
return b != null && x >= b.Value.x0 && x <= b.Value.x1 && y >= b.Value.y0 && y <= b.Value.y1;
}
var other = Arg( a, 0 ).AsObj as DisplayObject;
if ( other == null ) return false;
var b1 = d.BoundsIn( d.WorldMatrix ); var b2 = other.BoundsIn( other.WorldMatrix );
if ( b1 == null || b2 == null ) return false;
return b1.Value.x0 <= b2.Value.x1 && b2.Value.x0 <= b1.Value.x1 && b1.Value.y0 <= b2.Value.y1 && b2.Value.y0 <= b1.Value.y1;
} );
vm.Method( p, "setMask", ( vm, t, a ) =>
{
var d = Dobj( t ); if ( d == null ) return Value.Undefined;
if ( d.MaskedBy != null ) { d.MaskedBy.MaskFor = null; d.MaskedBy = null; }
if ( Arg( a, 0 ).AsObj is DisplayObject m ) { d.MaskedBy = m; m.MaskFor = d; }
return Value.Undefined;
} );
vm.Method( p, "startDrag", ( vm, t, a ) =>
{
var d = Dobj( t ); if ( d == null ) return Value.Undefined;
double[] rect = a.Length >= 5 ? new[] { Arg( a, 1 ).ToNumber( vm ), Arg( a, 2 ).ToNumber( vm ), Arg( a, 3 ).ToNumber( vm ), Arg( a, 4 ).ToNumber( vm ) } : null;
vm.Player.StartDrag( d, Arg( a, 0 ).ToBool( vm.SwfVersion ), rect );
return Value.Undefined;
} );
vm.Method( p, "stopDrag", ( vm, t, a ) => { vm.Player.StopDrag(); return Value.Undefined; } );
vm.Method( p, "createTextField", ( vm, t, a ) =>
{
var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
var def = new EditTextDef { X0 = Arg( a, 2 ).ToNumber( vm ), Y0 = Arg( a, 3 ).ToNumber( vm ), Size = 12, FontId = 0, Color = new double[] { 0, 0, 0, 255 } };
def.X1 = def.X0 + Arg( a, 4 ).ToNumber( vm ); def.Y1 = def.Y0 + Arg( a, 5 ).ToNumber( vm );
var tf = new EditText( vm.Player, def ) { Name = Arg( a, 0 ).ToStr( vm ) };
mc.AddChild( tf, Arg( a, 1 ).ToInt32( vm ) );
return Value.From( tf );
} );
}
static void InstallTextFieldButton( Avm1 vm )
{
var tp = vm.NewObject();
vm.TextFieldProto = tp;
Ctor( vm, "TextField", tp, ( vm, t, a ) => Value.Undefined );
vm.Method( tp, "setTextFormat", ( vm, t, a ) => Value.Undefined );
vm.Method( tp, "setNewTextFormat", ( vm, t, a ) => Value.Undefined );
vm.Method( tp, "getTextFormat", ( vm, t, a ) => Value.From( vm.NewObject() ) );
vm.Method( tp, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
vm.Method( tp, "removeTextField", ( vm, t, a ) => { Dobj( t )?.RemoveFromScript(); return Value.Undefined; } );
vm.Method( tp, "replaceSel", ( vm, t, a ) => Value.Undefined );
var bp = vm.NewObject();
vm.ButtonProto = bp;
Ctor( vm, "Button", bp, ( vm, t, a ) => Value.Undefined );
vm.Method( bp, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
}
// ========================================================================= Sound
sealed class SoundState
{
public DisplayObject Target;
public SoundDef Def;
public object Handle;
public double Volume = 100, Pan = 0;
public double StartedAt;
public bool Completed;
}
static readonly List<(AsObject obj, SoundState st)> liveSounds = new();
public static void StopAllSoundObjects( Player p )
{
foreach ( var (o, st) in liveSounds ) if ( st.Handle != null ) { p.Host?.StopSound( st.Handle ); st.Handle = null; }
}
/// <summary>Volume is per target clip in Flash: all Sound objects on the same target share it.</summary>
static readonly Dictionary<DisplayObject, double> targetVolume = new();
static void InstallSound( Avm1 vm )
{
var p = vm.NewObject();
vm.SoundProto = p;
Ctor( vm, "Sound", p, ( vm, t, a ) =>
{
if ( !t.IsObject ) return Value.Undefined;
t.Obj.Native = new SoundState { Target = Arg( a, 0 ).AsObj as DisplayObject };
return Value.Undefined;
} );
SoundState St( Value t ) => t.AsObj?.Native as SoundState;
double Vol( SoundState s ) => IsGlobal( vm.Player, s ) ? vm.Player.GlobalVolume * 100 : (targetVolume.TryGetValue( s.Target, out var v ) ? v : 100);
vm.Method( p, "attachSound", ( vm, t, a ) =>
{
var s = St( t ); if ( s == null ) return Value.Undefined;
s.Def = vm.Player.Lib.GetExport( Arg( a, 0 ).ToStr( vm ) ) as SoundDef;
return Value.Undefined;
} );
vm.Method( p, "start", ( vm, t, a ) =>
{
var s = St( t ); if ( s?.Def == null ) return Value.Undefined;
double offset = Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToNumber( vm );
int loops = Arg( a, 1 ).IsUndefined ? 1 : Math.Max( 1, Arg( a, 1 ).ToInt32( vm ) );
if ( s.Handle != null ) { vm.Player.Host?.StopSound( s.Handle ); s.Handle = null; }
s.Handle = vm.Player.Host?.PlaySound( s.Def, Effective( vm.Player, s ), s.Pan / 100.0, loops, double.IsNaN( offset ) ? 0 : offset );
s.StartedAt = vm.Player.TimeMs;
s.Completed = false;
liveSounds.RemoveAll( x => ReferenceEquals( x.obj, t.Obj ) );
liveSounds.Add( (t.Obj, s) );
return Value.Undefined;
} );
vm.Method( p, "stop", ( vm, t, a ) =>
{
var s = St( t ); if ( s == null ) return Value.Undefined;
if ( !Arg( a, 0 ).IsUndefined )
{
var def = vm.Player.Lib.GetExport( Arg( a, 0 ).ToStr( vm ) );
foreach ( var (o, st) in liveSounds ) if ( st.Def == def && st.Handle != null ) { vm.Player.Host?.StopSound( st.Handle ); st.Handle = null; }
return Value.Undefined;
}
if ( s.Target == null || s.Target == vm.Player.Root ) vm.Player.StopAllSounds();
else
{
// Flash: stop() on a targeted Sound stops every sound attached to that clip
foreach ( var (o, st) in liveSounds )
if ( st.Target == s.Target && st.Handle != null ) { vm.Player.Host?.StopSound( st.Handle ); st.Handle = null; st.Completed = true; }
if ( s.Handle != null ) { vm.Player.Host?.StopSound( s.Handle ); s.Handle = null; }
}
return Value.Undefined;
} );
vm.Method( p, "setVolume", ( vm, t, a ) =>
{
var s = St( t ); if ( s == null ) return Value.Undefined;
var v = Arg( a, 0 ).ToNumber( vm );
if ( double.IsNaN( v ) ) return Value.Undefined;
s.Volume = v;
if ( IsGlobal( vm.Player, s ) ) vm.Player.SetGlobalVolume( v / 100.0 );
else { targetVolume[s.Target] = v; RefreshSoundVolumes( vm.Player ); }
return Value.Undefined;
} );
vm.Method( p, "getVolume", ( vm, t, a ) => St( t ) is { } s ? Vol( s ) : 100 );
vm.Method( p, "setPan", ( vm, t, a ) => { if ( St( t ) is { } s ) s.Pan = Arg( a, 0 ).ToNumber( vm ); return Value.Undefined; } );
vm.Method( p, "getPan", ( vm, t, a ) => St( t )?.Pan ?? 0 );
vm.Method( p, "loadSound", ( vm, t, a ) => Value.Undefined );
vm.Method( p, "getBytesLoaded", ( vm, t, a ) => 1 );
vm.Method( p, "getBytesTotal", ( vm, t, a ) => 1 );
p.AddVirtual( "position", vm.Native( "position", ( vm, t, a ) =>
{
var s = St( t ); if ( s?.Handle == null ) return 0;
return Math.Floor( (vm.Player.Host?.SoundPosition( s.Handle ) ?? 0) * 1000 );
} ), null );
p.AddVirtual( "duration", vm.Native( "duration", ( vm, t, a ) => St( t )?.Def is { } d ? Math.Floor( d.Seconds * 1000 ) : 0 ), null );
}
static bool IsGlobal( Player p, SoundState s ) => s.Target == null || s.Target == p.Root;
/// <summary>Flash: a sound's volume is its target clip's volume times the global (no-target) volume.</summary>
static double Effective( Player p, SoundState s )
{
double t = !IsGlobal( p, s ) && targetVolume.TryGetValue( s.Target, out var v ) ? v / 100.0 : 1;
return Math.Max( 0, t * p.GlobalVolume );
}
public static void RefreshSoundVolumes( Player p )
{
foreach ( var (o, st) in liveSounds )
if ( st.Handle != null ) p.Host?.SetSoundVolume( st.Handle, Effective( p, st ), st.Pan / 100.0 );
}
/// <summary>Fire onSoundComplete for finished sounds (call once per frame).</summary>
public static void UpdateSounds( Player p )
{
foreach ( var (o, st) in liveSounds.ToArray() )
{
if ( st.Handle == null || st.Completed ) continue;
if ( p.Host != null && !p.Host.IsSoundPlaying( st.Handle ) )
{
st.Completed = true;
st.Handle = null;
var f = o.Get( "onSoundComplete", p.Vm ).AsObj as AsFunction;
if ( f != null ) p.QueueCall( f, Value.From( o ), Array.Empty<Value>() );
}
}
liveSounds.RemoveAll( x => x.st.Completed && x.st.Handle == null );
}
// ========================================================================= Color
static void InstallColor( Avm1 vm )
{
var p = vm.NewObject();
vm.ColorProto = p;
Ctor( vm, "Color", p, ( vm, t, a ) =>
{
if ( !t.IsObject ) return Value.Undefined;
var target = Arg( a, 0 ).AsObj as DisplayObject;
if ( target == null && Arg( a, 0 ).IsString ) target = vm.ResolveTargetPath( vm.Player.Root, Arg( a, 0 ).Str );
t.Obj.Native = target;
return Value.Undefined;
} );
DisplayObject T( Value t ) => t.AsObj?.Native as DisplayObject;
vm.Method( p, "setRGB", ( vm, t, a ) =>
{
var d = T( t ); if ( d == null ) return Value.Undefined;
int c = Arg( a, 0 ).ToInt32( vm );
d.Cx = new ColorTransform { Rm = 0, Gm = 0, Bm = 0, Am = d.Cx.Am, Ra = (c >> 16) & 255, Ga = (c >> 8) & 255, Ba = c & 255, Aa = d.Cx.Aa };
d.ScriptTransformed = true;
return Value.Undefined;
} );
vm.Method( p, "getRGB", ( vm, t, a ) =>
{
var d = T( t ); if ( d == null ) return 0;
return ((int)d.Cx.Ra << 16) | ((int)d.Cx.Ga << 8) | (int)d.Cx.Ba;
} );
vm.Method( p, "setTransform", ( vm, t, a ) =>
{
var d = T( t ); var o = Arg( a, 0 ).AsObj; if ( d == null || o == null ) return Value.Undefined;
var cx = d.Cx;
double G( string n, double def ) { var v = o.Get( n, vm ); return v.IsUndefined ? def : v.ToNumber( vm ); }
cx.Rm = G( "ra", cx.Rm * 100 ) / 100; cx.Gm = G( "ga", cx.Gm * 100 ) / 100; cx.Bm = G( "ba", cx.Bm * 100 ) / 100; cx.Am = G( "aa", cx.Am * 100 ) / 100;
cx.Ra = G( "rb", cx.Ra ); cx.Ga = G( "gb", cx.Ga ); cx.Ba = G( "bb", cx.Ba ); cx.Aa = G( "ab", cx.Aa );
d.Cx = cx;
d.ScriptTransformed = true;
return Value.Undefined;
} );
vm.Method( p, "getTransform", ( vm, t, a ) =>
{
var d = T( t ); var r = vm.NewObject(); if ( d == null ) return r;
r.Set( "ra", d.Cx.Rm * 100, vm ); r.Set( "ga", d.Cx.Gm * 100, vm ); r.Set( "ba", d.Cx.Bm * 100, vm ); r.Set( "aa", d.Cx.Am * 100, vm );
r.Set( "rb", d.Cx.Ra, vm ); r.Set( "gb", d.Cx.Ga, vm ); r.Set( "bb", d.Cx.Ba, vm ); r.Set( "ab", d.Cx.Aa, vm );
return r;
} );
}
// ========================================================================= Key / Mouse / Stage
static void InstallKeyMouseStage( Avm1 vm )
{
var key = vm.NewObject();
vm.Global.DefineHidden( "Key", Value.From( key ) );
MakeBroadcaster( vm, key );
vm.Method( key, "isDown", ( vm, t, a ) => vm.Player.KeysDown.Contains( Arg( a, 0 ).ToInt32( vm ) ) );
vm.Method( key, "getCode", ( vm, t, a ) => vm.Player.LastKey );
vm.Method( key, "getAscii", ( vm, t, a ) => vm.Player.LastAscii );
vm.Method( key, "isToggled", ( vm, t, a ) => false );
var codes = new (string, int)[] { ("BACKSPACE", 8), ("TAB", 9), ("ENTER", 13), ("SHIFT", 16), ("CONTROL", 17), ("CAPSLOCK", 20), ("ESCAPE", 27), ("SPACE", 32),
("PGUP", 33), ("PGDN", 34), ("END", 35), ("HOME", 36), ("LEFT", 37), ("UP", 38), ("RIGHT", 39), ("DOWN", 40), ("INSERT", 45), ("DELETEKEY", 46) };
foreach ( var (n, c) in codes ) key.DefineHidden( n, c );
var mouse = vm.NewObject();
vm.Global.DefineHidden( "Mouse", Value.From( mouse ) );
MakeBroadcaster( vm, mouse );
vm.Method( mouse, "show", ( vm, t, a ) => 1 );
vm.Method( mouse, "hide", ( vm, t, a ) => 1 );
var stage = vm.NewObject();
vm.Global.DefineHidden( "Stage", Value.From( stage ) );
MakeBroadcaster( vm, stage );
stage.AddVirtual( "width", vm.Native( "width", ( vm, t, a ) => vm.Player.Lib.StageWidth ), null );
stage.AddVirtual( "height", vm.Native( "height", ( vm, t, a ) => vm.Player.Lib.StageHeight ), null );
stage.SetOwn( "scaleMode", "showAll" );
stage.SetOwn( "align", "" );
stage.SetOwn( "showMenu", true );
bool fullscreen = false;
stage.AddVirtual( "displayState",
vm.Native( "displayState", ( vm, t, a ) => fullscreen ? "fullScreen" : "normal" ),
vm.Native( "displayState", ( vm, t, a ) => { fullscreen = Arg( a, 0 ).ToStr( vm ) == "fullScreen"; vm.Player.Host?.SetFullScreen( fullscreen ); return Value.Undefined; } ) );
}
// ========================================================================= SharedObject
static void InstallSharedObject( Avm1 vm )
{
var p = vm.NewObject();
vm.SharedObjectProto = p;
var ctor = Ctor( vm, "SharedObject", p, ( vm, t, a ) => Value.Undefined );
var cache = new Dictionary<string, AsObject>();
vm.Method( ctor, "getLocal", ( vm, t, a ) =>
{
var name = Arg( a, 0 ).ToStr( vm );
if ( cache.TryGetValue( name, out var so ) ) return so;
so = new AsObject( p ) { Native = name };
AsObject data = null;
var saved = vm.Player.Host?.LoadShared( name );
if ( !string.IsNullOrEmpty( saved ) )
{
try { data = FromJson( vm, Json.Parse( saved ) ).AsObj; } catch ( Exception ) { data = null; }
}
so.SetOwn( "data", Value.From( data ?? vm.NewObject() ) );
cache[name] = so;
return so;
} );
vm.Method( p, "flush", ( vm, t, a ) =>
{
if ( t.AsObj?.Native is not string name ) return false;
var data = t.Obj.Get( "data", vm );
var sb = new StringBuilder();
ToJson( vm, data, sb, new HashSet<AsObject>() );
vm.Player.Host?.SaveShared( name, sb.ToString() );
return true;
} );
vm.Method( p, "clear", ( vm, t, a ) =>
{
if ( t.AsObj?.Native is not string name ) return Value.Undefined;
t.Obj.SetOwn( "data", Value.From( vm.NewObject() ) );
vm.Player.Host?.SaveShared( name, "" );
return Value.Undefined;
} );
vm.Method( p, "getSize", ( vm, t, a ) => 1024 );
}
static void ToJson( Avm1 vm, Value v, StringBuilder sb, HashSet<AsObject> seen )
{
switch ( v.Kind )
{
case ValueKind.Undefined: sb.Append( "{\"$u\":1}" ); return;
case ValueKind.Null: sb.Append( "null" ); return;
case ValueKind.Bool: sb.Append( v.BoolValue ? "true" : "false" ); return;
case ValueKind.Number:
if ( double.IsNaN( v.Num ) || double.IsInfinity( v.Num ) ) { sb.Append( "{\"$n\":\"" ).Append( Value.NumberToString( v.Num ) ).Append( "\"}" ); return; }
sb.Append( v.Num.ToString( "R", CultureInfo.InvariantCulture ) ); return;
case ValueKind.String: Str( v.Str, sb ); return;
}
var o = v.Obj;
if ( o is AsFunction || o is DisplayObject || !seen.Add( o ) ) { sb.Append( "{\"$u\":1}" ); return; }
if ( o is AsArray arr )
{
sb.Append( '[' );
for ( int i = 0; i < arr.Items.Count; i++ ) { if ( i > 0 ) sb.Append( ',' ); ToJson( vm, arr.Items[i], sb, seen ); }
sb.Append( ']' );
}
else
{
sb.Append( '{' );
bool first = true;
foreach ( var k in o.OwnKeys )
{
if ( !first ) sb.Append( ',' );
first = false;
Str( k, sb ); sb.Append( ':' );
ToJson( vm, o.Get( k, vm ), sb, seen );
}
sb.Append( '}' );
}
seen.Remove( o );
}
static void Str( string s, StringBuilder sb )
{
sb.Append( '"' );
foreach ( var c in s )
{
switch ( c )
{
case '"': sb.Append( "\\\"" ); break;
case '\\': sb.Append( "\\\\" ); break;
case '\n': sb.Append( "\\n" ); break;
case '\r': sb.Append( "\\r" ); break;
case '\t': sb.Append( "\\t" ); break;
default:
if ( c < 32 ) sb.Append( "\\u" ).Append( ((int)c).ToString( "x4" ) );
else sb.Append( c );
break;
}
}
sb.Append( '"' );
}
static Value FromJson( Avm1 vm, object j )
{
switch ( j )
{
case null: return Value.Null;
case bool b: return b;
case double d: return d;
case string s: return s;
case List<object> l:
{
var arr = vm.NewArray();
foreach ( var x in l ) arr.Items.Add( FromJson( vm, x ) );
return arr;
}
case Dictionary<string, object> dict:
{
if ( dict.Count == 1 && dict.ContainsKey( "$u" ) ) return Value.Undefined;
if ( dict.Count == 1 && dict.TryGetValue( "$n", out var n ) ) return Value.StringToNumber( (string)n, 7 );
var o = vm.NewObject();
foreach ( var kv in dict ) o.Set( kv.Key, FromJson( vm, kv.Value ), vm );
return o;
}
}
return Value.Undefined;
}
// ========================================================================= AsBroadcaster
static void InstallBroadcaster( Avm1 vm )
{
var b = vm.NewObject();
vm.Global.DefineHidden( "AsBroadcaster", Value.From( b ) );
vm.Method( b, "initialize", ( vm, t, a ) => { if ( Arg( a, 0 ).AsObj is AsObject o ) MakeBroadcaster( vm, o ); return Value.Undefined; } );
}
static void MakeBroadcaster( Avm1 vm, AsObject o )
{
o.DefineHidden( "_listeners", Value.From( vm.NewArray() ) );
vm.Method( o, "addListener", ( vm, t, a ) =>
{
var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
if ( l == null ) return false;
var x = Arg( a, 0 );
l.Items.RemoveAll( v => vm.StrictEquals( v, x ) );
l.Items.Add( x );
return true;
} );
vm.Method( o, "removeListener", ( vm, t, a ) =>
{
var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
if ( l == null ) return false;
var x = Arg( a, 0 );
return l.Items.RemoveAll( v => vm.StrictEquals( v, x ) ) > 0;
} );
vm.Method( o, "broadcastMessage", ( vm, t, a ) =>
{
var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
if ( l == null || a.Length == 0 ) return Value.Undefined;
var name = Arg( a, 0 ).ToStr( vm );
var rest = a[1..];
foreach ( var x in l.Items.ToArray() )
{
var f = vm.GetMember( x, name ).AsObj as AsFunction;
if ( f != null ) vm.CallFunction( f, x, rest );
}
return l.Items.Count > 0;
} );
}
}