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