Flash/Core/Library.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Flash;
/// <summary>2D affine matrix in Flash order: x' = A*x + C*y + Tx, y' = B*x + D*y + Ty.</summary>
public struct Matrix
{
public double A, B, C, D, Tx, Ty;
public static readonly Matrix Identity = new() { A = 1, D = 1 };
public Matrix( double a, double b, double c, double d, double tx, double ty ) { A = a; B = b; C = c; D = d; Tx = tx; Ty = ty; }
/// <summary>this * other : apply other first, then this.</summary>
public Matrix Mul( Matrix o ) => new(
A * o.A + C * o.B, B * o.A + D * o.B,
A * o.C + C * o.D, B * o.C + D * o.D,
A * o.Tx + C * o.Ty + Tx, B * o.Tx + D * o.Ty + Ty );
public (double x, double y) Apply( double x, double y ) => (A * x + C * y + Tx, B * x + D * y + Ty);
public Matrix Inverse()
{
double det = A * D - B * C;
if ( Math.Abs( det ) < 1e-12 ) return Identity;
double ia = D / det, ib = -B / det, ic = -C / det, id = A / det;
return new Matrix( ia, ib, ic, id, -(ia * Tx + ic * Ty), -(ib * Tx + id * Ty) );
}
public static Matrix FromArray( List<object> a )
{
if ( a is null ) return Identity;
return new Matrix( Json.D( a[0] ), Json.D( a[1] ), Json.D( a[2] ), Json.D( a[3] ), Json.D( a[4] ), Json.D( a[5] ) );
}
public bool IsIdentity => A == 1 && B == 0 && C == 0 && D == 1 && Tx == 0 && Ty == 0;
}
/// <summary>Color transform: out = in * Mult + Add (add terms in 0..255).</summary>
public struct ColorTransform
{
public double Rm, Gm, Bm, Am, Ra, Ga, Ba, Aa;
public static readonly ColorTransform Identity = new() { Rm = 1, Gm = 1, Bm = 1, Am = 1 };
public ColorTransform Mul( ColorTransform inner ) => new()
{
// apply inner first, then this
Rm = Rm * inner.Rm, Gm = Gm * inner.Gm, Bm = Bm * inner.Bm, Am = Am * inner.Am,
Ra = Rm * inner.Ra + Ra, Ga = Gm * inner.Ga + Ga, Ba = Bm * inner.Ba + Ba, Aa = Am * inner.Aa + Aa,
};
public static ColorTransform FromArray( List<object> a )
{
if ( a is null ) return Identity;
return new ColorTransform
{
Rm = Json.D( a[0] ), Gm = Json.D( a[1] ), Bm = Json.D( a[2] ), Am = Json.D( a[3] ),
Ra = Json.D( a[4] ), Ga = Json.D( a[5] ), Ba = Json.D( a[6] ), Aa = Json.D( a[7] ),
};
}
public bool IsIdentity => Rm == 1 && Gm == 1 && Bm == 1 && Am == 1 && Ra == 0 && Ga == 0 && Ba == 0 && Aa == 0;
}
public sealed class Filter
{
public string Kind; // glow, shadow, blur, cmatrix, bevel
public double[] Color; // rgba 0..255
public double BlurX, BlurY, Strength, Angle, Distance;
public bool Inner, Knockout;
public double[] ColorMatrix;
public static List<Filter> FromJson( List<object> list )
{
if ( list is null ) return null;
var r = new List<Filter>();
foreach ( List<object> f in list )
{
var k = (string)f[0];
var x = new Filter { Kind = k };
switch ( k )
{
case "glow":
x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[2] ); x.BlurY = Json.D( f[3] ); x.Strength = Json.D( f[4] );
x.Inner = Json.B( f[5] ); x.Knockout = Json.B( f[6] );
break;
case "shadow":
x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[2] ); x.BlurY = Json.D( f[3] ); x.Angle = Json.D( f[4] );
x.Distance = Json.D( f[5] ); x.Strength = Json.D( f[6] ); x.Inner = Json.B( f[7] ); x.Knockout = Json.B( f[8] );
break;
case "blur": x.BlurX = Json.D( f[1] ); x.BlurY = Json.D( f[2] ); break;
case "cmatrix": x.ColorMatrix = Json.Doubles( f[1] ); break;
case "bevel":
x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[3] ); x.BlurY = Json.D( f[4] ); x.Angle = Json.D( f[5] );
x.Distance = Json.D( f[6] ); x.Strength = Json.D( f[7] ); x.Inner = Json.B( f[8] );
break;
}
r.Add( x );
}
return r;
}
}
// ============================================================================ character definitions
public abstract class CharDef
{
public int Id;
public string ExportName;
}
public sealed class ShapeDef : CharDef
{
public double X0, Y0, X1, Y1; // bounds in px
public string Image; // path relative to the art root
public int Zoom; // image px per Flash px
public HitMask Mask;
}
public sealed class MorphDef : CharDef
{
public double[] B0, B1;
public string ImageStart, ImageEnd;
public int Zoom;
}
public sealed class StaticTextDef : CharDef
{
public double X0, Y0, X1, Y1;
public string Image;
public int Zoom;
}
public sealed class EditTextDef : CharDef
{
public double X0, Y0, X1, Y1;
public int FontId;
public double Size;
public double[] Color = { 0, 0, 0, 255 };
public int Align; // 0 left 1 right 2 center 3 justify
public double LeftMargin, RightMargin, Indent, Leading;
public int MaxLength;
public string InitialText, Variable;
public bool Html, Multiline, WordWrap, Password, Border, NoSelect, AutoSize, Input, Outlines;
}
public sealed class FrameOp
{
public char Kind; // P place, M modify, R remove, A action, S sound
public int Depth, CharId;
public bool Move;
public Matrix? M;
public ColorTransform? Cx;
public string Name;
public int? Ratio;
public int ClipDepth;
public List<Filter> Filters;
public bool HasFilters;
public int BlendMode;
public List<ClipActionDef> ClipActions;
public byte[] Code;
public int SoundId;
public bool SoundStop;
public bool SoundNoMultiple;
public int SoundLoops;
}
public sealed class ClipActionDef
{
public List<string> Events;
public int Key;
public byte[] Code;
}
public sealed class SpriteDef : CharDef
{
public int FrameCount;
public List<FrameOp>[] Frames;
public Dictionary<string, int> Labels = new();
}
public sealed class ButtonRecordDef
{
public string States; // u o d h
public int CharId, Depth;
public Matrix M;
public ColorTransform? Cx;
public List<Filter> Filters;
}
public sealed class ButtonActionDef
{
public List<string> On;
public int Key;
public byte[] Code;
}
public sealed class ButtonDef : CharDef
{
public List<ButtonRecordDef> Records = new();
public List<ButtonActionDef> Actions = new();
public bool TrackAsMenu;
}
public sealed class SoundDef : CharDef
{
public string File;
public int Samples, Rate;
public bool Stereo;
public double Seconds => Rate switch { 0 => Samples / 5512.5, 1 => Samples / 11025.0, 2 => Samples / 22050.0, _ => Samples / 44100.0 };
}
public sealed class FontDef
{
public int Id;
public string Name, Family;
public bool Bold, Italic;
public double Ascent, Descent, Leading;
}
public sealed class InitAction
{
public int SpriteId;
public byte[] Code;
}
/// <summary>1 bit per Flash pixel alpha mask for hit testing a shape.</summary>
public sealed class HitMask
{
public int X, Y, W, H; // in Flash px relative to the shape origin
public byte[] Bits;
public bool Test( double x, double y )
{
int ix = (int)Math.Floor( x ) - X, iy = (int)Math.Floor( y ) - Y;
if ( ix < 0 || iy < 0 || ix >= W || iy >= H ) return false;
int bit = iy * W + ix;
return (Bits[bit >> 3] & (1 << (bit & 7))) != 0;
}
}
/// <summary>Everything decoded from the SWF (by tools/extract.py).</summary>
public sealed class Library
{
public double StageWidth, StageHeight, FrameRate;
public int Version;
public double[] Background;
public readonly Dictionary<int, CharDef> Chars = new();
public readonly Dictionary<string, int> Exports = new();
public readonly Dictionary<int, FontDef> Fonts = new();
public readonly List<InitAction> Init = new();
public SpriteDef Main;
public Dictionary<string, int> ShapeZoom = new();
public CharDef Get( int id ) => Chars.TryGetValue( id, out var c ) ? c : null;
public CharDef GetExport( string name )
{
if ( name != null && Exports.TryGetValue( name, out var id ) ) return Get( id );
return null;
}
/// <param name="json">ss2.json</param>
/// <param name="zoomsJson">zooms.json (shape/text id → image zoom)</param>
/// <param name="hitmasks">hitmasks.bin, or null</param>
public static Library Load( string json, string zoomsJson, byte[] hitmasks )
{
var lib = new Library();
var root = (Dictionary<string, object>)Json.Parse( json );
var st = (Dictionary<string, object>)root["stage"];
lib.StageWidth = Json.D( st["w"] ); lib.StageHeight = Json.D( st["h"] ); lib.FrameRate = Json.D( st["fps"] );
lib.Version = (int)Json.D( st["version"] );
lib.Background = Json.Doubles( st["bg"] );
var zooms = zoomsJson != null ? (Dictionary<string, object>)Json.Parse( zoomsJson ) : new Dictionary<string, object>();
foreach ( var kv in (Dictionary<string, object>)root["exports"] ) lib.Exports[kv.Key] = (int)Json.D( kv.Value );
foreach ( var kv in (Dictionary<string, object>)root["fonts"] )
{
var f = (Dictionary<string, object>)kv.Value;
var fd = new FontDef
{
Id = int.Parse( kv.Key, CultureInfo.InvariantCulture ), Name = (string)f["name"], Bold = Json.B( f["bold"] ), Italic = Json.B( f["italic"] ),
Ascent = Json.D( f["ascent"] ), Descent = Json.D( f["descent"] ), Leading = Json.D( f["leading"] ),
};
fd.Family = "SS2 F" + fd.Id;
lib.Fonts[fd.Id] = fd;
}
foreach ( Dictionary<string, object> ia in (List<object>)root["init"] )
lib.Init.Add( new InitAction { SpriteId = (int)Json.D( ia["sprite"] ), Code = Base64.Decode( (string)ia["b"] ) } );
foreach ( var kv in (Dictionary<string, object>)root["chars"] )
{
int id = int.Parse( kv.Key, CultureInfo.InvariantCulture );
var c = (Dictionary<string, object>)kv.Value;
var t = (string)c["t"];
int zoom = zooms.TryGetValue( kv.Key, out var zv ) ? (int)Json.D( zv ) : 2;
CharDef def = null;
switch ( t )
{
case "s":
{
var b = Json.Doubles( c["b"] );
def = new ShapeDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3], Image = $"s/{id}.png", Zoom = zoom };
break;
}
case "x":
{
var b = Json.Doubles( c["b"] );
def = new StaticTextDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3], Image = $"x/{id}.png", Zoom = zoom };
break;
}
case "m":
def = new MorphDef { B0 = Json.Doubles( c["b0"] ), B1 = Json.Doubles( c["b1"] ), ImageStart = $"m/{id}.start.png", ImageEnd = $"m/{id}.end.png", Zoom = 4 };
break;
case "e":
{
var b = Json.Doubles( c["b"] );
var e = new EditTextDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3] };
if ( c.TryGetValue( "font", out var fo ) ) e.FontId = (int)Json.D( fo );
if ( c.TryGetValue( "size", out var so ) ) e.Size = Json.D( so );
if ( c.TryGetValue( "color", out var co ) ) e.Color = Json.Doubles( co );
if ( c.TryGetValue( "align", out var al ) ) e.Align = (int)Json.D( al );
if ( c.TryGetValue( "lm", out var lm ) ) e.LeftMargin = Json.D( lm );
if ( c.TryGetValue( "rm", out var rm ) ) e.RightMargin = Json.D( rm );
if ( c.TryGetValue( "indent", out var ind ) ) e.Indent = Json.D( ind );
if ( c.TryGetValue( "leading", out var ld ) ) e.Leading = Json.D( ld );
if ( c.TryGetValue( "max", out var mx ) ) e.MaxLength = (int)Json.D( mx );
if ( c.TryGetValue( "text", out var tx ) ) e.InitialText = (string)tx;
if ( c.TryGetValue( "var", out var va ) ) e.Variable = (string)va;
e.Html = c.ContainsKey( "html" ); e.Multiline = c.ContainsKey( "multi" ); e.WordWrap = c.ContainsKey( "wrap" );
e.Password = c.ContainsKey( "pass" ); e.Border = c.ContainsKey( "border" ); e.NoSelect = c.ContainsKey( "nosel" );
e.AutoSize = c.ContainsKey( "auto" ); e.Input = c.ContainsKey( "input" ); e.Outlines = c.ContainsKey( "outl" );
def = e;
break;
}
case "p":
def = ParseSprite( c );
break;
case "b":
{
var bd = new ButtonDef { TrackAsMenu = Json.D( c["menu"] ) != 0 };
foreach ( Dictionary<string, object> r in (List<object>)c["recs"] )
{
bd.Records.Add( new ButtonRecordDef
{
States = (string)r["st"], CharId = (int)Json.D( r["c"] ), Depth = (int)Json.D( r["d"] ),
M = Matrix.FromArray( r["m"] as List<object> ),
Cx = r.TryGetValue( "cx", out var cx ) ? ColorTransform.FromArray( (List<object>)cx ) : null,
Filters = r.TryGetValue( "f", out var ff ) ? Filter.FromJson( ff as List<object> ) : null,
} );
}
foreach ( Dictionary<string, object> a in (List<object>)c["acts"] )
{
var on = new List<string>();
foreach ( var o in (List<object>)a["on"] ) on.Add( (string)o );
bd.Actions.Add( new ButtonActionDef { On = on, Key = (int)Json.D( a["key"] ), Code = Base64.Decode( (string)a["b"] ) } );
}
bd.Records.Sort( ( x, y ) => x.Depth.CompareTo( y.Depth ) );
def = bd;
break;
}
case "a":
def = new SoundDef { File = $"a/{id}", Samples = (int)Json.D( c["samples"] ), Rate = (int)Json.D( c["rate"] ), Stereo = Json.B( c["stereo"] ) };
break;
default:
continue;
}
def.Id = id;
lib.Chars[id] = def;
}
foreach ( var kv in lib.Exports )
if ( lib.Chars.TryGetValue( kv.Value, out var d ) && d.ExportName == null ) d.ExportName = kv.Key;
lib.Main = lib.Chars[0] as SpriteDef;
if ( hitmasks != null ) LoadHitMasks( lib, hitmasks );
return lib;
}
static SpriteDef ParseSprite( Dictionary<string, object> c )
{
var sd = new SpriteDef { FrameCount = Math.Max( 1, (int)Json.D( c["fc"] ) ) };
foreach ( var kv in (Dictionary<string, object>)c["labels"] ) sd.Labels[kv.Key] = (int)Json.D( kv.Value );
var frames = (List<object>)c["frames"];
sd.Frames = new List<FrameOp>[Math.Max( sd.FrameCount, frames.Count )];
for ( int f = 0; f < sd.Frames.Length; f++ )
{
var list = new List<FrameOp>();
sd.Frames[f] = list;
if ( f >= frames.Count ) continue;
foreach ( List<object> op in (List<object>)frames[f] )
{
var k = ((string)op[0])[0];
var fo = new FrameOp { Kind = k };
switch ( k )
{
case 'P':
fo.Depth = (int)Json.D( op[1] ); fo.CharId = (int)Json.D( op[2] );
ReadPlace( fo, (Dictionary<string, object>)op[3] );
fo.Move = Json.D( op[4] ) != 0;
break;
case 'M':
fo.Depth = (int)Json.D( op[1] );
ReadPlace( fo, (Dictionary<string, object>)op[2] );
break;
case 'R': fo.Depth = (int)Json.D( op[1] ); break;
case 'A': fo.Code = Base64.Decode( (string)op[1] ); break;
case 'S':
fo.SoundId = (int)Json.D( op[1] );
var so = (Dictionary<string, object>)op[2];
fo.SoundStop = so.ContainsKey( "stop" );
fo.SoundNoMultiple = so.ContainsKey( "nomulti" );
fo.SoundLoops = so.TryGetValue( "loops", out var lo ) ? (int)Json.D( lo ) : 1;
break;
}
list.Add( fo );
}
}
if ( sd.Frames.Length > sd.FrameCount ) sd.FrameCount = sd.Frames.Length;
return sd;
}
static void ReadPlace( FrameOp fo, Dictionary<string, object> o )
{
if ( o.TryGetValue( "m", out var m ) ) fo.M = Matrix.FromArray( (List<object>)m );
if ( o.TryGetValue( "cx", out var cx ) ) fo.Cx = ColorTransform.FromArray( (List<object>)cx );
if ( o.TryGetValue( "n", out var n ) ) fo.Name = (string)n;
if ( o.TryGetValue( "r", out var r ) ) fo.Ratio = (int)Json.D( r );
if ( o.TryGetValue( "cd", out var cd ) ) fo.ClipDepth = (int)Json.D( cd );
if ( o.TryGetValue( "f", out var f ) ) { fo.Filters = Filter.FromJson( (List<object>)f ); fo.HasFilters = true; }
if ( o.TryGetValue( "bm", out var bm ) ) fo.BlendMode = (int)Json.D( bm );
if ( o.TryGetValue( "ca", out var ca ) )
{
fo.ClipActions = new List<ClipActionDef>();
foreach ( Dictionary<string, object> a in (List<object>)ca )
{
var evs = new List<string>();
foreach ( var e in (List<object>)a["ev"] ) evs.Add( (string)e );
fo.ClipActions.Add( new ClipActionDef { Events = evs, Key = (int)Json.D( a["key"] ), Code = Base64.Decode( (string)a["b"] ) } );
}
}
}
/// <summary>hitmasks.bin: repeated [int id, int x, int y, int w, int h, bytes...]</summary>
static void LoadHitMasks( Library lib, byte[] data )
{
int i = 0;
int Rd() { int v = data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24); i += 4; return v; }
while ( i + 20 <= data.Length )
{
int id = Rd(), x = Rd(), y = Rd(), w = Rd(), h = Rd();
int n = (w * h + 7) / 8;
var bits = new byte[n];
Array.Copy( data, i, bits, 0, n );
i += n;
if ( lib.Chars.TryGetValue( id, out var d ) && d is ShapeDef s ) s.Mask = new HitMask { X = x, Y = y, W = w, H = h, Bits = bits };
}
}
}
// ============================================================================ tiny JSON + base64
public static class Json
{
public static double D( object o ) => o switch { double d => d, long l => l, int i => i, bool b => b ? 1 : 0, _ => 0 };
public static bool B( object o ) => o switch { bool b => b, double d => d != 0, _ => false };
public static double[] Doubles( object o )
{
if ( o is not List<object> l ) return null;
var r = new double[l.Count];
for ( int i = 0; i < l.Count; i++ ) r[i] = D( l[i] );
return r;
}
public static object Parse( string s )
{
int i = 0;
var v = Read( s, ref i );
return v;
}
static void Ws( string s, ref int i ) { while ( i < s.Length && (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t') ) i++; }
static object Read( string s, ref int i )
{
Ws( s, ref i );
char c = s[i];
if ( c == '{' )
{
var d = new Dictionary<string, object>();
i++; Ws( s, ref i );
if ( s[i] == '}' ) { i++; return d; }
while ( true )
{
Ws( s, ref i );
var k = ReadString( s, ref i );
Ws( s, ref i ); i++; // :
d[k] = Read( s, ref i );
Ws( s, ref i );
if ( s[i] == ',' ) { i++; continue; }
i++; return d;
}
}
if ( c == '[' )
{
var l = new List<object>();
i++; Ws( s, ref i );
if ( s[i] == ']' ) { i++; return l; }
while ( true )
{
l.Add( Read( s, ref i ) );
Ws( s, ref i );
if ( s[i] == ',' ) { i++; continue; }
i++; return l;
}
}
if ( c == '"' ) return ReadString( s, ref i );
if ( c == 't' ) { i += 4; return true; }
if ( c == 'f' ) { i += 5; return false; }
if ( c == 'n' ) { i += 4; return null; }
int st = i;
while ( i < s.Length && "+-0123456789.eE".IndexOf( s[i] ) >= 0 ) i++;
return double.Parse( s.Substring( st, i - st ), NumberStyles.Float, CultureInfo.InvariantCulture );
}
static string ReadString( string s, ref int i )
{
i++; // "
var sb = new StringBuilder();
while ( s[i] != '"' )
{
char c = s[i++];
if ( c != '\\' ) { sb.Append( c ); continue; }
char e = s[i++];
switch ( e )
{
case 'n': sb.Append( '\n' ); break;
case 'r': sb.Append( '\r' ); break;
case 't': sb.Append( '\t' ); break;
case 'b': sb.Append( '\b' ); break;
case 'f': sb.Append( '\f' ); break;
case 'u': sb.Append( (char)int.Parse( s.Substring( i, 4 ), NumberStyles.HexNumber, CultureInfo.InvariantCulture ) ); i += 4; break;
default: sb.Append( e ); break;
}
}
i++;
return sb.ToString();
}
}
public static class Base64
{
static readonly int[] map = Build();
static int[] Build()
{
var m = new int[128];
for ( int i = 0; i < 128; i++ ) m[i] = -1;
const string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for ( int i = 0; i < a.Length; i++ ) m[a[i]] = i;
return m;
}
public static byte[] Decode( string s )
{
var outp = new List<byte>( s.Length * 3 / 4 );
int buf = 0, bits = 0;
foreach ( var ch in s )
{
if ( ch >= 128 ) continue;
int v = map[ch];
if ( v < 0 ) continue;
buf = (buf << 6) | v; bits += 6;
if ( bits >= 8 ) { bits -= 8; outp.Add( (byte)((buf >> bits) & 0xFF) ); }
}
return outp.ToArray();
}
}