diff --git a/lib/display/DisplayObjectContainer.ts b/lib/display/DisplayObjectContainer.ts index c8504390..b7f6719f 100644 --- a/lib/display/DisplayObjectContainer.ts +++ b/lib/display/DisplayObjectContainer.ts @@ -511,7 +511,11 @@ export class DisplayObjectContainer extends InteractiveObject { try { ( this._adaptee).removeChild(child.adaptee); } catch (e) { - throw ( this.sec).createError('ArgumentError', Errors.NotAChildError); + // The adaptee scene graph can diverge from the AS3 display list (orphan/unload + // management), so a child that is still in the AS3 list may already be detached + // here. Throwing Flash's #2025 in that case erupts through whatever frame + // callback is running (e.g. a tween onComplete) and can kill the shared ticker. + console.warn('[DisplayObjectContainer] removeChild: not a child (already removed?) — ignoring'); } //OrphanManager.addOrphan(child.adaptee); return child; diff --git a/lib/events/EventDispatcherBase.ts b/lib/events/EventDispatcherBase.ts index b1a3b4f0..70520f54 100644 --- a/lib/events/EventDispatcherBase.ts +++ b/lib/events/EventDispatcherBase.ts @@ -76,6 +76,11 @@ export class EventDispatcherBase extends ASObject { const l: ListenerObject = this._listenerObjects[event.type]; if (l) { + // Flash reports the aggregation target (EventDispatcher(target) constructor arg) + // as currentTarget during dispatch — e.g. GreenSock dispatches tween events via an + // internal aggregated dispatcher and handlers cast event.currentTarget to the tween. + if (this._t) + ( event).currentTarget = this._t; if (!event.target) event.target = this._t; //console.log("dispatchEvent", event.type, (this).adaptee?.id); diff --git a/lib/link.ts b/lib/link.ts index d4ffa7fc..67d03885 100644 --- a/lib/link.ts +++ b/lib/link.ts @@ -189,6 +189,7 @@ import { GameInputDevice } from './ui/GameInputDevice'; import { GameInputFinger } from './ui/GameInputFinger'; import { GameInputHand } from './ui/GameInputHand'; import { Mouse } from './ui/Mouse'; +import { MouseCursorData } from './ui/MouseCursorData'; import { MultitouchInputMode } from './ui/MultitouchInputMode'; import { Multitouch } from './ui/Multitouch'; import { Timer } from './utils/Timer'; @@ -640,7 +641,7 @@ export function initLink() { //M('flash.ui.KeyLocation', KeyLocation); M('flash.ui.Mouse', Mouse); //M('flash.ui.MouseCursor', MouseCursor); - //M('flash.ui.MouseCursorData', MouseCursorData); + M('flash.ui.MouseCursorData', MouseCursorData); M('flash.ui.Multitouch', Multitouch); M('flash.ui.MultitouchInputMode', MultitouchInputMode); diff --git a/lib/net/FileReference.ts b/lib/net/FileReference.ts index 1860e73b..5189b321 100644 --- a/lib/net/FileReference.ts +++ b/lib/net/FileReference.ts @@ -212,12 +212,12 @@ export class FileReference extends EventDispatcher { const mime = knownTypes[ext].mime; types[0] = { description: knownTypes[ext].description, - accept: { [mime] : ext }, + accept: { [mime] : ext as `.${string}` }, }; } else { types[0] = { description: '', - accept: { 'application/unknown' : ext }, + accept: { 'application/unknown' : ext as `.${string}` }, }; } diff --git a/lib/net/NetConnection.ts b/lib/net/NetConnection.ts index e720e188..f9cd506b 100644 --- a/lib/net/NetConnection.ts +++ b/lib/net/NetConnection.ts @@ -1,5 +1,6 @@ import { EventDispatcher } from '../events/EventDispatcher'; -import { axCoerceString, ASArray, ASObject } from '@awayfl/avm2'; +import { axCoerceString, ASArray, ASObject, Multiname } from '@awayfl/avm2'; +import { NetStatusEvent } from '../events/NetStatusEvent'; import { somewhatImplemented, release, notImplemented } from '@awayfl/swf-loader'; import { Responder } from './Responder'; @@ -46,8 +47,234 @@ export class NetConnection extends EventDispatcher { } call(command: string, responder: Responder /* more args can be provided */): void { - arguments[0] = axCoerceString(command); - this._invoke(2, arguments); + command = axCoerceString(command); + // Native methods receive AS3 arguments flat (no ...rest collection) — everything + // after `responder` is a call argument to be serialised. + const callArgs = Array.prototype.slice.call(arguments, 2); + this._httpCall(command, responder, callArgs); + } + + /* ------------------------------------------------------------------------------------ + * Flash Remoting (AMF over HTTP POST) — the transport used by Zend_Amf / AMFPHP / etc. + * Serialisation is delegated to the AVM2 ByteArray AMF codec so arguments and results + * stay proper AVM2 objects; only the remoting packet envelope is hand-written here. + * ---------------------------------------------------------------------------------- */ + + private static _netStatusEventClass: any = null; + private _httpCallId: number = 0; + private _pendingResponders: Record; + + private _debugAMF(...args: any[]): void { + if (typeof self !== 'undefined' && ( self).__AWAYFL_AMF_DEBUG) + console.log.apply(console, ['[AMF]', ...args]); + } + + private _makeStatusInfo(code: string, level: string, description?: string): ASObject { + const info: any = { code: code, level: level }; + if (description !== undefined) + info.description = description; + return this.sec.createObjectFromJS(info); + } + + private _dispatchStatus(code: string, level: string, description?: string): void { + this._debugAMF('netStatus ->', code, description || ''); + try { + // Events handed to compiled AS3 must be real AVM2 instances (axConstruct on the + // AXClass); a raw `new NetStatusEvent(...)` lacks runtime traits and property + // access from compiled code fails on it. + if (!NetConnection._netStatusEventClass) { + NetConnection._netStatusEventClass = this.sec.application.getClass( + Multiname.FromFQNString('flash.events.NetStatusEvent', 0 /* NamespaceType.Public */)); + } + const event = NetConnection._netStatusEventClass.axConstruct( + [NetStatusEvent.NET_STATUS, false, false, this._makeStatusInfo(code, level, description)]); + event.currentTarget = this; + event.target = this; + this.dispatchEvent(event); + } catch (e) { + console.warn('[NetConnection] netStatus dispatch failed:', e); + } + } + + private static _byteArrayClass: any = null; + + private _newAMFByteArray(): any { + // ByteArray's buffer machinery lives on DataBuffer.prototype and is merged onto the + // VM-linked prototype via instanceNatives — a raw `new ByteArray()` lacks it, so the + // instance must be constructed through the AXClass. + if (!NetConnection._byteArrayClass) { + NetConnection._byteArrayClass = this.sec.application.getClass( + Multiname.FromFQNString('flash.utils.ByteArray', 0 /* NamespaceType.Public */)); + } + const ba: any = NetConnection._byteArrayClass.axConstruct([]); + // the AMF codec resolves classes/objects through the security domain + ba.sec = this.sec; + return ba; + } + + private _httpCall(command: string, responder: Responder, callArgs: any[]): void { + const id = String(++this._httpCallId); + this._debugAMF('call ->', command, 'id', id); + try { + if (!this._pendingResponders) + this._pendingResponders = {}; + if (responder) + this._pendingResponders[id] = responder; + + const ba = this._newAMFByteArray(); + ba._objectEncoding = 3; // AMF3 + ba.writeByte(0x11); // AMF0 avmplus marker: body is AMF3 + ba.writeObject(this.sec.createArray(callArgs)); + const body = new Uint8Array(ba._buffer.slice(0, ba._length)); + const packet = NetConnection._buildRemotingPacket(command, id, body); + + if (!this._uri) { + this._httpFail(id, 'NetConnection is not connected (uri is null)'); + return; + } + fetch(this._uri, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-amf' }, + body: packet + }).then((r) => { + if (!r.ok) + throw new Error('HTTP ' + r.status); + const ct = (r.headers.get('content-type') || '').toLowerCase(); + // A misconfigured gateway can answer 200 with an HTML error page; surface it + // as a fault instead of feeding non-AMF bytes to the parser. + if (ct.indexOf('application/x-amf') === -1) { + return r.text().then((t) => { + throw new Error('Non-AMF response (content-type: ' + (ct || 'none') + '): ' + + t.replace(/\s+/g, ' ').slice(0, 300)); + }); + } + return r.arrayBuffer(); + }).then((buf) => { + this._handleRemotingResponse( buf); + }).catch((err) => { + this._httpFail(id, String((err && err.message) || err)); + }); + } catch (e) { + this._debugAMF('call FAILED synchronously:', e); + this._httpFail(id, String((e && ( e).message) || e)); + } + } + + private _handleRemotingResponse(buf: ArrayBuffer): void { + let messages: { target: string; body: ArrayBuffer }[]; + try { + messages = NetConnection._parseRemotingPacket(buf); + } catch (e) { + console.warn('[NetConnection] AMF response parse failed:', e); + return; + } + for (let i = 0; i < messages.length; i++) { + const msg = messages[i]; + let value: any; + try { + const ba = this._newAMFByteArray(); + ba.setArrayBuffer(msg.body); + ba._position = 0; + ba._objectEncoding = 0; // AMF0 reader; handles the 0x11 -> AMF3 marker + value = ba.readObject(); + } catch (e) { + console.warn('[NetConnection] AMF body decode failed for', msg.target, e); + continue; + } + const match = /^\/([^\/]*)\/(onResult|onStatus)$/.exec(msg.target); + if (!match) { + console.warn('[NetConnection] unrecognised AMF response target:', msg.target); + continue; + } + const id = match[1], kind = match[2]; + const responder = this._pendingResponders && this._pendingResponders[id]; + if (this._pendingResponders) + delete this._pendingResponders[id]; + this._debugAMF('response', kind, 'id', id, '->', responder ? 'responder' : '(no responder)'); + if (responder) { + try { + kind === 'onResult' ? responder.invokeResult(value) : responder.invokeStatus(value); + } catch (e) { + console.warn('[NetConnection] responder callback threw:', e); + } + } else if (kind === 'onStatus') { + this._dispatchStatus('NetConnection.Call.Failed', 'error'); + } + } + } + + private _httpFail(id: string, description: string): void { + this._debugAMF('call failed id', id, ':', description); + const responder = this._pendingResponders && this._pendingResponders[id]; + if (this._pendingResponders) + delete this._pendingResponders[id]; + if (responder) { + try { + responder.invokeStatus(this._makeStatusInfo('NetConnection.Call.Failed', 'error', description)); + return; + } catch (e) { + console.warn('[NetConnection] responder status callback threw:', e); + } + } + this._dispatchStatus('NetConnection.Call.Failed', 'error', description); + } + + /* AMF remoting packet: u16 version | u16 headerCount | u16 messageCount, then per + * message: utf targetURI | utf responseURI | u32 bodyLength | AMF body. */ + private static _buildRemotingPacket(target: string, responseId: string, body: Uint8Array): Uint8Array { + const enc = new TextEncoder(); + const head: number[] = []; + const u16 = (v: number) => head.push((v >>> 8) & 0xff, v & 0xff); + const u32 = (v: number) => head.push((v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff); + const utf = (str: string) => { + const b = enc.encode(str); + u16(b.length); + for (let i = 0; i < b.length; i++) head.push(b[i]); + }; + u16(3); // version 3 (AMF3-capable) + u16(0); // headers + u16(1); // messages + utf(target); + utf('/' + responseId); + u32(body.length); + const out = new Uint8Array(head.length + body.length); + out.set(head, 0); + out.set(body, head.length); + return out; + } + + private static _parseRemotingPacket(buf: ArrayBuffer): { target: string; body: ArrayBuffer }[] { + const dv = new DataView(buf); + const dec = new TextDecoder(); + let pos = 0; + const u16 = () => { const v = dv.getUint16(pos); pos += 2; return v; }; + const u32 = () => { const v = dv.getUint32(pos); pos += 4; return v; }; + const utf = () => { const n = u16(); const s = dec.decode(new Uint8Array(buf, pos, n)); pos += n; return s; }; + const messages: { target: string; body: ArrayBuffer }[] = []; + u16(); // version + const headers = u16(); + for (let h = 0; h < headers; h++) { + utf(); pos += 1; // name, mustUnderstand + const len = u32(); + if (len !== 0xffffffff) pos += len; + } + const count = u16(); + for (let m = 0; m < count; m++) { + const target = utf(); + utf(); // response uri (unused in replies) + const len = u32(); + let body: ArrayBuffer; + if (len !== 0xffffffff && pos + len <= buf.byteLength) { + body = buf.slice(pos, pos + len); + pos += len; + } else { + body = buf.slice(pos); + pos = buf.byteLength; + } + messages.push({ target: target, body: body }); + } + return messages; } // AS -> JS Bindings @@ -88,70 +315,19 @@ export class NetConnection extends EventDispatcher { } connect(command: string): void { - /* - command = axCoerceString(command); - - release || somewhatImplemented("public flash.net.NetConnection::connect"); - this._uri = command; - if (!command) { - this._connected = true; - this.dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, - this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Success'}))); - } else { - var parsedURL = RtmpJs.parseConnectionString(command); - if (!parsedURL || !parsedURL.host || - (parsedURL.protocol !== 'rtmp' && parsedURL.protocol !== 'rtmpt' && parsedURL.protocol !== 'rtmps')) { - this.dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, - this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Failed'}))); - return; - } - - var service: IRootElementService = this.sec.player; - - var rtmpProps = this.sec.createObjectFromJS({ - app: parsedURL.app, - flashver: Capabilities.version, - swfUrl: service.swfUrl, - tcUrl: command, - fpad: false, - audioCodecs: 0x0FFF, - videoCodecs: 0x00FF, - videoFunction: 1, - pageUrl: service.pageUrl || service.swfUrl, - objectEncoding: 0 - }); - - this._protocol = parsedURL.protocol; - - var secured = parsedURL.protocol === 'rtmps' || - (parsedURL.protocol === 'rtmpt' && (parsedURL.port === 443 || parsedURL.port === 8443)); - this._usingTLS = secured; - var rtmpConnection: RtmpJs.BaseTransport = parsedURL.protocol === 'rtmp' || parsedURL.protocol === 'rtmps' ? - new RtmpJs.Browser.RtmpTransport({ host: parsedURL.host, port: parsedURL.port || 1935, ssl: secured }) : - new RtmpJs.Browser.RtmptTransport({ host: parsedURL.host, port: parsedURL.port || 80, ssl: secured }); - this._rtmpConnection = rtmpConnection; - this._rtmpCreateStreamCallbacks = [null, null]; // reserve first two - - rtmpConnection.onresponse = function (e) { - // - }; - rtmpConnection.onevent = function (e) { - // - }; - rtmpConnection.onconnected = function (e) { - this._connected = true; - this.dispatchEvent(new this.sec.flash.events.NetStatusEvent(events.NetStatusEvent.NET_STATUS, - false, false, - this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Success'}))); - }.bind(this); - rtmpConnection.onstreamcreated = function (e) { - console.log('#streamcreated: ' + e.streamId); - var callback = this._rtmpCreateStreamCallbacks[e.transactionId]; - delete this._rtmpCreateStreamCallbacks[e.transactionId]; - callback(e.stream, e.streamId); - }.bind(this); - rtmpConnection.connect(rtmpProps); - }*/ + command = axCoerceString(command); + this._uri = command; + // A null command or an HTTP(S) url is a Flash Remoting gateway connection: it always + // "succeeds" immediately — calls are independent HTTP POSTs. Dispatch async so + // listeners registered right after connect() still receive the event. + if (!command || !/^rtmpt?s?:/i.test(command)) { + this._connected = true; + Promise.resolve().then(() => this._dispatchStatus('NetConnection.Connect.Success', 'status')); + return; + } + // RTMP / RTMPT / RTMPS streaming transports are not implemented. + console.warn('[NetConnection] rtmp transports are not implemented:', command); + this._dispatchStatus('NetConnection.Connect.Failed', 'error'); } _createRtmpStream(callback) { diff --git a/lib/net/Responder.ts b/lib/net/Responder.ts index 6a0d80b3..627e2ad0 100644 --- a/lib/net/Responder.ts +++ b/lib/net/Responder.ts @@ -42,4 +42,16 @@ export class Responder extends ASObject { this._result = result; this._status = status; } + + /** Internal: invoke the result callback (used by NetConnection HTTP/AMF remoting). */ + invokeResult(value: any): void { + if (this._result) + ( this._result).axApply(null, [value]); + } + + /** Internal: invoke the status/fault callback (used by NetConnection HTTP/AMF remoting). */ + invokeStatus(value: any): void { + if (this._status) + ( this._status).axApply(null, [value]); + } } \ No newline at end of file diff --git a/lib/ui/MouseCursorData.ts b/lib/ui/MouseCursorData.ts new file mode 100644 index 00000000..1a343187 --- /dev/null +++ b/lib/ui/MouseCursorData.ts @@ -0,0 +1,44 @@ +import { ASObject } from '@awayfl/avm2'; + +/** + * flash.ui.MouseCursorData — data holder for Mouse.registerCursor(). + * + * Custom native mouse cursors are not rendered by AwayFL (Mouse.registerCursor is a + * no-op), but content that constructs this class must not hit + * "Class native is not defined: flash.ui.MouseCursorData", so a working data holder + * is linked. (A fresh implementation: the _shumway_flash version has stale imports.) + */ +export class MouseCursorData extends ASObject { + + static classInitializer: any = null; + static classSymbols: string[] = null; + static instanceSymbols: string[] = null; + + private _data: any = null; + private _hotSpot: any = null; + private _frameRate: number = 0; + + get data(): any { + return this._data; + } + + set data(value: any) { + this._data = value; + } + + get hotSpot(): any { + return this._hotSpot; + } + + set hotSpot(value: any) { + this._hotSpot = value; + } + + get frameRate(): number { + return this._frameRate; + } + + set frameRate(value: number) { + this._frameRate = +value; + } +}