Get corneramp initializing without any error messages

This commit is contained in:
Jordan Eldredge 2021-06-15 00:16:48 -07:00
parent 1e86945f1e
commit 0e159c3bed
18 changed files with 567 additions and 33 deletions

View file

@ -1,5 +1,6 @@
# TODO Next
- [ ] Fix all `// FIXME`
- [ ] SystemObject.getruntimeversion
- [ ] SystemObject.getskinname
- [ ] Ensure only wrapped variables go on the stack

View file

@ -11,12 +11,24 @@ async function main() {
await parser.parse();
for (const container of parser._containers) {
container.init();
container.init({ containers: parser._containers });
}
let node = document.createElement("div");
for (const container of parser._containers) {
document.body.appendChild(container.getDebugDom());
node.appendChild(container.getDebugDom());
}
document.body.appendChild(node);
setInterval(() => {
document.body.removeChild(node);
node = document.createElement("div");
for (const container of parser._containers) {
node.appendChild(container.getDebugDom());
}
document.body.appendChild(node);
}, 1000);
}
main();

View file

@ -18,6 +18,7 @@ class Interpreter {
variables: Variable[];
methods: Method[];
commands: Command[];
debug: boolean;
classResolver: (guid: string) => any;
constructor(program: ParsedMaki, classResolver: (guid: string) => any) {
const { commands, methods, variables, classes } = program;
@ -29,6 +30,7 @@ class Interpreter {
this.stack = [];
this.callStack = [];
this.debug = false;
}
interpret(start: number) {
@ -36,6 +38,9 @@ class Interpreter {
let ip = start;
while (ip < this.commands.length) {
const command = this.commands[ip];
if (this.debug) {
console.log(command);
}
switch (command.opcode) {
// push
@ -70,7 +75,7 @@ class Interpreter {
a.type == b.type,
`Tried to compare a ${a.type} to a ${b.type}.`
);
const result = V.newInt(a.value === b.value);
const result = V.newInt(b.value === a.value);
this.stack.push(result);
break;
}
@ -82,7 +87,7 @@ class Interpreter {
a.type == b.type,
`Tried to compare a ${a.type} to a ${b.type}.`
);
const result = V.newInt(a.value !== b.value);
const result = V.newInt(b.value !== a.value);
this.stack.push(result);
break;
}
@ -104,7 +109,10 @@ class Interpreter {
case "NULL":
throw new Error("Tried to add non-numbers.");
}
this.stack.push(V.newInt(a.value > b.value));
if (this.debug) {
console.log(`${b.value} > ${a.value}`);
}
this.stack.push(V.newInt(b.value > a.value));
break;
}
// >=
@ -130,7 +138,11 @@ class Interpreter {
case "NULL":
throw new Error("Tried to add non-numbers.");
}
this.stack.push(V.newInt(a.value < b.value));
if (this.debug) {
console.log(`${b.value} < ${a.value}`);
}
this.stack.push(V.newInt(b.value < a.value));
break;
}
// <=
@ -184,7 +196,7 @@ class Interpreter {
// actually having access to the object instance.
if (!klass.prototype[methodName]) {
throw new Error(
`Need to add missing method "${methodName}" to ${klass.name}`
`Need to add missing method: ${klass.name}.${methodName}: ${returnType}`
);
}
let argCount = klass.prototype[methodName].length;
@ -200,13 +212,18 @@ class Interpreter {
"Tried to call a method on a primitive."
);
let value = obj.value[methodName](...methodArgs);
if (typeof value.then === "function") {
throw new Error("Did not expect maki method to return promise");
if (value === undefined && returnType !== "NULL") {
throw new Error(
`Did not expect ${klass.name}.${methodName}: ${returnType} to return undefined`
);
}
if (value === null) {
// variables[1] holds global NULL value
value = this.variables[1];
}
if (this.debug) {
console.log(`Calling method ${methodName}`);
}
this.stack.push({ type: returnType, value } as any);
break;
}
@ -234,7 +251,6 @@ class Interpreter {
case 48: {
const a = this.stack.pop();
const b = this.stack.pop();
assume(
a.type === b.type,
`Type mismatch: ${a.type} != ${b.type} at ip: ${ip}`
@ -319,7 +335,7 @@ class Interpreter {
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value + b.value });
this.stack.push({ type: a.type, value: b.value + a.value });
break;
}
// - (subtract)
@ -341,7 +357,7 @@ class Interpreter {
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value - b.value });
this.stack.push({ type: a.type, value: b.value - a.value });
break;
}
// * (multiply)
@ -363,7 +379,7 @@ class Interpreter {
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value * b.value });
this.stack.push({ type: a.type, value: b.value * a.value });
break;
}
// / (divide)
@ -384,7 +400,7 @@ class Interpreter {
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value / b.value });
this.stack.push({ type: a.type, value: b.value / a.value });
break;
}
// % (mod)
@ -451,7 +467,28 @@ class Interpreter {
}
// logAnd (&&)
case 80: {
assume(false, "Unimplimented && operator");
const a = this.stack.pop();
const b = this.stack.pop();
// Some of these are probably valid, but we'll enable them once we see usage.
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
if (b.value && a.value) {
this.stack.push(a);
} else {
this.stack.push(b);
}
break;
}
// logOr ||
@ -473,10 +510,12 @@ class Interpreter {
case "NULL":
throw new Error("Tried to add non-numbers.");
}
if (a.value) {
return a;
if (b.value) {
this.stack.push(b);
} else {
this.stack.push(a);
}
return b;
break;
}
// <<
case 88: {
@ -494,7 +533,7 @@ class Interpreter {
const guid = this.classes[classesOffset];
const Klass = this.classResolver(guid);
const klassInst = new Klass();
this.stack.push(klassInst);
this.stack.push({ type: "OBJECT", value: klassInst });
break;
}
// delete

View file

@ -26,6 +26,7 @@ export type ParsedMaki = {
export type Binding = {
commandOffset: number;
methodOffset: number;
};
const MAGIC = "FG";

View file

@ -0,0 +1,21 @@
/**
Object Class.
@short This is the base class from which all other classes inherit.
@author Nullsoft Inc.
@ver 1.0
*/
export default class BaseObject {
/**
* Returns the class name for the object.
*
* @ret The class name.
*/
getClassName(): string {
throw new Error("Unimplemented");
}
getId() {
throw new Error("Unimplemented");
}
}

View file

@ -0,0 +1,26 @@
import GuiObj from "./GuiObj";
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E
export default class Button extends GuiObj {
setXmlAttr(key: string, value: string): boolean {
if (super.setXmlAttr(key, value)) {
return true;
}
switch (key) {
default:
return false;
}
return true;
}
/*
extern Button.onActivate(int activated);
extern Button.onLeftClick();
extern Button.onRightClick();
extern Button.setActivated(Boolean onoff);
extern Button.setActivatedNoCallback(Boolean onoff);
extern Boolean Button.getActivated();
extern Button.leftClick();
extern Button.rightClick();e
*/
}

View file

@ -1,5 +1,7 @@
import { SkinContext } from "../types";
import { toBool } from "../utils";
import Group from "./Group";
import Layer from "./Layer";
import Layout from "./Layout";
import XmlObj from "./XmlObj";
@ -13,6 +15,7 @@ export default class Container extends XmlObj {
_layouts: Layout[] = [];
_activeLayout: Layout | null = null;
_defaultVisible: boolean = true;
_id: string;
constructor() {
super();
}
@ -22,6 +25,9 @@ export default class Container extends XmlObj {
return true;
}
switch (key) {
case "id":
this._id = value.toLowerCase();
break;
case "default_visible":
this._defaultVisible = toBool(value);
break;
@ -31,12 +37,35 @@ export default class Container extends XmlObj {
return true;
}
init() {
init(context: SkinContext) {
for (const layout of this._layouts) {
layout.init();
layout.init(context);
}
}
getId() {
return this._id;
}
/* Required for Maki */
/**
* Get the layout associated with the an id.
* This corresponds to the "id=..." attribute in
* the XML tag <layout .. />.
*
* @ret The layout associated with the id.
* @param layout_id The id of the layout you wish to retrieve.
*/
getlayout(layoutId: string): Layout {
const lower = layoutId.toLowerCase();
for (const layout of this._layouts) {
if (layout.getId() === lower) {
return layout;
}
}
throw new Error(`Could not find a container with the id; "${layoutId}"`);
}
addLayout(layout: Layout) {
layout.setParent(this);
this._layouts.push(layout);

View file

@ -2,6 +2,7 @@ import * as Utils from "../utils";
import UI_ROOT from "../UIRoot";
import GuiObj from "./GuiObj";
import SystemObject from "./SystemObject";
import { SkinContext } from "../types";
export default class Group extends GuiObj {
_background: string;
@ -43,12 +44,12 @@ export default class Group extends GuiObj {
return true;
}
init() {
init(context: SkinContext) {
for (const systemObject of this._systemObjects) {
systemObject.init();
systemObject.init(context);
}
for (const child of this._children) {
child.init();
child.init(context);
}
}
@ -61,6 +62,18 @@ export default class Group extends GuiObj {
this._children.push(child);
}
/* Required for Maki */
getobject(objectId: string): GuiObj {
const lower = objectId.toLowerCase();
for (const obj of this._children) {
if (obj.getId() === lower) {
return obj;
}
}
throw new Error(`Could not find an object with the id; "${objectId}"`);
}
getDebugDom(): HTMLDivElement {
const div = super.getDebugDom();
div.style.height = Utils.px(this._maximumHeight);

View file

@ -1,3 +1,4 @@
import { SkinContext } from "../types";
import * as Utils from "../utils";
import XmlObj from "./XmlObj";
@ -10,11 +11,12 @@ export default class GuiObj extends XmlObj {
_y: number = 0;
_droptarget: string;
_visible: boolean = true;
_dirty: boolean = false;
setXmlAttr(key: string, value: string): boolean {
switch (key) {
case "id":
this._id = value;
this._id = value.toLowerCase();
break;
case "w":
this._width = Utils.num(value);
@ -40,10 +42,76 @@ export default class GuiObj extends XmlObj {
return true;
}
init() {
init(context: SkinContext) {
// pass
}
getId(): string {
return this._id;
}
/**
* Trigger the show event.
*/
show() {
this._visible = true;
this._dirty = true;
}
/**
* Trigger the hide event.
*/
hide() {
this._visible = false;
this._dirty = true;
}
/**
* Get the Y position, in the screen, of the
* top edge of the object.
*
* @ret The top edge's position (in screen coordinates).
*/
gettop(): number {
return this._x;
}
/**
* Get the height of the object, in pixels.
*
* @ret The height of the object.
*/
getheight() {
// FIXME
return this._height || 100;
}
/**
* Get the width of the object, in pixels.
*
* @ret The width of the object.
*/
getwidth() {
// FIXME
return this._width || 100;
}
/**
* Resize the object to the desired size and position.
*
* @param x The X position where to anchor the object before resize.
* @param y The Y position where to anchor the object before resize.
* @param w The width you wish the object to have.
* @param h The height you wish the object to have.
*/
resize(x: number, y: number, w: number, h: number) {
this._x = x;
this._y = y;
this._width = w;
this._height = h;
this._dirty = true;
}
getDebugDom(): HTMLDivElement {
const div = window.document.createElement("div");
div.style.display = this._visible ? "inline-block" : "none";

View file

@ -0,0 +1,19 @@
import BaseObject from "./BaseObject";
export default class PopupMenu extends BaseObject {
addcommand(
cmdText: string,
cmd_id: number,
checked: boolean,
disabled: boolean
) {}
addseparator() {}
/*
extern PopupMenu.addSubMenu(PopupMenu submenu, String submenutext);
extern Int PopupMenu.popAtXY(int x, int y);
extern Int PopupMenu.popAtMouse();
extern Int PopupMenu.getNumCommands();
extern PopupMenu.checkCommand(int cmd_id, boolean check);
extern PopupMenu.disableCommand(int cmd_id, boolean disable);
*/
}

View file

@ -0,0 +1,25 @@
import GuiObj from "./GuiObj";
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cslider.2F.3E_.26_.3CWasabi:HSlider.2F.3E_.26_.3CWasabi:VSlider.2F.3E
export default class Slider extends GuiObj {
setXmlAttr(key: string, value: string): boolean {
if (super.setXmlAttr(key, value)) {
return true;
}
switch (key) {
default:
return false;
}
return true;
}
/*
extern Slider.onSetPosition(int newpos);
extern Slider.onPostedPosition(int newpos);
extern Slider.onSetFinalPosition(int pos);
extern Slider.setPosition(int pos);
extern Int Slider.getPosition();
extern Slider.lock(); // locks descendant core collbacks
extern Slider.unlock(); // unloads the
*/
}

View file

@ -0,0 +1,16 @@
import GuiObj from "./GuiObj";
// Maybe this?
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3CWasabi:StandardFrame:Status.2F.3E
export default class Status extends GuiObj {
setXmlAttr(key: string, value: string): boolean {
if (super.setXmlAttr(key, value)) {
return true;
}
switch (key) {
default:
return false;
}
return true;
}
}

View file

@ -1,18 +1,30 @@
import { interpret } from "../maki/interpreter";
import { getClass } from "../maki/objects";
import { ParsedMaki } from "../maki/parser";
import { SkinContext } from "../types";
import Button from "./Button";
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";
export default class SystemObject {
_parentGroup: Group;
_parsedScript: ParsedMaki;
_context: SkinContext;
constructor(parsedScript: ParsedMaki) {
this._parsedScript = parsedScript;
}
init() {
init(context: SkinContext) {
this._context = context;
// dumpScriptDebug(this._parsedScript);
const initialVariable = this._parsedScript.variables[0];
if (initialVariable.type !== "OBJECT") {
throw new Error("First variable was not SystemObject.");
@ -21,6 +33,12 @@ export default class SystemObject {
// 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
);
}
setParentGroup(group: Group) {
@ -35,16 +53,153 @@ export default class SystemObject {
getskinname() {
return "TODO: Get the Real skin name";
}
/**
* Read a private config entry of Integer type. Returns
* the specified default value if the section and item isn't
* found.
*
* @ret The value of the config entry.
* @param section The section from which to read the entry.
* @param item The name of the item to read.
* @param defvalue The defautl value to return if no item is found.
*/
getprivateint(section: string, item: string, defvalue: number) {
// TODO: Implement this!
// FIXME
return defvalue;
}
/**
* Create a private config entry for your script, of Int type.
*
* @param section The section for the entry.
* @param item The item name for the entry.
* @param value The value of the entry.
*/
setprivateint(section: string, item: string, value: number) {
// FIXME
}
/**
* Read the current time of the day. Returns a number that's
* the number of milliseconds since the start of the day (0:00).
*
* @ret The number of milliseconds since midnight.
**/
gettimeofday(): number {
const date = new Date();
const dateTime = date.getTime();
const dateCopy = new Date(dateTime);
return dateTime - dateCopy.setHours(0, 0, 0, 0);
}
/**
* @ret The requested container.
* @param container_id The containers identifier string.
**/
getcontainer(containerId: string): Container {
const lower = containerId.toLowerCase();
for (const container of this._context.containers) {
if (container.getId() === lower) {
return container;
}
}
throw new Error(`Could not find a container with the id; "${containerId}"`);
}
/**
* Get the value of the left vu meter.
* Range is from 0 to 255. Linear.
*
* @ret The value of the left vu meter.
*/
getleftvumeter(): number {
return 0;
}
/**
* Get the value of the right vu meter.
* Range is from 0 to 255. Linear.
*
* @ret The value of the right vu meter.
*/
getrightvumeter(): number {
return 0;
}
/**
* Get the current volume. Range is from 0 to 255.
*
* @ret The current volume.
**/
getvolume() {
return 100;
}
/**
* Get the string representation of an integer.
*
* @ret The string equivalent of the integer.
* @param value The integer to change into a string.
*/
integertostring(value: number): string {
return String(value);
}
/**
* Get the user's screen width in pixels.
*
* @ret The width of the user's screen.
*/
getviewportwidth() {
return window.document.documentElement.clientWidth;
}
/**
* Get the user's screen height in pixels.
*
* @ret The height of the user's screen.
*/
getviewportheight() {
return window.document.documentElement.clientHeight;
}
}
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:
console.log();
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];
const guid = script.classes[method.typeOffset];
const klass = getClass(guid);
console.log(
`${i}. ${klass.name}.${method.name} => ${binding.commandOffset}`
);
}
}

View file

@ -0,0 +1,24 @@
import GuiObj from "./GuiObj";
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Ctext.2F.3E_.26_.3CWasabi:Text.2F.3E
export default class Text extends GuiObj {
setXmlAttr(key: string, value: string): boolean {
if (super.setXmlAttr(key, value)) {
return true;
}
switch (key) {
default:
return false;
}
return true;
}
/*
extern Text.setText(String txt); // changes the display/text="something" param
extern Text.setAlternateText(String txt); // overrides the display/text parameter with a custom string, set "" to cancel
extern String Text.getText();
extern int Text.getTextWidth();
extern Text.onTextChanged(String newtxt);
*/
}

View file

@ -0,0 +1,20 @@
import Button from "./Button";
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E
export default class ToggleButton extends Button {
setXmlAttr(key: string, value: string): boolean {
if (super.setXmlAttr(key, value)) {
return true;
}
switch (key) {
default:
return false;
}
return true;
}
/*
extern ToggleButton.onToggle(Boolean onoff);
extern int TOggleButton.getCurCfgVal()
*/
}

View file

@ -8,4 +8,8 @@ export default class XmlObj {
setXmlAttr(_key: string, _value: string): boolean {
return false;
}
setxmlparam(key: string, value: string) {
this.setXmlAttr(key, value);
}
}

View file

@ -8,8 +8,13 @@ import Layout from "./Layout";
import Group from "./Group";
import Container from "./Container";
import Layer from "./Layer";
import Slider from "./Slider";
import Button from "./Button";
import Text from "./Text";
import Status from "./Status";
import { parse as parseMaki } from "../maki/parser";
import SystemObject from "./SystemObject";
import ToggleButton from "./ToggleButton";
class ParserContext {
container: Container | null = null;
@ -154,7 +159,16 @@ export default class SkinParser {
"Unexpected children in <text> XML node."
);
// TODO: Parse text
const text = new Text();
text.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
if (parentGroup == null) {
console.warn(
`FIXME: Expected <Text id="${text._id}"> to be within a <Layout> | <Group>`
);
return;
}
parentGroup.addChild(text);
}
async script(node: XmlElement) {
@ -202,7 +216,16 @@ export default class SkinParser {
"Unexpected children in <button> XML node."
);
// TODO: Parse buttons
const button = new Button();
button.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
if (parentGroup == null) {
console.warn(
`FIXME: Expected <Button id="${button._id}"> to be within a <Layout> | <Group>`
);
return;
}
parentGroup.addChild(button);
}
async wasabiButton(node: XmlElement) {
@ -220,7 +243,16 @@ export default class SkinParser {
"Unexpected children in <button> XML node."
);
// TODO: Parse buttons
const button = new ToggleButton();
button.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
if (parentGroup == null) {
console.warn(
`FIXME: Expected <ToggleButton id="${button._id}"> to be within a <Layout> | <Group>`
);
return;
}
parentGroup.addChild(button);
}
async color(node: XmlElement) {
@ -238,7 +270,16 @@ export default class SkinParser {
"Unexpected children in <slider> XML node."
);
// TODO: Parse slider
const slider = new Slider();
slider.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
if (parentGroup == null) {
console.warn(
`FIXME: Expected <Slider id="${slider._id}"> to be within a <Layout> | <Group>`
);
return;
}
parentGroup.addChild(slider);
}
async groupdef(node: XmlElement) {
@ -246,6 +287,11 @@ export default class SkinParser {
}
async layer(node: XmlElement) {
assume(
node.children.length === 0,
"Unexpected children in <layer> XML node."
);
const layer = new Layer();
layer.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
@ -256,7 +302,6 @@ export default class SkinParser {
return;
}
parentGroup.addChild(layer);
await this.traverseChildren(node);
}
async layout(node: XmlElement) {
@ -318,6 +363,17 @@ export default class SkinParser {
node.children.length === 0,
"Unexpected children in <status> XML node."
);
const status = new Status();
status.setXmlAttributes(node.attributes);
const { parentGroup } = this._context;
if (parentGroup == null) {
console.warn(
`FIXME: Expected <Status id="${status._id}"> to be within a <Layout> | <Group>`
);
return;
}
parentGroup.addChild(status);
}
async eqvis(node: XmlElement) {
assume(

View file

@ -0,0 +1,5 @@
import Container from "./skin/Container";
export type SkinContext = {
containers: Container[];
};