From 769c7b9577c15d021e0637c700242ef95e3ad4e4 Mon Sep 17 00:00:00 2001 From: jberg Date: Thu, 1 Aug 2019 17:25:03 -0700 Subject: [PATCH] Initialize maki object tree (#823) * make initialize tree out of maki objects * pass global flag to variables, only need hooks for global variables * implement new opcode * start using non generic makiobject * run scripts * fix running scripts * skip standardframe for now * switch to simple example for now * make WAL a group * move xml/init functions to be in GuiObject * handle creating backing XML nodes for objects created (new) in scripts * simpler null check * add js_ prefixes * only bind event handlers for hooks if they are present * uppercase for Classes * make null checking explicit * need to check undefined, not null * implement init for adding nodes to state tree * dont render groupdefs, dont execute scripts in groupdefs * implement getContainer * move find descendants to utils --- experiments/modern/src/App.js | 70 ++++++++++++----- experiments/modern/src/initialize.js | 75 +++++++++---------- .../modern/src/maki-interpreter/parser.js | 6 +- .../modern/src/maki-interpreter/variable.js | 6 +- .../src/maki-interpreter/virtualMachine.js | 8 ++ experiments/modern/src/runtime/Button.js | 15 ++++ experiments/modern/src/runtime/Container.js | 19 +++++ experiments/modern/src/runtime/Group.js | 8 +- experiments/modern/src/runtime/GuiObject.js | 27 +++++++ experiments/modern/src/runtime/Layout.js | 19 +++++ experiments/modern/src/runtime/MakiObject.js | 38 +++++++++- experiments/modern/src/runtime/System.js | 16 +++- .../src/runtime/WinampAbstractionLayer.js | 16 ++++ experiments/modern/src/runtime/index.js | 6 +- experiments/modern/src/utils.js | 36 +++++++++ 15 files changed, 295 insertions(+), 70 deletions(-) create mode 100644 experiments/modern/src/runtime/Button.js create mode 100644 experiments/modern/src/runtime/Container.js create mode 100644 experiments/modern/src/runtime/Layout.js create mode 100644 experiments/modern/src/runtime/WinampAbstractionLayer.js diff --git a/experiments/modern/src/App.js b/experiments/modern/src/App.js index 20ffc5b5..d5ed95c9 100644 --- a/experiments/modern/src/App.js +++ b/experiments/modern/src/App.js @@ -3,6 +3,9 @@ import JSZip from "jszip"; import "./App.css"; import * as Utils from "./utils"; import initialize from "./initialize"; +const System = require("./runtime/System"); +const runtime = require("./runtime"); +const interpret = require("./maki-interpreter/interpreter"); // const runtime = require("./maki-interpreter/runtime"); // const System = require("./maki-interpreter/runtime/System"); @@ -34,7 +37,8 @@ const IGNORE_IDS = new Set([ const SkinContext = React.createContext(null); async function getSkin() { - const resp = await fetch(process.env.PUBLIC_URL + "/skins/CornerAmp_Redux.wal"); + // const resp = await fetch(process.env.PUBLIC_URL + "/skins/CornerAmp_Redux.wal"); + const resp = await fetch(process.env.PUBLIC_URL + "/skins/simple.wal"); const blob = await resp.blob(); const zip = await JSZip.loadAsync(blob); const skinXml = await Utils.inlineIncludes( @@ -42,11 +46,7 @@ async function getSkin() { zip ); - const { nodes, registry } = await initialize(zip, skinXml); - - // Gross hack returing a tuple here. We're just doing some crazy stuff to get - // some data returned in the laziest way possible - return [skinXml, nodes, registry]; + return await initialize(zip, skinXml); } function Layout({ @@ -118,7 +118,7 @@ function Layer({ id, image, children, x, y }) { ); } -function Button({ id, image, action, x, y, downImage, tooltip, children }) { +function Button({ id, image, action, x, y, downImage, tooltip, node, children }) { const data = React.useContext(SkinContext); const [down, setDown] = React.useState(false); const imgId = down && downImage ? downImage : image; @@ -133,6 +133,16 @@ function Button({ id, image, action, x, y, downImage, tooltip, children }) { return null; } + const hooks = node.js_getActiveHooks(); + const eventHandlers = {}; + if (hooks.includes("onLeftClick")) { + eventHandlers["onClick"] = e => { + if (hooks.includes("onLeftClick")) { + node.js_trigger("onLeftClick"); + } + }; + } + return (
( - + )); if (Component == null) { console.warn("Unknown node type", name); @@ -213,23 +224,46 @@ function XmlNode({ node }) { } return null; } - return {children}; + return {children}; } function App() { const [data, setData] = React.useState(null); React.useEffect(() => { - getSkin().then(setData); + getSkin().then(async ({ root, registry }) => { + // Execute scripts + await Utils.asyncTreeFlatMap(root, async node => { + switch (node.xmlNode.name) { + case "groupdef": { + // removes groupdefs from consideration (only run scripts when actually referenced by group) + return {}; + } + case "script": { + // TODO: stop ignoring standardframe + if (node.xmlNode.file.endsWith("standardframe.maki")) { + break; + } + const scriptGroup = Utils.findParentNodeOfType(node, ["group", "WinampAbstractionLayer"]); + const system = new System(scriptGroup); + await interpret({ runtime, data: node.xmlNode.script, system, log: false }); + return node; + } + default: { + return node; + } + } + }); + + setData({ root, registry }); + }); }, []); if (data == null) { return

Loading...

; } - const [skinXml, nodes, registry] = data; + const { root, registry } = data; return ( - + ); } diff --git a/experiments/modern/src/initialize.js b/experiments/modern/src/initialize.js index ad186060..28ea367f 100644 --- a/experiments/modern/src/initialize.js +++ b/experiments/modern/src/initialize.js @@ -1,4 +1,10 @@ import * as Utils from "./utils"; +const MakiObject = require("./runtime/MakiObject"); +const WinampAbstractionLayer = require("./runtime/WinampAbstractionLayer"); +const Layout = require('./runtime/Layout'); +const Container = require('./runtime/Container'); +const Group = require("./runtime/Group"); +const Button = require("./runtime/Button"); function splitValues(str) { return str.split(",").map(parseFloat); @@ -14,11 +20,6 @@ async function loadImage(imgUrl) { }); } -let idCount = 0; -function getId() { - return '_' + idCount++; -} - const schema = { groupdef: [ "scripts", @@ -115,14 +116,14 @@ const schema = { accelerators: ["accelerator"], }; -const noop = (node) => node; +const noop = (node, parent) => new MakiObject(node, parent); const parsers = { groupdef: (node, parent, registry) => { const attributeId = node.attributes.id; registry.groupdefs[attributeId] = node; - return node; + return new MakiObject(node, parent); }, skininfo: noop, version: noop, @@ -132,7 +133,7 @@ const parsers = { email: noop, homepage: noop, screenshot: noop, - container: noop, + container: (node, parent) => new Container(node, parent), scripts: noop, gammaset: (node, parent, registry) => { const gammaId = node.attributes.id; @@ -140,29 +141,30 @@ const parsers = { registry.gammasets[gammaId] = {}; } - return node; + return new MakiObject(node, parent); }, color: noop, layer: noop, layoutstatus: noop, hideobject: noop, - button: noop, + button: (node, parent) => new Button(node, parent), group: (node, parent, registry) => { if (!node.children || node.children.length === 0) { const groupdef = registry.groupdefs[node.attributes.id]; if (groupdef) { - return { + const newNode = { ...node, ...groupdef, attributes: { ...node.attributes, ...groupdef.attributes }, name: "group", }; + return new Group(newNode, parent); } } - return node; + return new Group(node, parent); }, - layout: noop, + layout: (node, parent) => new Layout(node, parent), sendparams: noop, elements: noop, bitmap: async (node, parent, registry, zip) => { @@ -170,7 +172,7 @@ const parsers = { // TODO: Escape file for regex const img = Utils.getCaseInsensitveFile(zip, file); if (img === undefined) { - return node; + return new MakiObject(node, parent); } const imgBlob = await img.async("blob"); const imgUrl = URL.createObjectURL(imgBlob); @@ -183,23 +185,22 @@ const parsers = { } registry.images[id.toLowerCase()] = { file, gammagroup, h, w, x, y, imgUrl }; - return node; + return new MakiObject(node, parent); }, eqvis: noop, slider: noop, gammagroup: (node, parent, registry) => { - const gammaId = parent.attributes.id; + const gammaId = parent.xmlNode.attributes.id; const attributeId = node.attributes.id; const attributeValues = splitValues(node.attributes.value); registry.gammasets[gammaId][attributeId] = attributeValues; - return node; + return new MakiObject(node, parent); }, truetypefont: noop, component: noop, text: noop, layer: noop, - button: noop, togglebutton: noop, status: noop, slider: noop, @@ -228,28 +229,29 @@ const parsers = { const script = await Utils.readUint8array(zip, file); registry.scripts.push({ parent, id, param, script }); - return { ...node, script, param }; + const newNode = { ...node, script, param, file }; + return new MakiObject(newNode, parent); }, }; async function parseChildren(node, registry, zip) { - if (node.type === "comment") { + if (node.xmlNode.type === "comment") { return; } - if (node.name == null) { - console.error(node); + if (node.xmlNode.name == null) { + console.error(node.xmlNode); throw new Error("Unknown node"); } - const validChildren = new Set(schema[node.name.toLowerCase()]); + const validChildren = new Set(schema[node.xmlNode.name.toLowerCase()]); const resolvedChildren = await Promise.all( - node.children.map(async child => { + node.xmlNode.children.map(async child => { if (child.type === "comment") { return; } if (child.type === "text") { // TODO: Handle text - return { ...child, id: getId() }; + return new MakiObject({ ...child }, node); } if (child.name == null) { console.error(child); @@ -257,12 +259,12 @@ async function parseChildren(node, registry, zip) { } const childName = child.name.toLowerCase(); if (childName == null) { - console.error(node); + console.error(node.xmlNode); throw new Error("Unknown node"); } if (!validChildren.has(childName)) { - throw new Error(`Invalid child of a ${node.name}: ${childName}`); + throw new Error(`Invalid child of a ${node.xmlNode.name}: ${childName}`); } const childParser = parsers[childName]; @@ -271,28 +273,23 @@ async function parseChildren(node, registry, zip) { return; } const parsedChild = await childParser(child, node, registry, zip); - const returnNode = { ...parsedChild, id: getId() }; - if (parsedChild.children != null) { - const parsedChildren = await parseChildren(parsedChild, registry, zip); - returnNode.children = parsedChildren.children; + if (parsedChild.xmlNode.children != null && parsedChild.xmlNode.children.length > 0) { + await parseChildren(parsedChild, registry, zip); } - return returnNode; + return parsedChild; }) ); // remove comments other trimmed nodes const filteredChildren = resolvedChildren.filter(item => item !== undefined); - return { - ...node, - children: filteredChildren - }; + node.js_addChildren(filteredChildren); } async function initialize(zip, skinXml) { const registry = { scripts: [], gammasets: {}, images: {}, groupdefs: {} }; - const nodes = await parseChildren(skinXml.children[0], registry, zip); - nodes.id = getId(); - return { nodes, registry }; + const root = new WinampAbstractionLayer(skinXml.children[0], null); + await parseChildren(root, registry, zip); + return { root, registry }; } export default initialize; diff --git a/experiments/modern/src/maki-interpreter/parser.js b/experiments/modern/src/maki-interpreter/parser.js index 9f492346..929529d9 100644 --- a/experiments/modern/src/maki-interpreter/parser.js +++ b/experiments/modern/src/maki-interpreter/parser.js @@ -160,13 +160,13 @@ function readVariables({ makiFile, classes }) { if (klass == null) { throw new Error("Invalid type"); } - variables.push(new Variable({ type: klass, typeName: "OBJECT" })); + variables.push(new Variable({ type: klass, typeName: "OBJECT", global: !!global })); } else if (subClass) { const variable = variables[typeOffset]; if (variable == null) { throw new Error("Invalid type"); } - variables.push(new Variable({ type: variable, typeName: "SUBCLASS" })); + variables.push(new Variable({ type: variable, typeName: "SUBCLASS", global: !!global })); } else { const typeName = PRIMITIVE_TYPES[typeOffset]; if (typeName == null) { @@ -195,7 +195,7 @@ function readVariables({ makiFile, classes }) { default: throw new Error("Invalid primitive type"); } - const variable = new Variable({ type: typeName, typeName }); + const variable = new Variable({ type: typeName, typeName, global: !!global }); variable.setValue(value); variables.push(variable); } diff --git a/experiments/modern/src/maki-interpreter/variable.js b/experiments/modern/src/maki-interpreter/variable.js index e0a60162..72f184b1 100644 --- a/experiments/modern/src/maki-interpreter/variable.js +++ b/experiments/modern/src/maki-interpreter/variable.js @@ -1,9 +1,10 @@ const Emitter = require("../Emitter"); class Variable { - constructor({ type, typeName }) { + constructor({ type, typeName, global }) { this.type = type; this.typeName = typeName; + this.global = global; this._emitter = new Emitter(); this._unsubscribeFromValue = null; } @@ -16,10 +17,11 @@ class Variable { if (this._unsubscribeFromValue != null) { this._unsubscribeFromValue(); } - if (this.typeName === "OBJECT") { + if (this.global && this.typeName === "OBJECT" && value !== null) { this._unsubscribeFromValue = value.js_listenToAll((eventName, args) => { this._emitter.trigger(eventName, args); }); + value.js_updateHooks(this, Object.keys(this._emitter._hooks)); } this._value = value; } diff --git a/experiments/modern/src/maki-interpreter/virtualMachine.js b/experiments/modern/src/maki-interpreter/virtualMachine.js index ec4b70d0..1ec632fd 100644 --- a/experiments/modern/src/maki-interpreter/virtualMachine.js +++ b/experiments/modern/src/maki-interpreter/virtualMachine.js @@ -287,6 +287,14 @@ function* interpret(start, program, stack = []) { twoArgOperator((b, a) => b >> a); break; } + // new + case 96: { + const classesOffset = command.arg; + const Klass = classes[classesOffset]; + const klassInst = new Klass(); + stack.push(klassInst); + break; + } default: throw new Error(`Unhandled opcode ${command.opcode}`); } diff --git a/experiments/modern/src/runtime/Button.js b/experiments/modern/src/runtime/Button.js new file mode 100644 index 00000000..574b163b --- /dev/null +++ b/experiments/modern/src/runtime/Button.js @@ -0,0 +1,15 @@ +const GuiObject = require("./GuiObject"); + +class Button extends GuiObject { + /** + * getClassName() + * + * Returns the class name for the object. + * @ret The class name. + */ + static getClassName() { + return "Button"; + } +} + +module.exports = Button; diff --git a/experiments/modern/src/runtime/Container.js b/experiments/modern/src/runtime/Container.js new file mode 100644 index 00000000..62f5d5e9 --- /dev/null +++ b/experiments/modern/src/runtime/Container.js @@ -0,0 +1,19 @@ +const GuiObject = require("./GuiObject"); +const { findDescendantByTypeAndId } = require("../utils"); + +class Container extends GuiObject { + /** + * getClassName() + * + * Returns the class name for the object. + * @ret The class name. + */ + static getClassName() { + return "Container"; + } + getLayout(id) { + return findDescendantByTypeAndId(this, "layout", id); + } +} + +module.exports = Container; diff --git a/experiments/modern/src/runtime/Group.js b/experiments/modern/src/runtime/Group.js index 069d9069..3c5c323b 100644 --- a/experiments/modern/src/runtime/Group.js +++ b/experiments/modern/src/runtime/Group.js @@ -10,11 +10,9 @@ class Group extends GuiObject { static getClassName() { return "Group"; } - findObject(id) { - throw new Error("Not implemented"); - } - setXmlParam(id, value) { - throw new Error("Not implemented"); + getObject(id) { + // Not sure this is correct, but it is my understanding this is just an alias + return this.findObject(id); } } diff --git a/experiments/modern/src/runtime/GuiObject.js b/experiments/modern/src/runtime/GuiObject.js index 77ac94c4..39c7ed95 100644 --- a/experiments/modern/src/runtime/GuiObject.js +++ b/experiments/modern/src/runtime/GuiObject.js @@ -1,4 +1,5 @@ const MakiObject = require("./MakiObject"); +const { findDescendantByTypeAndId } = require("../utils"); class GuiObject extends MakiObject { /** @@ -10,6 +11,32 @@ class GuiObject extends MakiObject { static getClassName() { return "GuiObject"; } + findObject(id) { + return findDescendantByTypeAndId(this, null, id); + } + init(newRoot) { + newRoot.js_addChild(this); + return this; + } + setXmlParam(param, value) { + this.xmlNode.attributes[param] = value; + return value; + } + // need to force all function names to lowercase in the interpreter + // bad hack for now + setXMLParam(param, value) { + return this.setXmlParam(param, value); + } + getXmlParam(param) { + const attributes = this.xmlNode.attributes; + if (attributes !== undefined && attributes.hasOwnProperty(param)) { + return attributes[param]; + } + return null; + } + getParent() { + return this.parent; + } } module.exports = GuiObject; diff --git a/experiments/modern/src/runtime/Layout.js b/experiments/modern/src/runtime/Layout.js new file mode 100644 index 00000000..79f93605 --- /dev/null +++ b/experiments/modern/src/runtime/Layout.js @@ -0,0 +1,19 @@ +const GuiObject = require("./GuiObject"); +const { findParentNodeOfType } = require("../utils"); + +class Layout extends GuiObject { + /** + * getClassName() + * + * Returns the class name for the object. + * @ret The class name. + */ + static getClassName() { + return "Layout"; + } + getContainer() { + return findParentNodeOfType(this, ["container"]);; + } +} + +module.exports = Layout; diff --git a/experiments/modern/src/runtime/MakiObject.js b/experiments/modern/src/runtime/MakiObject.js index 41a34807..6cd6068a 100644 --- a/experiments/modern/src/runtime/MakiObject.js +++ b/experiments/modern/src/runtime/MakiObject.js @@ -1,8 +1,34 @@ const Emitter = require("../Emitter"); class MakiObject { - constructor() { + constructor(node, parent) { + this.xmlNode = node; + this.parent = parent; + this.children = []; + this.hooks = {}; this._emitter = new Emitter(); + + if (!this.xmlNode) { + // When dynamically creating a new object with `new` we need to add an underlying "XML" + // node that we can edit + this.xmlNode = { + children: [], + attributes: { + id: null, + }, + // ugly but works for now + name: this.constructor.name.toLowerCase(), + type: 'element' + } + } + } + + js_addChild(child) { + this.children.push(child); + } + + js_addChildren(children) { + this.children = this.children.concat(children); } js_trigger(eventName, ...args) { @@ -17,6 +43,16 @@ class MakiObject { this._emitter.dispose(); } + // updating hooks like this is probably totally wrong, but just hacking for now + js_updateHooks (node, hooks) { + this.hooks[node] = hooks; + } + + js_getActiveHooks () { + const hookArrs = Object.values(this.hooks); + return hookArrs.reduce((acc, val) => acc.concat(val), []); + } + /** * getClassName() * diff --git a/experiments/modern/src/runtime/System.js b/experiments/modern/src/runtime/System.js index db24d8f5..bb81c3b6 100644 --- a/experiments/modern/src/runtime/System.js +++ b/experiments/modern/src/runtime/System.js @@ -1,7 +1,18 @@ const Group = require("./Group"); const MakiObject = require("./MakiObject"); +const { findDescendantByTypeAndId } = require("../utils"); class System extends MakiObject { + constructor(scriptGroup = new Group()) { + super(null, null); + + this.scriptGroup = scriptGroup; + this.root = scriptGroup; + while(this.root.parent) { + this.root = this.root.parent; + } + } + /** * getClassName() * @@ -17,7 +28,10 @@ class System extends MakiObject { } getScriptGroup() { - return new Group(); + return this.scriptGroup; + } + getContainer(id) { + return findDescendantByTypeAndId(this.root, "container", id); } getRuntimeVersion() { return "5.666"; diff --git a/experiments/modern/src/runtime/WinampAbstractionLayer.js b/experiments/modern/src/runtime/WinampAbstractionLayer.js new file mode 100644 index 00000000..0ef5e3fd --- /dev/null +++ b/experiments/modern/src/runtime/WinampAbstractionLayer.js @@ -0,0 +1,16 @@ +const Group = require("./Group"); + +// Needs to behavve like a group to work with top level scripts that call `getScriptGroup` +class WinampAbstractionLayer extends Group { + /** + * getClassName() + * + * Returns the class name for the object. + * @ret The class name. + */ + static getClassName() { + return "WinampAbstractionLayer"; + } +} + +module.exports = WinampAbstractionLayer; diff --git a/experiments/modern/src/runtime/index.js b/experiments/modern/src/runtime/index.js index c370580e..73ee70ca 100644 --- a/experiments/modern/src/runtime/index.js +++ b/experiments/modern/src/runtime/index.js @@ -1,10 +1,12 @@ const System = require("./System"); +const Button = require("./Button"); const Group = require("./Group"); +const Layout = require('./Layout'); +const Container = require('./Container'); const GuiObject = require("./GuiObject"); const MakiObject = require("./MakiObject"); class PopupMenu {} -class Container {} class List {} const runtime = { @@ -12,8 +14,10 @@ const runtime = { d6f50f6449b793fa66baf193983eaeef: System, "45be95e5419120725fbb5c93fd17f1f9": Group, "4ee3e1994becc636bc78cd97b028869c": GuiObject, + "698eddcd4fec8f1e44f9129b45ff09f9": Button, f4787af44ef7b2bb4be7fb9c8da8bea9: PopupMenu, e90dc47b4ae7840d0b042cb0fcf775d2: Container, + "60906d4e482e537e94cc04b072568861": Layout, b2023ab54ba1434d6359aebec6f30375: List, }; diff --git a/experiments/modern/src/utils.js b/experiments/modern/src/utils.js index 20daf977..52ee9a7d 100644 --- a/experiments/modern/src/utils.js +++ b/experiments/modern/src/utils.js @@ -96,3 +96,39 @@ export async function inlineIncludes(xml, zip) { return includedFile.children; }); } + +// Operations on trees +export function findParentNodeOfType(node, type) { + let n = node; + while(n.parent !== null) { + n = n.parent; + if ((!Array.isArray(type) && n.xmlNode.name === type) || + (Array.isArray(type) && type.includes(n.xmlNode.name))) { + return n; + } + } +} + +export function findDescendantByTypeAndId(node, type, id) { + if (node.children.length === 0) { + return null; + } + + for(let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if ((!type || child.xmlNode.name === type) && + (child.xmlNode.attributes !== undefined && child.xmlNode.attributes.id === id)) { + return child; + } + } + + for(let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const descendant = findDescendantByTypeAndId(child, type, id); + if (descendant) { + return descendant; + } + } + + return null; +}