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
This commit is contained in:
jberg 2019-08-01 17:25:03 -07:00 committed by Jordan Eldredge
parent 1efded00d3
commit 769c7b9577
15 changed files with 295 additions and 70 deletions

View file

@ -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 (
<div
data-node-type="button"
@ -144,6 +154,7 @@ function Button({ id, image, action, x, y, downImage, tooltip, children }) {
setDown(false);
});
}}
{...eventHandlers}
title={tooltip}
style={{
position: "absolute",
@ -192,19 +203,19 @@ const NODE_NAME_TO_COMPONENT = {
// Given a skin XML node, pick which component to use, and render it.
function XmlNode({ node }) {
const attributes = node.attributes;
const name = node.name;
const attributes = node.xmlNode.attributes;
const name = node.xmlNode.name;
if (attributes && IGNORE_IDS.has(attributes.id)) {
return null;
}
if (name == null) {
// This is likely a comment
if (name == null || name === "groupdef") {
// name is null is likely a comment
return null;
}
const Component = NODE_NAME_TO_COMPONENT[name];
const childNodes = node.children || [];
const children = childNodes.map((childNode, i) => (
<XmlNode key={i} parent={node} node={childNode} />
<XmlNode key={i} node={childNode} />
));
if (Component == null) {
console.warn("Unknown node type", name);
@ -213,23 +224,46 @@ function XmlNode({ node }) {
}
return null;
}
return <Component {...attributes}>{children}</Component>;
return <Component node={node} {...attributes}>{children}</Component>;
}
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 <h1>Loading...</h1>;
}
const [skinXml, nodes, registry] = data;
const { root, registry } = data;
return (
<SkinContext.Provider value={registry.images}>
<XmlNode
node={nodes}
/>
<XmlNode node={root} />
</SkinContext.Provider>
);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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}`);
}

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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()
*

View file

@ -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";

View file

@ -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;

View file

@ -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,
};

View file

@ -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;
}