UNPKG

@awayfl/avm1

Version:

Virtual machine for executing AS1 and AS2 code

543 lines (471 loc) 15.5 kB
import { IDisplayObjectAdapter, IFilter } from '@awayjs/scene'; import { IAVM1Context, AVM1PropertyFlags, alToString, alIsName, IAVM1Callable, AVM1DefaultValueHint, alIsFunction } from '../runtime'; import { IAsset } from '@awayjs/core'; import { AVM1Context } from '../context'; import { release, Debug } from '@awayfl/swf-loader'; import { AVM1PropertyDescriptor } from './AVM1PropertyDescriptor'; /** * Base class for object instances we prefer to not inherit Object.prototype properties. */ export class NullPrototypeObject { } const DEBUG_PROPERTY_PREFIX = '$Bg'; /** * Base class for the ActionScript AVM1 object. */ export class AVM1Object extends NullPrototypeObject implements IDisplayObjectAdapter { // Using our own bag of properties public _ownProperties: any; public _prototype: AVM1Object; public _avm1Context: IAVM1Context; public adaptee: IAsset; public avmType: string; protected initialDepth: number=0; protected scriptRefsToChilds: any={}; public _eventObserver: AVM1Object; public _blockedByScript: boolean; public _ctBlockedByScript: boolean; public protoTypeChanged: boolean; protected _visibilityByScript: boolean; // mark that object is GHOST, FP not allow assign/get/call props in this mode, instanceOf always is false private _isGhost: boolean = false; public get isGhost() { return this._isGhost; } public get eventObserver(): AVM1Object { return this._eventObserver; } public set eventObserver(value: AVM1Object) { this._eventObserver = value; } /** * Move object to ghost mode, we can't recover back from this mode, all props and methods will be undef */ public makeGhost() { // remove all props that was assigned in runtime // require for batch3/DarkValentine // moved this into this condition. required for chickClick level-button issue this.deleteOwnProperties(); // drop prototype, instanceOf always will false this.alPut('__proto__', null); this._isGhost = true; } public dispose(): any { } public updateFilters(newFilters: IFilter[]) { /*let filter: IFilter; for (let f = 0; f < newFilters.length; f++) { filter = newFilters[f]; }*/ // console.warn('[AVM1Object] update_filters not implemented'); } public isBlockedByScript(): boolean { return this._blockedByScript; } public isColorTransformByScript(): boolean { return this._ctBlockedByScript; } public isVisibilityByScript(): boolean { return this._visibilityByScript; } public initAdapter(): void { } public freeFromScript(): void { this.protoTypeChanged = false; this._blockedByScript = false; this._ctBlockedByScript = false; this._visibilityByScript = false; } public clone() { const newAVM1Object: AVM1Object = new AVM1Object(this._avm1Context); return newAVM1Object; } public get context(): AVM1Context { // too painful to have it as IAVM1Context return <AVM1Context> this._avm1Context; } public constructor(avm1Context: IAVM1Context) { super(); this._avm1Context = avm1Context; this._ownProperties = Object.create(null); this.scriptRefsToChilds = {}; this._prototype = null; this._blockedByScript = false; this._ctBlockedByScript = false; this._visibilityByScript = false; const self = this; // Using IAVM1Callable here to avoid circular calls between AVM1Object and // AVM1Function during constructions. // TODO do we need to support __proto__ for all SWF versions? const getter = { alCall: function (thisArg: any, args?: any[]): any { return self.alPrototype; } }; const setter = { alCall: function (thisArg: any, args?: any[]): any { self.alPrototype = args[0]; } }; const desc = new AVM1PropertyDescriptor(AVM1PropertyFlags.ACCESSOR | AVM1PropertyFlags.DONT_DELETE | AVM1PropertyFlags.DONT_ENUM, null, getter, setter); this.alSetOwnProperty('__proto__', desc); } get alPrototype(): AVM1Object { return this._prototype; } set alPrototype(v: AVM1Object) { // checking for circular references let p = v; while (p) { if (p === this) { return; // possible loop in __proto__ chain is found } p = p.alPrototype; } // TODO recursive chain check this._prototype = v; } public alGetPrototypeProperty(): AVM1Object { return this.alGet('prototype'); } // TODO shall we add mode for readonly/native flags of the prototype property? public alSetOwnPrototypeProperty(v: any): void { this.alSetOwnProperty('prototype', new AVM1PropertyDescriptor(AVM1PropertyFlags.DATA | AVM1PropertyFlags.DONT_ENUM, v)); } public alGetConstructorProperty(): AVM1Object { return this.alGet('__constructor__'); } public alSetOwnConstructorProperty(v: any): void { this.alSetOwnProperty('__constructor__', new AVM1PropertyDescriptor(AVM1PropertyFlags.DATA | AVM1PropertyFlags.DONT_ENUM, v)); } _debugEscapeProperty(p: any): string { const context = this.context; let name = alToString(context, p); if (!context.isPropertyCaseSensitive) { name = name.toLowerCase(); } return DEBUG_PROPERTY_PREFIX + name; } public alGetOwnProperty(name: string | number): AVM1PropertyDescriptor { if (this._isGhost) { return null; } if (typeof name === 'string' && !this.context.isPropertyCaseSensitive) { name = name.toLowerCase(); } release || Debug.assert(alIsName(this.context, name)); // TODO __resolve return this._ownProperties[name]; } public alSetOwnProperty(propName: string | number, desc: AVM1PropertyDescriptor): void { if (this._isGhost) { return; } const name = this.context.normalizeName(propName); if (!desc.originalName && !this.context.isPropertyCaseSensitive) { desc.originalName = propName; } if (!release) { Debug.assert(desc instanceof AVM1PropertyDescriptor); // Ensure that a descriptor isn't used multiple times. If it were, we couldn't update // values in-place. Debug.assert(!desc['owningObject'] || desc['owningObject'] === this); desc['owningObject'] = this; // adding data property on the main object for convenience of debugging. if ((desc.flags & AVM1PropertyFlags.DATA) && !(desc.flags & AVM1PropertyFlags.DONT_ENUM)) { Object.defineProperty(this, this._debugEscapeProperty(name), { value: desc.value, enumerable: true, configurable: true }); } } this._ownProperties[name] = desc; } public alHasOwnProperty(propName: string | number): boolean { if (this._isGhost) { return false; } const name = this.context.normalizeName(propName); return !!this._ownProperties[name]; } public alDeleteOwnProperty(propName: string | number): void { const name = this.context.normalizeName(propName); delete this._ownProperties[name]; if (!release) { delete this[this._debugEscapeProperty(propName)]; } } public deleteOwnProperties() { const allProps = this.alGetOwnPropertiesKeys(); for (let i = 0;i < allProps.length;i++) { this.alDeleteOwnProperty(allProps[i]); } } public alGetOwnPropertiesKeys(): string[] { const keys: string[] = []; if (this._isGhost) { return keys; } let desc; if (!this.context.isPropertyCaseSensitive) { for (const name in this._ownProperties) { desc = this._ownProperties[name]; release || Debug.assert('originalName' in desc); if (!(desc.flags & AVM1PropertyFlags.DONT_ENUM)) { keys.push(desc.originalName); } } } else { for (const name in this._ownProperties) { desc = this._ownProperties[name]; if (!(desc.flags & AVM1PropertyFlags.DONT_ENUM)) { keys.push(name); } } } return keys; } public alGetProperty(propName: string | number): AVM1PropertyDescriptor { if (this._isGhost) { return null; } const desc = this.alGetOwnProperty(propName); if (desc) { return desc; } if (!this._prototype) { return undefined; } return this._prototype.alGetProperty(propName); } public alGet(propName: string | number): any { if (this._isGhost) { return void 0; } const name = this.context.normalizeName(propName); const desc = this.alGetProperty(name); if (!desc) { return void 0; } if ((desc.flags & AVM1PropertyFlags.DATA)) { const val = desc.value; // redurant, XML should return value direct // for xml nodes we need to return the nodeValue // https://developer.mozilla.org/ru/docs/Web/API/Node/nodeType // if (val && (val.nodeType == 2 /* Attr */ || val.nodeType == 3 /* Text */ || val.nodeValue)) { // return desc.value.nodeValue; // } return val; } release || Debug.assert(!!(desc.flags & AVM1PropertyFlags.ACCESSOR)); const getter = desc.get; return getter ? getter.alCall(this) : void 0; } public alCanPut(propName: string | number): boolean { if (this._isGhost) { return false; } const desc = this.alGetOwnProperty(propName); if (desc) { if ((desc.flags & AVM1PropertyFlags.ACCESSOR)) { return !!desc.set; } else { return !(desc.flags & AVM1PropertyFlags.READ_ONLY); } } const proto = this._prototype; if (!proto) { return true; } return proto.alCanPut(propName); } public alPut(propName: string | number, value: any): void { if (this._isGhost) { return; } // Perform all lookups with the canonicalized name, but keep the original name around to // pass it to `alSetOwnProperty`, which stores it on the descriptor. const originalName = propName; propName = this.context.normalizeName(propName); // stupid hack to make sure we can update references to objects in cases when the timeline changes the objects // if a new object is registered for the same name, we can use the "scriptRefsToChilds" // to update all references to the old object with the new one if (value && typeof value === 'object' && value.avmType === 'symbol' && propName != 'this' && propName != '_parent' && !value.dynamicallyCreated) { if (value.adaptee && value.adaptee.parent && value.adaptee.parent.adapter && value.adaptee.parent.adapter.scriptRefsToChilds) { value.adaptee.parent.adapter.scriptRefsToChilds[value.adaptee.name] = { obj:this, name:propName }; } } if (!this.alCanPut(propName)) { return; } const ownDesc = this.alGetOwnProperty(propName); if (ownDesc && (ownDesc.flags & AVM1PropertyFlags.DATA)) { if (ownDesc.watcher) { value = ownDesc.watcher.callback.alCall(this, [ownDesc.watcher.name, ownDesc.value, value, ownDesc.watcher.userData]); } // Real properties (i.e., not things like "_root" on MovieClips) can be updated in-place. if (propName in this._ownProperties) { ownDesc.value = value; } else { this.alSetOwnProperty(originalName, new AVM1PropertyDescriptor(ownDesc.flags, value)); } return; } if (typeof value === 'undefined' && (propName == '_x' || propName == '_y' || propName == '_xscale' || propName == '_yscale' || propName == '_width' || propName == '_height')) { // certain props do not allow their value to be set to "undefined", so we exit here // todo: there might be more props that do not allow "undefined" return; } const desc = this.alGetProperty(propName); if (desc && (desc.flags & AVM1PropertyFlags.ACCESSOR)) { if (desc.watcher) { const oldValue = desc.get ? desc.get.alCall(this) : undefined; value = desc.watcher.callback.alCall(this, [desc.watcher.name, oldValue, value, desc.watcher.userData]); } const setter = desc.set; release || Debug.assert(setter); setter.alCall(this, [value]); } else { if (desc && desc.watcher) { release || Debug.assert(desc.flags & AVM1PropertyFlags.DATA); value = desc.watcher.callback.alCall(this, [desc.watcher.name, desc.value, value, desc.watcher.userData]); } if (value && value.isTextVar) { value = value.value; const newDesc = new AVM1PropertyDescriptor(desc ? desc.flags : AVM1PropertyFlags.DATA, value); (<any>newDesc).isTextVar = true; this.alSetOwnProperty(originalName, newDesc); } else { const newDesc = new AVM1PropertyDescriptor(desc ? desc.flags : AVM1PropertyFlags.DATA, value); this.alSetOwnProperty(originalName, newDesc); } } } public alHasProperty(p): boolean { if (this._isGhost) { return false; } const desc = this.alGetProperty(p); return !!desc; } public alDeleteProperty(propName: string | number): boolean { const desc = this.alGetOwnProperty(propName); if (!desc) { return true; } if ((desc.flags & AVM1PropertyFlags.DONT_DELETE)) { return false; } this.alDeleteOwnProperty(propName); return true; } public alAddPropertyWatcher(propName: string | number, callback: IAVM1Callable, userData: any): boolean { if (this._isGhost) { return false; } // TODO verify/test this functionality to match ActionScript const desc = this.alGetProperty(propName); if (!desc) { return false; } desc.watcher = { name: propName, callback: callback, userData: userData }; return true; } public alRemotePropertyWatcher(p: any): boolean { const desc = this.alGetProperty(p); if (!desc || !desc.watcher) { return false; } desc.watcher = undefined; return true; } public alDefaultValue(hint: AVM1DefaultValueHint = AVM1DefaultValueHint.NUMBER): any { if (hint === AVM1DefaultValueHint.STRING) { const toString = this.alGet(this.context.normalizeName('toString')); if (alIsFunction(toString)) { return toString.alCall(this); } const valueOf = this.alGet(this.context.normalizeName('valueOf')); if (alIsFunction(valueOf)) { return valueOf.alCall(this); } } else { release || Debug.assert(hint === AVM1DefaultValueHint.NUMBER); const valueOf = this.alGet(this.context.normalizeName('valueOf')); if (alIsFunction(valueOf)) { return valueOf.alCall(this); } const toString = this.alGet(this.context.normalizeName('toString')); if (alIsFunction(toString)) { return toString.alCall(this); } } // TODO is this a default? return this; } public alGetKeys(): string[] { if (this._isGhost) { return []; } const ownKeys = this.alGetOwnPropertiesKeys(); const proto = this._prototype; if (!proto) { return ownKeys; } const otherKeys = proto.alGetKeys(); if (ownKeys.length === 0) { return otherKeys; } // Merging two keys sets // TODO check if we shall worry about __proto__ usage here const context = this.context; let k: number; // If the context is case-insensitive, names only differing in their casing overwrite each // other. Iterating over the keys returns the first original, case-preserved key that was // ever used for the property, though. if (!context.isPropertyCaseSensitive) { const keyLists = [ownKeys, otherKeys]; const canonicalKeysMap = Object.create(null); const keys = []; let keyList; let key; let canonicalKey; for (k = 0; k < keyLists.length; k++) { keyList = keyLists[k]; for (let i = 0; i < keyList.length; i++) { key = keyList[i]; canonicalKey = context.normalizeName(key); if (canonicalKeysMap[canonicalKey]) { continue; } canonicalKeysMap[canonicalKey] = true; keys.push(key); } } return keys; } else { const processed = Object.create(null); const keyLength1: number = ownKeys.length; for (k = 0; k < keyLength1; k++) { processed[ownKeys[k]] = true; } const keyLength2: number = otherKeys.length; for (k = 0; k < keyLength2; k++) { processed[otherKeys[k]] = true; } return Object.getOwnPropertyNames(processed); } } }