Enable proper event hooking

This commit is contained in:
Jordan Eldredge 2021-06-16 00:54:40 -07:00
parent cb7e9e6882
commit 8650ae00b1
14 changed files with 219 additions and 67 deletions

View file

@ -1,8 +1,17 @@
import JSZip from "jszip";
// This module is imported early here in order to avoid a circular dependency.
import { classResolver } from "./skin/resolver";
import SkinParser from "./skin/parse";
function hack() {
// Without this Snowpack will try to treeshake out resolver causing a circular
// dependency.
classResolver("A funny joke about why this is needed.");
}
async function main() {
const response = await fetch("assets/CornerAmp_Redux.wal");
// const response = await fetch("assets/Default_winamp3_build499.wal");
const data = await response.blob();
const zip = await JSZip.loadAsync(data);
@ -22,6 +31,9 @@ async function main() {
document.body.appendChild(node);
setInterval(() => {
if (window.pause) {
return;
}
document.body.removeChild(node);
node = document.createElement("div");
for (const container of parser._containers) {

View file

@ -5,9 +5,11 @@ import { ParsedMaki, Command, Method } from "./parser";
export function interpret(
start: number,
program: ParsedMaki,
stack: Variable[],
classResolver: (guid: string) => any
) {
const interpreter = new Interpreter(program, classResolver);
interpreter.stack = stack;
return interpreter.interpret(start);
}
@ -18,7 +20,7 @@ class Interpreter {
variables: Variable[];
methods: Method[];
commands: Command[];
debug: boolean;
debug: boolean = false;
classResolver: (guid: string) => any;
constructor(program: ParsedMaki, classResolver: (guid: string) => any) {
const { commands, methods, variables, classes } = program;
@ -30,7 +32,6 @@ class Interpreter {
this.stack = [];
this.callStack = [];
this.debug = false;
}
interpret(start: number) {

View file

@ -2,7 +2,7 @@ import { assert, assume } from "../utils";
import { COMMANDS } from "./constants";
import { DataType, Variable } from "./v";
import MakiFile from "./MakiFile";
import { getMethod, getReturnType } from "./objects";
import { getReturnType } from "./objects";
export type Command = {
opcode: number;
@ -27,6 +27,8 @@ export type ParsedMaki = {
export type Binding = {
commandOffset: number;
methodOffset: number;
variableOffset: number;
binaryOffset: number;
};
const MAGIC = "FG";
@ -70,18 +72,24 @@ export function parse(data: ArrayBuffer): ParsedMaki {
}
});
const resolvedBindings = bindings.map((binding): Binding => {
return Object.assign({}, binding, {
commandOffset: offsetToCommand[binding.binaryOffset],
});
});
const resolvedCommands = commands.map((command): Command => {
if (command.argType === "COMMAND_OFFSET") {
return Object.assign({}, command, { arg: offsetToCommand[command.arg] });
const resolvedBindings = bindings.map(
(binding): Binding => {
return Object.assign({}, binding, {
commandOffset: offsetToCommand[binding.binaryOffset],
});
}
return command;
});
);
const resolvedCommands = commands.map(
(command): Command => {
if (command.argType === "COMMAND_OFFSET") {
return Object.assign({}, command, {
arg: offsetToCommand[command.arg],
});
}
return command;
}
);
return {
classes,
methods,
@ -150,7 +158,7 @@ function readMethods(makiFile: MakiFile, classes: string[]): Method[] {
const typeOffset = classCode & 0xff;
// This is probably the second half of a uint32
makiFile.readUInt16LE();
const name = makiFile.readString();
const name = makiFile.readString().toLowerCase();
const className = classes[typeOffset];
@ -238,7 +246,7 @@ function readConstants({ makiFile, variables }) {
}
}
function readBindings(makiFile) {
function readBindings(makiFile: MakiFile): Binding[] {
let count = makiFile.readUInt32LE();
const bindings = [];
while (count--) {

View file

@ -1,4 +1,4 @@
import GuiObj from "../skin/GuiObj";
import BaseObject from "../skin/BaseObject";
export type Variable =
| {
@ -15,7 +15,7 @@ export type Variable =
}
| {
type: "OBJECT";
value: GuiObj;
value: BaseObject;
}
| {
type: "NULL";

View file

@ -1,7 +1,5 @@
import { SkinContext } from "../types";
import { toBool } from "../utils";
import Group from "./Group";
import Layer from "./Layer";
import Layout from "./Layout";
import XmlObj from "./XmlObj";
@ -74,6 +72,9 @@ export default class Container extends XmlObj {
getDebugDom(): HTMLDivElement {
const div = window.document.createElement("div");
div.setAttribute("data-xml-id", this.getId());
div.setAttribute("data-obj-name", "Container");
if (this._defaultVisible && this._activeLayout) {
div.appendChild(this._activeLayout.getDebugDom());
}

View file

@ -76,6 +76,7 @@ export default class Group extends GuiObj {
getDebugDom(): HTMLDivElement {
const div = super.getDebugDom();
div.setAttribute("data-obj-name", "Group");
div.style.height = Utils.px(this._maximumHeight);
div.style.width = Utils.px(this._maximumWidth);
if (this._background != null && this._drawBackground) {

View file

@ -1,5 +1,6 @@
import { SkinContext } from "../types";
import * as Utils from "../utils";
import { VM } from "./VM";
import XmlObj from "./XmlObj";
// http://wiki.winamp.com/wiki/XML_GUI_Objects#GuiObject_.28Global_params.29
@ -112,8 +113,65 @@ export default class GuiObj extends XmlObj {
this._dirty = true;
}
/**
* Hookable. Event happens when the left mouse
* button was previously down and is now up.
*
* @param x The X position in the screen where the cursor was when the event was triggered.
* @param y The Y position in the screen where the cursor was when the event was triggered.
*/
onLeftButtonUp(x: number, y: number) {
VM.dispatch(this, "onleftbuttonup", [
{ type: "INT", value: x },
{ type: "INT", value: y },
]);
}
/**
* Hookable. Event happens when the left mouse button
* is pressed.
*
* @param x The X position in the screen where the cursor was when the event was triggered.
* @param y The Y position in the screen where the cursor was when the event was triggered.
*/
onLeftButtonDown(x: number, y: number) {
VM.dispatch(this, "onleftbuttondown", [
{ type: "INT", value: x },
{ type: "INT", value: y },
]);
}
/**
* Hookable. Event happens when the right mouse button
* was previously down and is now up.
*
* @param x The X position in the screen where the cursor was when the event was triggered.
* @param y The Y position in the screen where the cursor was when the event was triggered.
*/
onRightButtonUp(x: number, y: number) {
VM.dispatch(this, "onrightbuttonup", [
{ type: "INT", value: x },
{ type: "INT", value: y },
]);
}
/**
* Hookable. Event happens when the right mouse button
* is pressed.
*
* @param x The X position in the screen where the cursor was when the event was triggered.
* @param y The Y position in the screen where the cursor was when the event was triggered.
*/
onRightButtonDown(x: number, y: number) {
VM.dispatch(this, "onrightbuttondown", [
{ type: "INT", value: x },
{ type: "INT", value: y },
]);
}
getDebugDom(): HTMLDivElement {
const div = window.document.createElement("div");
div.setAttribute("data-id", this.getId());
div.style.display = this._visible ? "inline-block" : "none";
div.style.position = "absolute";
if (this._x) {
@ -128,6 +186,13 @@ export default class GuiObj extends XmlObj {
if (this._height) {
div.style.height = Utils.px(this._height);
}
div.addEventListener("mouseup", (e) => {
this.onLeftButtonUp(e.clientX, e.clientX);
});
div.addEventListener("mousedown", (e) => {
this.onLeftButtonDown(e.clientX, e.clientX);
});
return div;
}
}

View file

@ -22,6 +22,7 @@ export default class Layer extends GuiObj {
getDebugDom(): HTMLDivElement {
const div = super.getDebugDom();
div.setAttribute("data-obj-name", "Layer");
if (this._image != null) {
const bitmap = UI_ROOT.getBitmap(this._image);
div.style.background = bitmap.getBackgrondCSSAttribute();

View file

@ -1,7 +1,6 @@
import Group from "./Group";
import * as Utils from "../utils";
import Container from "./Container";
import Layer from "./Layer";
// > A layout is a special kind of group, which shown inside a container. Each
// > layout represents an appearance for that window. Layouts give you the ability
@ -31,4 +30,10 @@ export default class Layout extends Group {
setParent(container: Container) {
this._parent = container;
}
getDebugDom(): HTMLDivElement {
const div = super.getDebugDom();
div.setAttribute("data-obj-name", "Layout");
return div;
}
}

View file

@ -1,24 +1,19 @@
import { interpret } from "../maki/interpreter";
import { getClass } from "../maki/objects";
import { ParsedMaki } from "../maki/parser";
import { SkinContext } from "../types";
import Button from "./Button";
import BaseObject from "./BaseObject";
import Container from "./Container";
import Group from "./Group";
import Layer from "./Layer";
import Layout from "./Layout";
import PopupMenu from "./PopupMenu";
import Status from "./Status";
import Text from "./Text";
import ToggleButton from "./ToggleButton";
import { VM } from "./VM";
export default class SystemObject {
export default class SystemObject extends BaseObject {
_parentGroup: Group;
_parsedScript: ParsedMaki;
_context: SkinContext;
constructor(parsedScript: ParsedMaki) {
super();
this._parsedScript = parsedScript;
}
@ -30,15 +25,9 @@ export default class SystemObject {
throw new Error("First variable was not SystemObject.");
}
initialVariable.value = this;
// TODO: How should we setup bindings?
// console.log(this._parsedScript.bindings);
interpret(0, this._parsedScript, classResover);
interpret(
this._parsedScript.bindings[0].commandOffset,
this._parsedScript,
classResover
);
VM.addScript(this._parsedScript);
VM.dispatch(this, "onscriptloaded");
}
setParentGroup(group: Group) {
@ -166,33 +155,6 @@ export default class SystemObject {
}
}
function classResover(guid: string): any {
switch (guid) {
case "d6f50f6449b793fa66baf193983eaeef":
return SystemObject;
case "e90dc47b4ae7840d0b042cb0fcf775d2":
return Container;
case "60906d4e482e537e94cc04b072568861":
return Layout;
case "5ab9fa1545579a7d5765c8aba97cc6a6":
return Layer;
case "f4787af44ef7b2bb4be7fb9c8da8bea9":
return PopupMenu;
case "698eddcd4fec8f1e44f9129b45ff09f9":
return Button;
case "b4dccfff4bcc81fe0f721b96ff0fbed5":
return ToggleButton;
case "efaa867241fa310ea985dcb74bcb5b52":
return Text;
case "0f08c9404b23af39c4b8f38059bb7e8f":
return Status;
default:
throw new Error(
`Unresolvable class "${getClass(guid).name}" (guid: ${guid})`
);
}
}
function dumpScriptDebug(script: ParsedMaki) {
for (const [i, binding] of script.bindings.entries()) {
const method = script.methods[binding.methodOffset];

View file

@ -0,0 +1,37 @@
import { interpret } from "../maki/interpreter";
import { ParsedMaki } from "../maki/parser";
import { Variable } from "../maki/v";
import BaseObject from "./BaseObject";
import { classResolver } from "./resolver";
class Vm {
_scripts: ParsedMaki[] = [];
// This could easily become performance sensitive. We could make this more
// performant by normalizing some of these things when scripts are added.
dispatch(object: BaseObject, event: string, args: Variable[] = []) {
for (const [scriptId, script] of this._scripts.entries()) {
for (const binding of script.bindings) {
if (
script.methods[binding.methodOffset].name === event &&
script.variables[binding.variableOffset].value === object
) {
this.interpret(scriptId, binding.commandOffset, args);
}
}
}
}
addScript(maki: ParsedMaki): number {
const index = this._scripts.length;
this._scripts.push(maki);
return index;
}
interpret(scriptId: number, commandOffset: number, args: Variable[]) {
const script = this._scripts[scriptId];
interpret(commandOffset, script, args, classResolver);
}
}
export const VM = new Vm();

View file

@ -1,4 +1,6 @@
export default class XmlObj {
import BaseObject from "./BaseObject";
export default class XmlObj extends BaseObject {
setXmlAttributes(attributes: { [attrName: string]: string }) {
for (const [key, value] of Object.entries(attributes)) {
this.setXmlAttr(key, value);

View file

@ -55,6 +55,8 @@ export default class SkinParser {
switch (node.name.toLowerCase()) {
case "wasabixml":
return this.wasabiXml(node);
case "winampabstractionlayer":
return this.winampAbstractionLayer(node);
case "include":
return this.include(node);
case "skininfo":
@ -63,6 +65,8 @@ export default class SkinParser {
return this.elements(node);
case "bitmap":
return this.bitmap(node);
case "bitmapfont":
return this.bitmapFont(node);
case "color":
return this.color(node);
case "groupdef":
@ -130,6 +134,10 @@ export default class SkinParser {
await this.traverseChildren(node);
}
async winampAbstractionLayer(node: XmlElement) {
await this.traverseChildren(node);
}
async elements(node: XmlElement) {
await this.traverseChildren(node);
}
@ -153,6 +161,13 @@ export default class SkinParser {
UI_ROOT.addBitmap(bitmap);
}
async bitmapFont(node: XmlElement) {
assume(
node.children.length === 0,
"Unexpected children in <bitmapFont> XML node."
);
}
async text(node: XmlElement) {
assume(
node.children.length === 0,
@ -416,7 +431,10 @@ export default class SkinParser {
const path = [...this._path, fileName].join("/");
const zipFile = this.getCaseInsensitiveFile(path);
assert(zipFile != null, `Zip file not found for ${file}`);
if (zipFile == null) {
console.warn(`Zip file not found: ${file}`);
return;
}
const includedXml = await zipFile.async("string");
// Note: Included files don't have a single root node, so we add a synthetic one.

View file

@ -0,0 +1,39 @@
import { getClass } from "../maki/objects";
import Button from "./Button";
import SystemObject from "./SystemObject";
import Container from "./Container";
import Layout from "./Layout";
import Layer from "./Layer";
import PopupMenu from "./PopupMenu";
import ToggleButton from "./ToggleButton";
import Status from "./Status";
import Text from "./Text";
// TODO: We could write a test using the data in object.ts which confirms that
// this is complete.
export function classResolver(guid: string): any {
switch (guid) {
case "d6f50f6449b793fa66baf193983eaeef":
return SystemObject;
case "e90dc47b4ae7840d0b042cb0fcf775d2":
return Container;
case "60906d4e482e537e94cc04b072568861":
return Layout;
case "5ab9fa1545579a7d5765c8aba97cc6a6":
return Layer;
case "f4787af44ef7b2bb4be7fb9c8da8bea9":
return PopupMenu;
case "698eddcd4fec8f1e44f9129b45ff09f9":
return Button;
case "b4dccfff4bcc81fe0f721b96ff0fbed5":
return ToggleButton;
case "efaa867241fa310ea985dcb74bcb5b52":
return Text;
case "0f08c9404b23af39c4b8f38059bb7e8f":
return Status;
default:
throw new Error(
`Unresolvable class "${getClass(guid).name}" (guid: ${guid})`
);
}
}