mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
Set the 2nd modern iteration as only one on (modern) track. (#1166)
* yarn extract-object-types (run again) * preparing to load Application.mi * avoid error about using function ins strict mode. turn this to "function" syntax to see the error/warn * embed TrueTypeFont in one CSS * add api: getAttribute * reaching zero erro at devtool Console * set color correctly. * move compiler to new project * take some yarn-scripts from sibling (modern-1) * reconfigure objectData target path * remove duplicated folder * discontinuing the 1st iteration captbaritone: it's probably time to del modern & ren modern-2: modern * set the 2nd iteration as only one on track. captbaritone: it's probably time to del modern & ren modern-2: modern * bugfix test not working: path unavailable * implement TODO * reduce warning at import jzip * solving Deploy: failed * solving deploy failed error: Unknown workspace "webamp-modern * bugfix premateur call of this._font_id * bugfix font-family : ''; prettier. Co-authored-by: Fathony <fathony@smart-leaders.net>
This commit is contained in:
parent
69122b7c89
commit
10555e093f
228 changed files with 421 additions and 30603 deletions
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
packages/webamp/demo/built/
|
||||
packages/webamp/built/
|
||||
packages/webamp-modern-2/src/build/
|
||||
packages/webamp-modern-2/tools/eslint-rules/dist/
|
||||
packages/webamp-modern/src/build/
|
||||
packages/webamp-modern/tools/eslint-rules/dist/
|
||||
|
|
@ -2,5 +2,5 @@
|
|||
yarn workspace ani-cursor build
|
||||
yarn workspace webamp build
|
||||
yarn workspace webamp build-library
|
||||
yarn workspace webamp-modern-2 build
|
||||
mv packages/webamp-modern-2/src/build packages/webamp/demo/built/modern
|
||||
yarn workspace webamp-modern build
|
||||
mv packages/webamp-modern/src/build packages/webamp/demo/built/modern
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint . --ext ts,tsx,js,jsx --rulesdir=packages/webamp-modern-2/tools/eslint-rules",
|
||||
"lint": "eslint . --ext ts,tsx,js,jsx --rulesdir=packages/webamp-modern/tools/eslint-rules",
|
||||
"deploy": "sh deploy.sh",
|
||||
"format": "prettier --write '**/*.{js,ts,tsx}'"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
build/
|
||||
1
packages/webamp-modern-2/.gitignore
vendored
1
packages/webamp-modern-2/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
build/
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
## Running locally
|
||||
|
||||
Assuming you have [Yarn](https://yarnpkg.com/) installed:
|
||||
|
||||
```bash
|
||||
cd packages/webamp-modern-2
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- [ ] We could use WebGL to try to improve the speed of switching gamma colors
|
||||
- [ ] We could use some CSS techniques to avoid having to appply inline style to each BitmapFont character's DOM node.
|
||||
- [ ] We should profile the parse phase to see what's taking time. Perhaps there's some sync image work that could be done lazily.
|
||||
- [ ] Remove some paranoid validation in the VM.
|
||||
- [ ] Consider throttling time updates coming from audio
|
||||
|
||||
# TODO Next
|
||||
|
||||
- [ ] Why doesn't scrolling work property in MMD3?
|
||||
- [ ] Implement proper color
|
||||
- [ ] Move gammacolor to GPU
|
||||
- [ ] Requires VM
|
||||
- [ ] Look at componentbucket (Where can I find the images)
|
||||
- [ ] How is the scroll window for colors supposed to work?
|
||||
- [ ] How is the position slider supposed to work?
|
||||
- [ ] Standardize handling of different type condition permutations in interpreter
|
||||
- [ ] Implement EQ
|
||||
- [ ] Implament global actions
|
||||
- [ ] TOGGLE
|
||||
- [ ] MINIMIZE
|
||||
- [ ] Allow for skins which don't have gamma sets
|
||||
- [ ] Figure out if global NULL is actually typed as INT in Maki. I suspect there is no NULL type, but only an INT who happens to be zero.
|
||||
- [ ] Fix all `// FIXME`
|
||||
- [ ] SystemObject.getruntimeversion
|
||||
- [ ] SystemObject.getskinname
|
||||
- [ ] Handle clicking through transparent: https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
|
||||
|
||||
# TODO Some day
|
||||
|
||||
- [ ] Handle case (in)sensitivity of includes.
|
||||
- [ ] Handle forward/backward slashes issues (if they exist)
|
||||
|
||||
## Known Bugs
|
||||
|
||||
- [ ] In GuiObj's handling of left click, it's possible for the y/x of the click event to fall outside of the element being clicked. To repro click just above the volume2 of MMD3. Y can be one pixel above the clientBoundingRect of the element. Why?
|
||||
|
||||
# Phases of Initialization
|
||||
|
||||
## Asset Parse
|
||||
|
||||
Starting with `skin.xml`, and inlining each `<include />` we parse XML. As we go, we initialize GUI objects and attach them to their parent. During this phase we also encounter other asset files like Maki script, images, and fonts. These are parsed as they are encountered and setaside into a look-aside table (Maki scripts might live in the tree...).
|
||||
|
||||
This phase is `async` since it may require reading files from zip or doing image/font manipulation which is inherently `async`.
|
||||
|
||||
## Object Initialization
|
||||
|
||||
Once all look-aside tables are populated, we notify all GUI objects to initialize themselves by propogating from the root of the tree to the leaves. Each node is reponsible for notifying its children. In this phase components pull images/scripts/fonts out of their look-aside tables. [Question: Could these just be lazy?]. At this point we also hook up any event bindings/hooks that exist in Maki.
|
||||
|
||||
## Maki Initialization
|
||||
|
||||
Once all nodes have been initialized, we trigger/dispatch `System.onScriptLoaded` for each Maki script.
|
||||
|
||||
## First paint
|
||||
|
||||
Now we can begin panting
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
const std = require("./std.json");
|
||||
|
||||
const NAME_TO_DEF = {};
|
||||
|
||||
Object.values(std).forEach((value) => {
|
||||
NAME_TO_DEF[value.name] = value;
|
||||
});
|
||||
|
||||
function getMethod(className, methodName) {
|
||||
return NAME_TO_DEF[className].functions.find(({ name }) => {
|
||||
return name === methodName;
|
||||
});
|
||||
}
|
||||
|
||||
// Between myself and the author of the decompiler, a number of manual tweaks
|
||||
// have been made to our current object definitions. This function recreates
|
||||
// those tweaks so we can have an apples to apples comparison.
|
||||
|
||||
/*
|
||||
* From object.js
|
||||
*
|
||||
* > The std.mi has this set as void, but we checked in Winamp and confirmed it
|
||||
* > returns 0/1
|
||||
*/
|
||||
getMethod("Timer", "isRunning").result = "boolean";
|
||||
|
||||
/*
|
||||
* From Object.pm
|
||||
*
|
||||
* > note, std.mi does not have this parameter!
|
||||
*/
|
||||
getMethod("ToggleButton", "onToggle").parameters[0][1] = "onoff";
|
||||
|
||||
// Some methods are not compatible with the type signature of their parent class
|
||||
getMethod("GuiTree", "onChar").parameters[0][0] = "string";
|
||||
getMethod("GuiList", "onSetVisible").parameters[0][0] = "boolean";
|
||||
|
||||
// I'm not sure how to get these to match
|
||||
getMethod("Wac", "onNotify").parameters = getMethod(
|
||||
"Object",
|
||||
"onNotify"
|
||||
).parameters;
|
||||
getMethod("Wac", "onNotify").result = "int";
|
||||
|
||||
/*
|
||||
|
||||
Here's the error we get without that patch:
|
||||
|
||||
__generated__/makiInterfaces.ts:254:18 - error TS2430: Interface 'Wac' incorrectly extends interface 'MakiObject'.
|
||||
Types of property 'onnotify' are incompatible.
|
||||
Type '(notifstr: string, a: number, b: number) => void' is not assignable to type '(command: string, param: string, a: number, b: number) => number'.
|
||||
Types of parameters 'a' and 'param' are incompatible.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
|
||||
254 export interface Wac extends MakiObject {
|
||||
~~~
|
||||
|
||||
|
||||
Found 1 error.
|
||||
|
||||
*/
|
||||
|
||||
module.exports = std;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"name": "webamp-modern-2",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "snowpack dev",
|
||||
"build": "snowpack build",
|
||||
"lint": "yarn build-lint && eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"test": "yarn jest",
|
||||
"build-lint": "tsup tools/eslint-rules/proper-maki-types.ts -d tools/eslint-rules/dist --no-splitting --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^7.2.14",
|
||||
"@types/estree": "^0.0.50",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.0",
|
||||
"snowpack": "^3.5.5",
|
||||
"tsup": "^4.12.5",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"prettier": {},
|
||||
"dependencies": {
|
||||
"@rgrove/parse-xml": "^3.0.0",
|
||||
"jszip": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
{
|
||||
"593DBA22D0774976B952F4713655400B": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getItem",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getItemByGuid",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_guid"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "newItem",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_name"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"item_guid"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "Config"
|
||||
},
|
||||
"D40302823AAB4d87878D12326FADFCD5": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "ConfigAttribute",
|
||||
"name": "getAttribute",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigAttribute",
|
||||
"name": "newAttribute",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"default_value"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getGuid",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getName",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "ConfigItem"
|
||||
},
|
||||
"24DEC283B76E4a368CCC9E24C46B6C73": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "",
|
||||
"name": "setData",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"value"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getData",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "onDataChanged",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getParentItem",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getAttributeName",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "ConfigAttribute"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
{
|
||||
"345BEEBC0229492190BE6CB6A49A79D9": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumTracks",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getCurrentIndex",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumSelectedTracks",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNextSelectedTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"i"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showCurrentlyPlayingTrack",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "enqueueFile",
|
||||
"parameters": [
|
||||
[
|
||||
"string",
|
||||
"file"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "clear",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "removeTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "swapTracks",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item1"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"item2"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveUp",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveDown",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveTo",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"pos"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "playTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getRating",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "setRating",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"rating"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getTitle",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getLength",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getMetaData",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"metadatastring"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getFileName",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "onPleditModified",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "PlEdit"
|
||||
},
|
||||
"61A7ABAD7D7941f6B1D0E1808603A4F4": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumItems",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getItemName",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showCurrentlyPlayingEntry",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "refresh",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "renameItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "enqueueItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "playItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "PlDir"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,160 +0,0 @@
|
|||
import JSZip, { JSZipObject } from "jszip";
|
||||
|
||||
export function assert(condition: boolean, message: string) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
// While developing I want to clarify some assumptions. These are things which
|
||||
// don't need to break the world, but I would like to know if/when my
|
||||
// assumptions are invalidated.
|
||||
// In the future these can be turned into warnings.
|
||||
export function assume(condition: boolean, message: string) {
|
||||
if (!condition) {
|
||||
console.warn(message);
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
export function getCaseInsensitiveFile(
|
||||
zip: JSZip,
|
||||
filePath: string
|
||||
): JSZipObject | null {
|
||||
const normalized = filePath.replace(/[\/\\]/g, `[/\\\\]`);
|
||||
return zip.file(new RegExp(normalized, "i"))[0] ?? null;
|
||||
}
|
||||
|
||||
export function num(str: string | void): number | null {
|
||||
return str == null ? null : Number(str);
|
||||
}
|
||||
|
||||
export function px(size: number): string {
|
||||
return `${size}px`;
|
||||
}
|
||||
|
||||
export function relative(size: number): string {
|
||||
if (size === 0) return "100%";
|
||||
return `calc(100% + ${size}px)`;
|
||||
}
|
||||
|
||||
export function toBool(str: string) {
|
||||
assert(
|
||||
str === "0" || str === "1",
|
||||
`Expected bool value to be "0" or "1", but it was "${str}".`
|
||||
);
|
||||
return str === "1";
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
export function getId(): number {
|
||||
return id++;
|
||||
}
|
||||
|
||||
// TODO: Delete this once we have proper type coersion in the VM.
|
||||
export function ensureVmInt(num: number): number {
|
||||
return Math.floor(num);
|
||||
}
|
||||
|
||||
export function clamp(num: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(num, max));
|
||||
}
|
||||
|
||||
export function normalizeDomId(id: string) {
|
||||
return id.replace(/[^a-zA-Z0-9]/g, "-");
|
||||
}
|
||||
|
||||
export function removeAllChildNodes(parent: Element) {
|
||||
while (parent.firstChild) {
|
||||
parent.removeChild(parent.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
export function integerToTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = String(Math.round(seconds % 60)).padStart(2, "0");
|
||||
return `${mins}:${secs}`;
|
||||
}
|
||||
|
||||
export function findLast<T>(
|
||||
arr: T[],
|
||||
predicate: (value: T) => boolean
|
||||
): T | undefined {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
const value = arr[i];
|
||||
if (predicate(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getUrlQuery(location: Location, variable: string): string {
|
||||
return new URL(location.href).searchParams.get(variable);
|
||||
}
|
||||
|
||||
export const throttle = (fn: Function, wait: number = 300) => {
|
||||
let inThrottle: boolean,
|
||||
lastFn: ReturnType<typeof setTimeout>,
|
||||
lastTime: number;
|
||||
return function (this: any) {
|
||||
const context = this,
|
||||
args = arguments;
|
||||
if (!inThrottle) {
|
||||
fn.apply(context, args);
|
||||
lastTime = Date.now();
|
||||
inThrottle = true;
|
||||
} else {
|
||||
clearTimeout(lastFn);
|
||||
lastFn = setTimeout(() => {
|
||||
if (Date.now() - lastTime >= wait) {
|
||||
fn.apply(context, args);
|
||||
lastTime = Date.now();
|
||||
}
|
||||
}, Math.max(wait - (Date.now() - lastTime), 0));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Purpuse: to hold eventListeners
|
||||
*/
|
||||
export class Emitter {
|
||||
_cbs: { [event: string]: Array<Function> } = {};
|
||||
|
||||
// call this to register a callback to a specific event
|
||||
on(event: string, cb: Function) {
|
||||
if (this._cbs[event] == null) {
|
||||
this._cbs[event] = [];
|
||||
}
|
||||
this._cbs[event].push(cb);
|
||||
|
||||
// return a function for later unregistering
|
||||
return () => {
|
||||
//TODO: consider using this.off(), or integrate both
|
||||
this._cbs[event] = this._cbs[event].filter((c) => c !== cb);
|
||||
};
|
||||
}
|
||||
|
||||
// remove an registered callback from a specific event
|
||||
off(event: string, cb: Function) {
|
||||
if (this._cbs[event] == null) {
|
||||
return;
|
||||
}
|
||||
const cbs = this._cbs[event];
|
||||
const index = cbs.indexOf(cb, 0);
|
||||
if (index > -1) {
|
||||
cbs.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// call this to run registered callbacks of an event
|
||||
trigger(event: string, ...args: any[]) {
|
||||
const subscriptions = this._cbs[event];
|
||||
if (subscriptions == null) {
|
||||
return;
|
||||
}
|
||||
for (const cb of subscriptions) {
|
||||
cb(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { parseFile } from "./parse-mi";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const compilers = path.join(__dirname, "../../../resources/maki_compiler/");
|
||||
|
||||
const lib566 = path.join(compilers, "v1.2.0 (Winamp 5.66)/lib/");
|
||||
|
||||
const files = {
|
||||
pldir: path.join(lib566, "pldir.mi"),
|
||||
config: path.join(lib566, "config.mi"),
|
||||
std: path.join(lib566, "std.mi"),
|
||||
};
|
||||
|
||||
Object.keys(files).forEach((name) => {
|
||||
const sourcePath = files[name];
|
||||
const types = parseFile(sourcePath);
|
||||
const destinationPath = path.join(__dirname, `../objectData/${name}.json`);
|
||||
|
||||
fs.writeFileSync(destinationPath, JSON.stringify(types, null, 2));
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": "2",
|
||||
"targets": {
|
||||
"browsers": [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 ChromeAndroid versions",
|
||||
"last 2 Safari versions",
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Edge versions",
|
||||
"last 2 iOS versions",
|
||||
"last 2 Opera versions"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-modules-commonjs",
|
||||
"@babel/plugin-syntax-dynamic-import"
|
||||
]
|
||||
},
|
||||
"library": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
},
|
||||
"production": {
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1 @@
|
|||
*.min.js
|
||||
built/
|
||||
coverage/
|
||||
**/node_modules/
|
||||
examples/webpack/bundle.js
|
||||
pacakges/tweetBot/env/
|
||||
build/
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"jsx": true,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"prettier",
|
||||
"import",
|
||||
"@typescript-eslint",
|
||||
"react-hooks"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.2"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"amd": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
// TODO: Consider removing some of these.
|
||||
// https://github.com/facebook/create-react-app/pull/1840
|
||||
// Create React App and "Standard" only allow the following:
|
||||
// * document
|
||||
// * window
|
||||
// * console
|
||||
// * navigator
|
||||
"globals": {
|
||||
"window": true,
|
||||
"document": true,
|
||||
"console": true,
|
||||
"navigator": true,
|
||||
"alert": true,
|
||||
"Blob": true,
|
||||
"fetch": true,
|
||||
"FileReader": true,
|
||||
"Element": true,
|
||||
"AudioNode": true,
|
||||
"MutationObserver": true,
|
||||
"Image": true,
|
||||
"location": true
|
||||
},
|
||||
"rules": {
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 2,
|
||||
"maxEOF": 0,
|
||||
"maxBOF": 0
|
||||
}
|
||||
],
|
||||
"block-scoped-var": "warn",
|
||||
"constructor-super": "error",
|
||||
"dot-notation": "error",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"guard-for-in": "error",
|
||||
"lines-between-class-members": [
|
||||
"warn",
|
||||
"always",
|
||||
{ "exceptAfterSingleLine": true }
|
||||
],
|
||||
"max-depth": ["warn", 4],
|
||||
"max-params": ["warn", 5],
|
||||
"new-cap": "error",
|
||||
"no-caller": "error",
|
||||
"no-catch-shadow": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-div-regex": "warn",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-eval": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extend-native": "warn",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-extra-semi": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-inner-declarations": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-label-var": "error",
|
||||
"no-labels": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-multi-spaces": "warn",
|
||||
"no-multi-str": "error",
|
||||
"no-native-reassign": "error",
|
||||
"no-negated-in-lhs": "warn",
|
||||
"no-nested-ternary": "warn",
|
||||
"no-new-object": "error",
|
||||
"no-new-symbol": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-proto": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "warn",
|
||||
"no-spaced-func": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "ignoreRestSiblings": true }
|
||||
],
|
||||
"no-useless-rename": "error",
|
||||
"no-var": "error",
|
||||
"no-with": "error",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "warn",
|
||||
"radix": "error",
|
||||
"no-return-await": "error",
|
||||
"react/no-string-refs": "error",
|
||||
"react/jsx-boolean-value": "error",
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"react/prefer-es6-class": "error",
|
||||
"react/jsx-pascal-case": "error",
|
||||
"react/require-render-return": "error",
|
||||
"react/self-closing-comp": "error",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/first": "error",
|
||||
"import/named": "error",
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/no-extraneous-dependencies": "error",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const rulesDirPlugin = require("eslint-plugin-rulesdir");
|
||||
rulesDirPlugin.RULES_DIR = "packages/webamp-modern-2/tools/eslint-rules/dist";
|
||||
rulesDirPlugin.RULES_DIR = "packages/webamp-modern/tools/eslint-rules/dist";
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
7
packages/webamp-modern/.gitignore
vendored
7
packages/webamp-modern/.gitignore
vendored
|
|
@ -1,6 +1 @@
|
|||
**/node_modules
|
||||
|
||||
/built
|
||||
/coverage
|
||||
/examples/webpack/bundle.js
|
||||
**/__diff_output__/
|
||||
build/
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package.json
|
||||
**/*.min.css
|
||||
**/base-skin.css
|
||||
built/*
|
||||
coverage/*
|
||||
examples/webpack/bundle.js
|
||||
|
|
@ -1,46 +1,67 @@
|
|||
# Modern Winamp Skins
|
||||
## Running locally
|
||||
|
||||
This package is an experiment to see if we can bring "modern" Winamp skins to the browser. It's still very early. If you have any qeustions or are interested in getting involved, feel free to check out our [Discord server](https://discord.gg/mEcRbVq).
|
||||
Assuming you have [Yarn](https://yarnpkg.com/) installed:
|
||||
|
||||
- We have a master task here which is sometimes up to date: https://github.com/captbaritone/webamp/issues/796
|
||||
- We have a shared Dropbox Paper doc with contains a grab bag of notes, but it will likely be hard to newcomers to read. You can find it [here](https://paper.dropbox.com/doc/Webamp-Modern-Skins-Notes--AgWp4Jwdobq13VLYYOgwJGOCAQ-UpeDNptmJ0t6aN1jlWbfU).
|
||||
- We have a document with [meeting notes](https://paper.dropbox.com/doc/Meeting-Notes-lPgIliU4ZThefBT3J8g2a) from the few in-person meetings that Jordan Eldredge and Jordan Berg have had to discuss the project.
|
||||
|
||||
## Maki Interpreter
|
||||
|
||||
One of the biggest challenges to this project is that modern skins could define their own behavior by writing scripts in a custom language called Maki (Make a Killer Interface). One of the critical pieces of this project will be to write a working Maki interpreter and runtime in browser-compatible JavaScript. We have made good progress on this front. The work on that project lives in `src/maki-interpreter` and has its own [readme](./src/maki-interpreter/readme.md).
|
||||
|
||||
One goal of this project is to document what we learn about the Maki language so that if others wish to travel down this path they will have an open source reference implementation and also better docs than we had. We've started that effort with out [Maki Language Spec](https://paper.dropbox.com/doc/Maki-Language-Spec--AlIjyyR70bQuNFJD7rIeuFfiAg-csainvAwSr3SBUXO5DWXy) document. Once the document stabalizes, we will likely convert it to Markdown and check it into this repository.
|
||||
|
||||
Another way to document the behavior of Maki is to write automated tests in the form of Maki scripts. We have few of these so far, but intend to be more systematic about writing tests in the future. For now our tests can be found in `resources/maki_compiler/*/*.maki` and `src/maki-interpreter/fixtures/issue_*/*.maki`.
|
||||
|
||||
## Standard Library
|
||||
|
||||
In addition to the Maki interpreter, we also need an implementation of the Maki standard library. We have some portion of that implmented, but it's still very much a work in progress. You can find the code in `src/runtime/`. The definition for how these classes and methods should behave is derived from looking at the types defined in the `std.mi` file distributed with the Maki compiler. We have a file that contains a JSON representation of these types. It can be found in `src/objects.js`. That file is used for a few runtime checks (which I hope we can remove) but also to power some static analysis and tests.
|
||||
|
||||
`src/objects.test.js` does some tests to double check that every method no prefixed with `_` or `js_` is a maki method. It also tracks which methods are still unimplemented in a Jest snapshot file.
|
||||
|
||||
`eslint-local-rules.js` contains a custom [ESLint](https://eslint.org/) rule which uses the type definitions to check many of the same things that the Jest test checks, but also can make assertions about argument names and TypeScript types. This approach also has the advantage that it can automatically correct some errors and generate stubs for missing methods.
|
||||
|
||||
We also have a tool for examining a corpus of modern skins an extracting which methods of the standard libary they use. This lives in `src/maki-interpreter/tools/extract-functions.js` it's not really built for anyone but Jordan to run, so it has a few paths hard coded into it. This could be fixed if somebody else had the interest. By running `yarn analyze-wals` it will look in a specific hard-coded folder for skins and extract method data from them. It will then write that data to `resources/maki-skin-data.json`. This data is invaluable for prioritizing which methods we should implement next. Some methods are only used by a very small number of skins. Others are not used at all.
|
||||
|
||||
The data extracted by the `extract-functions.js` utility and the list of unimplmented methods which is validated by `src/objects.test.js` can be visualized visiting [https://webamp.org/ready/](https://webamp.org/ready/) or `localhost:8080/ready` if developing locally. This dashboard makes it very easy to see current progress and explore the usage of different methods. Keep in mind that as of this writting some ~20% of `.maki` files fail to parse, so the data on this page represents a lower bound of actual usage.
|
||||
|
||||
## Architecture
|
||||
|
||||
How we tie together our standard library implementation, the interpreter and the DOM (React) is still not a solved problem. We have stuff working, but we are not sure it's the right approach. We are currently doing work to move toward what we are calling [Mutable XML Tree](https://paper.dropbox.com/doc/A-Third-Way-Mutable-XML-Tree-vx3iPfGIBmSHEDJSMh0bn) architecture but, to be honest, we are not 100% this will actually work.
|
||||
|
||||
## Running
|
||||
|
||||
This experiment is now built as part of the main Webamp project. To run it locally you just need to do the following from the repositories root directory:
|
||||
|
||||
```
|
||||
```bash
|
||||
cd packages/webamp-modern-2
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then open: `http://localhost:8080/`.
|
||||
## Performance Improvements
|
||||
|
||||
## Tests
|
||||
- [ ] We could use WebGL to try to improve the speed of switching gamma colors
|
||||
- [ ] We could use some CSS techniques to avoid having to appply inline style to each BitmapFont character's DOM node.
|
||||
- [ ] We should profile the parse phase to see what's taking time. Perhaps there's some sync image work that could be done lazily.
|
||||
- [ ] Remove some paranoid validation in the VM.
|
||||
- [ ] Consider throttling time updates coming from audio
|
||||
|
||||
This experiments tests are run as part of the main Webamp test suite. To run all tests just run: `yart test`.
|
||||
# TODO Next
|
||||
|
||||
- [ ] Why doesn't scrolling work property in MMD3?
|
||||
- [ ] Implement proper color
|
||||
- [ ] Move gammacolor to GPU
|
||||
- [ ] Requires VM
|
||||
- [ ] Look at componentbucket (Where can I find the images)
|
||||
- [ ] How is the scroll window for colors supposed to work?
|
||||
- [ ] How is the position slider supposed to work?
|
||||
- [ ] Standardize handling of different type condition permutations in interpreter
|
||||
- [ ] Implement EQ
|
||||
- [ ] Implament global actions
|
||||
- [ ] TOGGLE
|
||||
- [ ] MINIMIZE
|
||||
- [ ] Allow for skins which don't have gamma sets
|
||||
- [ ] Figure out if global NULL is actually typed as INT in Maki. I suspect there is no NULL type, but only an INT who happens to be zero.
|
||||
- [ ] Fix all `// FIXME`
|
||||
- [ ] SystemObject.getruntimeversion
|
||||
- [ ] SystemObject.getskinname
|
||||
- [ ] Handle clicking through transparent: https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
|
||||
|
||||
# TODO Some day
|
||||
|
||||
- [ ] Handle case (in)sensitivity of includes.
|
||||
- [ ] Handle forward/backward slashes issues (if they exist)
|
||||
|
||||
## Known Bugs
|
||||
|
||||
- [ ] In GuiObj's handling of left click, it's possible for the y/x of the click event to fall outside of the element being clicked. To repro click just above the volume2 of MMD3. Y can be one pixel above the clientBoundingRect of the element. Why?
|
||||
|
||||
# Phases of Initialization
|
||||
|
||||
## Asset Parse
|
||||
|
||||
Starting with `skin.xml`, and inlining each `<include />` we parse XML. As we go, we initialize GUI objects and attach them to their parent. During this phase we also encounter other asset files like Maki script, images, and fonts. These are parsed as they are encountered and setaside into a look-aside table (Maki scripts might live in the tree...).
|
||||
|
||||
This phase is `async` since it may require reading files from zip or doing image/font manipulation which is inherently `async`.
|
||||
|
||||
## Object Initialization
|
||||
|
||||
Once all look-aside tables are populated, we notify all GUI objects to initialize themselves by propogating from the root of the tree to the leaves. Each node is reponsible for notifying its children. In this phase components pull images/scripts/fonts out of their look-aside tables. [Question: Could these just be lazy?]. At this point we also hook up any event bindings/hooks that exist in Maki.
|
||||
|
||||
## Maki Initialization
|
||||
|
||||
Once all nodes have been initialized, we trigger/dispatch `System.onScriptLoaded` for each Maki script.
|
||||
|
||||
## First paint
|
||||
|
||||
Now we can begin panting
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
|
@ -1,18 +0,0 @@
|
|||
module.exports = {
|
||||
displayName: "test",
|
||||
rootDir: "../",
|
||||
testRegex: "\\.test\\.(js|ts|tsx)$",
|
||||
globals: {
|
||||
SENTRY_DSN: null,
|
||||
},
|
||||
moduleFileExtensions: ["js", "tsx", "ts"],
|
||||
moduleNameMapper: {
|
||||
// "\\.css$": "<rootDir>/js/__mocks__/styleMock.js",
|
||||
// "\\.wsz$": "<rootDir>/js/__mocks__/fileMock.js",
|
||||
// "\\.mp3$": "<rootDir>/js/__mocks__/fileMock.js",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.(js|ts|tsx)$": "babel-jest",
|
||||
},
|
||||
testPathIgnorePatterns: ["/node_modules/"],
|
||||
};
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".tsx"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{ loader: "css-loader", options: { importLoaders: 1 } },
|
||||
// We really only need this in prod. We could find a way to disable it in dev.
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(js|ts|tsx)?$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
envName: "production",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(wsz|wal|mp3|png|ico|jpg|svg)$/,
|
||||
use: [
|
||||
{
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
emitFile: true,
|
||||
name: "[path][name]-[hash].[ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
noParse: [/jszip\.js$/],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "index.html",
|
||||
template: "./index.html",
|
||||
chunks: ["modern"],
|
||||
}),
|
||||
// Ideally we could just do this via client-side routing, but it's tricky
|
||||
// with both the real app and this sub directory. So we just hack it to
|
||||
// duplicate the html file in both places and move on with our lives.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "./ready/index.html",
|
||||
template: "./index.html",
|
||||
chunks: ["modern"],
|
||||
}),
|
||||
],
|
||||
|
||||
performance: {
|
||||
// We do some crazy shit okay! Don't judge!
|
||||
maxEntrypointSize: 7000000,
|
||||
maxAssetSize: 7000000,
|
||||
},
|
||||
entry: {
|
||||
modern: ["./src/index.js"],
|
||||
},
|
||||
context: path.resolve(__dirname, "../"),
|
||||
output: {
|
||||
filename: "[name]-[hash].js",
|
||||
chunkFilename: "[name]-[hash].js",
|
||||
publicPath: "/",
|
||||
path: path.resolve(__dirname, "../built"),
|
||||
},
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const merge = require("webpack-merge");
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: "source-map",
|
||||
mode: "development",
|
||||
devServer: {
|
||||
overlay: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const merge = require("webpack-merge");
|
||||
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
const config = merge(common, {
|
||||
devtool: "source-map",
|
||||
mode: "production",
|
||||
});
|
||||
|
||||
module.exports = config;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Webamp · Modern Skin Rendering Experiment [PRE-ALPHA]</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
# MAKI
|
||||
|
||||
In the process of starting to build support for "modern" Winamp skins for Webamp, we built an interpreter for MAKI (Make A Killer Interface) byte code, the scripting language used for adding custom functionality to modern Winamp skins.
|
||||
|
||||
Most of what we learned came from the Ralf Engels' [Maki Decompiler](http://www.rengels.de/maki_decompiler/) (which seems to be offline at the moment) and trial and error testing of what the compiler output.
|
||||
|
||||
My goal here is to document the structure and semantics of the compiled `.maki` file that the MAKI compiler outputs and Winamp interprerates.
|
||||
|
||||
## Notation
|
||||
|
||||
- Terminal symbols are written in **bold**
|
||||
- Nonterminal symbols are written in _italics_
|
||||
- Productions are written _sym_ ::= A B C
|
||||
- Vectors are written as vec(_A_) where _A_ is the production being enumerated.
|
||||
|
||||
### _u16_
|
||||
|
||||
TODO: Clarify encoding.
|
||||
|
||||
Encoded as little endien.
|
||||
|
||||
### _u32_
|
||||
|
||||
TODO: Clarify encoding.
|
||||
|
||||
Encoded as little endien.
|
||||
|
||||
### _Vector_
|
||||
|
||||
_Vectors_ are encoded with their _u32_ length followed by the encoding of their element sequence.
|
||||
|
||||
### _String_
|
||||
|
||||
A string is encoded as a _u16_ indicating the length of the string n followed by n bytes of ASCII.
|
||||
|
||||
## Module Sections
|
||||
|
||||
_module_ ::= _header_ _classes_ _methods_ _variables_ _constants_ _bindings_ _codes_
|
||||
|
||||
A module (file) consists of the above seven consecutive sections.
|
||||
|
||||
## Header
|
||||
|
||||
_header_ ::= _magic_ _moduleVersion_ _extraVersion_
|
||||
|
||||
_magic_ ::= **`0x46`** **`0x47`**
|
||||
|
||||
_moduleVersion_ ::= _u16_
|
||||
_extraVersion_ ::= _u32_
|
||||
|
||||
The _magic_ that beings the header consists of the binary representation of the ASCII characters "FG". I'm not sure what, if any, significance there is to this choice. These characters are simply here for the parser to validate that it's actually geting a `.maki` file.
|
||||
|
||||
_moduleVersion_ represents the version of the compiler with which this module was compiled.
|
||||
|
||||
_extraVersion_ is unused. I suspect that it may be additional version information, but I don't know for sure.
|
||||
|
||||
## Classes
|
||||
|
||||
_classes_ ::= vec(_identifier_)
|
||||
_identifier_ ::= _u32_ _u32_ _u32_ _u32_
|
||||
|
||||
The _identifier_ represents the unique ID found in the `std.mi` against which the module was compiled. The hex string can be computed by interpreting each _u32_ as a hex string (left padded to 8 characters) and then concatenating them.
|
||||
|
||||
Note: The ASCII representation we construct above may not directly match that found in `std.mi` due to the [oddities of UUID encoding](https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding). We use the following function to map from our representation to the one fond in `std.mi`:
|
||||
|
||||
```JavaScript
|
||||
function getFormattedId(id) {
|
||||
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
|
||||
const formattedId = id.replace(
|
||||
/(........)(....)(....)(..)(..)(..)(..)(..)(..)(..)(..)/,
|
||||
"$1$3$2$7$6$5$4$11$10$9$8"
|
||||
);
|
||||
return formattedId.toLowerCase();
|
||||
}
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
_methods_ ::= vec(_method_)
|
||||
_method_ ::= _classCode_ _extraClassCode_ _string_
|
||||
_classCode_ ::= _u16_
|
||||
_extraClassCode_ ::= _u16_
|
||||
|
||||
We use `classCode & 0xff` to derive the "class offset" and then discard/ignore `_extraClassCode_. It's possible that this is actually intended to be parsed as a single _u32_ and we just did something wrong.
|
||||
|
||||
The "class offset" is an offset into the _classes_ vector and indicates to which class this method belongs.
|
||||
|
||||
The final _string_ is the name of this method.
|
||||
|
||||
## Variables
|
||||
|
||||
_variables_ ::= vec(_variable_)
|
||||
_variable_ ::= _typeOffset_ _object_ _subclass_ _uint1_ _uint2_ _uint3_ _uint4_ _global_ _system_
|
||||
_typeOffset_ ::= **byte**
|
||||
_object_ ::= **byte**
|
||||
_subclass_ ::= _u16_
|
||||
_uint1_ ::= _u16_
|
||||
_uint2_ ::= _u16_
|
||||
_uint3_ ::= _u16_
|
||||
_uint4_ ::= _u16_
|
||||
_global_ ::= **byte**
|
||||
_system_ ::= **byte**
|
||||
|
||||
Each variable declared here is an entry in our variables table. There are three different types of variables: subclasses, objects, and primitives.
|
||||
|
||||
Our interpretation of _variables_ is a bit squirrely but does seem to work according to our tests. Our method of determining which type we have seems suspect and there are number of bytes which go completely ignored.
|
||||
|
||||
### Subclass
|
||||
|
||||
If _subclass_ is truthy, this variable is a subclass and _typeOffset_ represents an earlier offset into _variables_ which is this variables superclass.
|
||||
|
||||
_global_ indicates if this variable is global.
|
||||
|
||||
In the subclass case all other values are ignored.
|
||||
|
||||
### Object
|
||||
|
||||
If _object_ is truthy, this variable is an object and _typeOffset_ is an offset into the _classes_ vector which is this variables type.
|
||||
|
||||
_global_ indicates if this variable is global.
|
||||
|
||||
In the object case all other values are ignored.
|
||||
|
||||
### Primitives
|
||||
|
||||
If a variable is neithe a subclass nor an object, it is a primitive. _typeOffset_ determines which type of primitive it is. There are at least five types of primitives:
|
||||
|
||||
- Int (2)
|
||||
- Float (3)
|
||||
- Double (4)
|
||||
- Boolean (5)
|
||||
- String (6)
|
||||
|
||||
The fact that index 0 and 1 are not used is confusing, but in our testing we did not discover any `.maki` files that referenced them.
|
||||
|
||||
If _typeOffset_ is **2** this variable is an Int and its value can be found in _uint1_.
|
||||
|
||||
If _typeOffset_ is **3** this variable is a Float. We use the following function to derive the floating value from _uint1_ and _uint2_:
|
||||
|
||||
```JavaScript
|
||||
function decodeFloat(uint1, unint2) {
|
||||
const exponent = (uint2 & 0xff80) >> 7;
|
||||
const mantisse = ((0x80 | (uint2 & 0x7f)) << 16) | uint1;
|
||||
return mantisse * 2.0 ** (exponent - 0x96);
|
||||
}
|
||||
```
|
||||
|
||||
If _typeOffset_ is **4** this variable is a Double. We currently derive the value exactly the same as we do for floats. This is likely wrong. Probably we should also be looking at _uint3_ and _uint4_.
|
||||
|
||||
If _typeOffset_ is **5** this variable is a Boolean and its value can be found in _uint1_.
|
||||
|
||||
If _typeOffset_ is **6** this variable is a String. The actual string will be found in the _constants_ section which will reference this variable via its offset in the _variables_ vector.
|
||||
|
||||
## Constants
|
||||
|
||||
_constants_ ::= vec(_constant_)
|
||||
_constant_ ::= _variableOffset_ _string_
|
||||
_variableOffset_ ::= _u32_
|
||||
|
||||
String constants are encoded in this section. _variableOffset_ is the offset into the _variables_ vector that is a string and has this string as its initial value.
|
||||
|
||||
_string_ is the actual value.
|
||||
|
||||
## Bindings
|
||||
|
||||
_bindings_ ::= vec(_binding_)
|
||||
_binding_ ::= _variableOffset_ _methodOffset_ _binaryOffset_
|
||||
_methodOffset_ ::= _u32_
|
||||
_binaryOffset_ ::= _u32_
|
||||
|
||||
MAKI allows you to define behavior for events on an instance of an object. These are represented as _bindings_ in the byte code.
|
||||
|
||||
_variableOffset_ is an offset into the _variables_ vector. It indicates the instance to which this binding is attached.
|
||||
|
||||
_methodOffset_ is an offset into the _methods_ vector and indicates the method on object instance that will be called.
|
||||
|
||||
_binaryOffset_ is the byte offset into the _codes_ section where the opcodes for this method begin.
|
||||
|
||||
## Code
|
||||
|
||||
_codes_ ::= _codeLength_ _code_
|
||||
_codeLength_ ::= _u32_
|
||||
|
||||
_codeLength_ indicates the byte size of _code_.
|
||||
|
||||
_code_ is made up of a stream of opcodes, some of which include an "immediate" values. For the opcodes that do include an immediate, the immediate is encoded as a _u32_. See the Opcodes table below for a list of opcodes and their behavior.
|
||||
|
||||
There are a few small quirks to reading _code_ which I _beleive_ have to do with stack protection, but I honestly don't understand them.
|
||||
|
||||
After we parse each opcode, we must peek at the next _u32_. If it's `>= 0xffff0000` and `<= 0xffff000f`, then we must consume that _u32_ and throw it away.
|
||||
|
||||
After parsing opcode `112` we must consume and throw away a single **byte**. I don't know why or what that byte might contain.
|
||||
|
||||
## Opcodes
|
||||
|
||||
Opcodes may or may not have an associated immediate value. If it does, there are a number of different types:
|
||||
|
||||
- var: Offset into the _variables_ vector
|
||||
- inst: Relative instruction pointer (ip) value
|
||||
- method: Offset into the _methods_ vector
|
||||
- class: Offset into the _classes_ vector
|
||||
|
||||
| Opcode | Name | Immediate | Behavior |
|
||||
| ------ | ----------- | --------- | ------------------------------------------------------------------------------ |
|
||||
| 1 | push | var | Pushes variable onto the stack |
|
||||
| 2 | pop | | Pops a variable off the stack and discards it |
|
||||
| 3 | popTo | var | Pops a variable off the stack stores it into var |
|
||||
| 8 | eq | | Pops a and b. Leaves a == b on the stack |
|
||||
| 9 | neq | | Pops a and b. Leaves a != b on the stack |
|
||||
| 10 | gt | | Pops a and b. Leaves a > b on the stack |
|
||||
| 11 | gte | | Pops a and b. Leaves a >= b on the stack |
|
||||
| 12 | lt | | Pops a and b. Leaves a < b on the stack |
|
||||
| 13 | lte | | Pops a and b. Leaves a <= b on the stack |
|
||||
| 16 | jumpIf | inst | Pops a value and jumps to inst if it is _false_ |
|
||||
| 17 | jumpIfNot | inst | Pops a value and jumps to inst if it is _true_ |
|
||||
| 18 | jump | inst | Jumps to inst |
|
||||
| 24 | call | method | Pops off a value for each arg and one for the object |
|
||||
| 112 | strangeCall | method | Same as above? Not sure why this is different. |
|
||||
| 25 | callGlobal | inst | Jump to inst and execute until return, leaves returned value on the stack |
|
||||
| 33 | return | | Pops value off stack and returns it to caller |
|
||||
| 40 | complete | | ? |
|
||||
| 48 | mov | | Pops a and b, and assigns variable b the value of a. Leaves value on the stack |
|
||||
| 56 | postinc | | Pops a variable, increments it, and pushes original value back on the stack |
|
||||
| 57 | postdec | | Pops a variable, decrements it, and pushes original value back on the stack |
|
||||
| 58 | preinc | | Pops a variable, increments it, and pushes new value back on the stack |
|
||||
| 59 | postinc | | Pops a variable, decrements it, and pushes new value back on the stack |
|
||||
| 64 | add | | Pops a and b. Leaves a + b on the stack |
|
||||
| 65 | sub | | Pops a and b. Leaves a - b on the stack |
|
||||
| 66 | mul | | Pops a and b. Leaves a \* b on the stack |
|
||||
| 67 | div | | Pops a and b. Leaves a / b on the stack |
|
||||
| 68 | mod | | Pops a and b. Leaves a % b on the stack |
|
||||
| 72 | band | | Pops a and b. Leaves a & b on the stack |
|
||||
| 73 | bor | | Pops a and b. Leaves a \| b on the stack |
|
||||
| 74 | not | | Pops a. Leaves !a |
|
||||
| 76 | neg | | Pops a. Leaves -a |
|
||||
| 80 | land | | Pops a and b. Leaves a && b |
|
||||
| 81 | lor | | Pops a and b. Leaves a \|\| b |
|
||||
| 88 | shiftl | | Pops a and b. Leaves a << b |
|
||||
| 89 | shiftr | | Pops a and b. Leaves a >> b |
|
||||
| 96 | new | class | Constructs a new instance of class and leaves it on the stack |
|
||||
| 97 | delete | | Pops variable off the stack and deletes it |
|
||||
|
||||
## TODO
|
||||
|
||||
- Document int8 production rule
|
||||
- Document u16 production rule
|
||||
- Document u32 production rule
|
||||
- Can we get a sample of what _extraVersion_ contains?
|
||||
- What are the four 32s of an _identifier_?
|
||||
- Could _extraClassCode_ actually be the second half of _classCode_?
|
||||
- What happens if we try to encode an int larger than uint16? Do we spill over into uint2?
|
||||
- What happens if we try to encode a negative int?
|
||||
- What happens if we try to encode a high precision float? Does it spill over into uint3 or uint4?
|
||||
- Are we really limited to 255 classes/superclasses. Can we test these cases? It seems like _typeOffset_ is just a byte so we fail if we had more.
|
||||
- Do we know what _sytem_ means?
|
||||
- Can we express variables using explicit productions?
|
||||
- Does _subclass_ have an interesting value when it's true?
|
||||
- Does _object_ have an interesting value when it's true?
|
||||
- What is the byte the follows a strangecall?
|
||||
|
|
@ -1,130 +1,34 @@
|
|||
{
|
||||
"name": "webamp-modern",
|
||||
"version": "0.0.0",
|
||||
"description": "Winamp Modern Skins in the browser",
|
||||
"scripts": {
|
||||
"lint-fix": "eslint . --ext ts,tsx,js --fix",
|
||||
"lint": "eslint . --ext ts,tsx,js --rulesdir=eslint",
|
||||
"type-check": "tsc",
|
||||
"serve": "http-server ./built",
|
||||
"start": "webpack-dev-server --open --config=config/webpack.dev.js",
|
||||
"format": "prettier --write \"**/*.{js,ts,tsx,d.ts,css}\"",
|
||||
"analyze-wals": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/extract-functions.js > resources/maki-skin-data.json",
|
||||
"extract-object-types": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/extract-object-types.js",
|
||||
"extract-attributes": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/extract-attributes.js > resources/attribute-skin-data.json",
|
||||
"maki-interfaces": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/build-typescript-interfaces.js > __generated__/makiInterfaces.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/captbaritone/webamp.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Winamp",
|
||||
"HTML5",
|
||||
"audio",
|
||||
"web-audio-api"
|
||||
],
|
||||
"author": "Jordan Eldredge <jordan@jordaneldredge.com>",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/captbaritone/webamp/issues"
|
||||
"scripts": {
|
||||
"start": "snowpack dev",
|
||||
"build": "snowpack build",
|
||||
"serve": "http-server ./built",
|
||||
"lint": "yarn build-lint && eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"test": "yarn jest",
|
||||
"extract-object-types": "node tools/extract-object-types.js",
|
||||
"extract-attributes": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/extract-attributes.js > resources/attribute-skin-data.json",
|
||||
"maki-interfaces": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/build-typescript-interfaces.js > __generated__/makiInterfaces.ts",
|
||||
"analyze-wals": "babel-node --extensions=\".ts,.js,.tsx\" src/maki-interpreter/tools/extract-functions.js > resources/maki-skin-data.json",
|
||||
"build-lint": "tsup tools/eslint-rules/proper-maki-types.ts -d tools/eslint-rules/dist --no-splitting --minify"
|
||||
},
|
||||
"homepage": "https://github.com/captbaritone/webamp/",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/node": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||
"@babel/polyfill": "^7.7.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-react": "^7.7.0",
|
||||
"@babel/preset-typescript": "^7.7.2",
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@sentry/browser": "5.9.1",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/css-font-loading-module": "^0.0.2",
|
||||
"@types/fscreen": "^1.0.1",
|
||||
"@types/invariant": "^2.2.29",
|
||||
"@types/jszip": "^3.1.5",
|
||||
"@types/lodash": "^4.14.116",
|
||||
"@types/lodash-es": "^4.17.1",
|
||||
"@types/rc-slider": "^8.6.3",
|
||||
"@types/react": "^16.8.13",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-redux": "^7.1.1",
|
||||
"@types/webaudioapi": "^0.0.27",
|
||||
"@typescript-eslint/eslint-plugin": "^2.6.1",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^9.0.0-beta.3",
|
||||
"babel-loader": "^8.0.4",
|
||||
"butterchurn": "^2.6.7",
|
||||
"canvas-mock": "0.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"data-uri-to-buffer": "^2.0.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"git-revision-webpack-plugin": "^3.0.3",
|
||||
"glob": "^7.1.4",
|
||||
"html-webpack-inline-svg-plugin": "^1.2.4",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^0.11.1",
|
||||
"imagemin": "^6.1.0",
|
||||
"imagemin-optipng": "^6.0.0",
|
||||
"invariant": "^2.2.3",
|
||||
"jszip": "^3.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"milkdrop-preset-converter-aws": "^0.1.6",
|
||||
"music-metadata-browser": "^0.6.1",
|
||||
"postcss": "^8.2.10",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"puppeteer": "^1.15.0",
|
||||
"rc-slider": "^8.7.1",
|
||||
"react-redux": "^7.1.0",
|
||||
"react-test-renderer": "^16.8.1",
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^3.0.1",
|
||||
"screenfull": "^4.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"tinyqueue": "^1.2.3",
|
||||
"typescript": "^3.7.2",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.8.2",
|
||||
"webpack-merge": "^4.1.2",
|
||||
"winamp-eqf": "^1.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"globalSetup": "jest-environment-puppeteer/setup",
|
||||
"globalTeardown": "jest-environment-puppeteer/teardown",
|
||||
"projects": [
|
||||
"config/jest.*.js"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5"
|
||||
"@types/eslint": "^7.2.14",
|
||||
"@types/estree": "^0.0.50",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.0",
|
||||
"snowpack": "^3.5.5",
|
||||
"tsup": "^4.12.5",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"prettier": {},
|
||||
"dependencies": {
|
||||
"eslint-plugin-react-hooks": "^2.1.2",
|
||||
"fscreen": "^1.0.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-dropzone": "^10.1.7",
|
||||
"redux-sentry-middleware": "^0.1.3",
|
||||
"xml-js": "^1.6.11"
|
||||
"@rgrove/parse-xml": "^3.0.0",
|
||||
"jszip": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,121 +0,0 @@
|
|||
import {
|
||||
MakiTree,
|
||||
ModernAction,
|
||||
ModernStore,
|
||||
XmlTree,
|
||||
XmlNode,
|
||||
Thunk,
|
||||
} from "./types";
|
||||
import JSZip from "jszip";
|
||||
import * as Utils from "./utils";
|
||||
import initialize from "./initialize";
|
||||
import initializeStateTree from "./initializeStateTree";
|
||||
import { run } from "./maki-interpreter/virtualMachine";
|
||||
import System from "./runtime/System";
|
||||
import runtime from "./runtime";
|
||||
import MakiObject from "./runtime/MakiObject";
|
||||
import JsScript from "./runtime/JsScript";
|
||||
|
||||
export function setMakiTree(makiTree: MakiTree): ModernAction {
|
||||
return { type: "SET_MAKI_TREE", makiTree };
|
||||
}
|
||||
|
||||
export function setXmlTree(xmlTree: XmlTree): Thunk {
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
type: "SET_XML_TREE",
|
||||
xmlTree: await initializeStateTree(xmlTree),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function gotSkinUrl(skinUrl: string, store: ModernStore): Thunk {
|
||||
return async (dispatch) => {
|
||||
const resp = await fetch(skinUrl);
|
||||
dispatch(gotSkinBlob(await resp.blob(), store));
|
||||
};
|
||||
}
|
||||
|
||||
export function gotSkinBlob(blob: Blob, store: ModernStore): Thunk {
|
||||
return async (dispatch) => {
|
||||
dispatch(gotSkinZip(await JSZip.loadAsync(blob), store));
|
||||
};
|
||||
}
|
||||
|
||||
async function unloadSkin(makiTree: MakiTree): Promise<void> {
|
||||
await Utils.asyncTreeFlatMap(makiTree, async (node: MakiObject) => {
|
||||
if (node instanceof JsScript && node.system) {
|
||||
node.system.onscriptunloading();
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
export function gotSkinZip(zip: JSZip, store: ModernStore): Thunk {
|
||||
return async (dispatch) => {
|
||||
// unload current skin if one has been loaded
|
||||
if (store.getState().modernSkin.skinLoaded) {
|
||||
await unloadSkin(store.getState().modernSkin.makiTree);
|
||||
dispatch({ type: "SKIN_UNLOADED" });
|
||||
}
|
||||
|
||||
const skinXml = await Utils.readXml(zip, "skin.xml");
|
||||
if (skinXml == null) {
|
||||
throw new Error("Could not find skin.xml in skin");
|
||||
}
|
||||
const rawXmlTree = await Utils.inlineIncludes(skinXml, zip);
|
||||
const xmlTree = Utils.mapTreeBreadth(
|
||||
rawXmlTree,
|
||||
(node: XmlNode, parent: XmlNode) => {
|
||||
return { ...node, uid: Utils.getId(), parent };
|
||||
}
|
||||
);
|
||||
|
||||
dispatch(setXmlTree(xmlTree));
|
||||
|
||||
const makiTree: MakiObject = await initialize(zip, xmlTree);
|
||||
// Execute scripts
|
||||
await Utils.asyncTreeFlatMap(makiTree, async (node: MakiObject) => {
|
||||
if (!(node instanceof JsScript)) {
|
||||
return node;
|
||||
}
|
||||
const scriptPath = node.getScriptPath();
|
||||
if (scriptPath == null) {
|
||||
return node;
|
||||
}
|
||||
if (scriptPath.endsWith("standardframe.maki")) {
|
||||
// TODO: stop ignoring standardframe
|
||||
return node;
|
||||
}
|
||||
|
||||
// removes groupdefs from consideration (only run scripts when actually referenced by group)
|
||||
if (Utils.findParentNodeOfType(node, new Set(["groupdef"]))) {
|
||||
return node;
|
||||
}
|
||||
const scriptGroup = Utils.findParentNodeOfType(
|
||||
node,
|
||||
new Set(["group", "winampabstractionlayer", "wasabixml"])
|
||||
);
|
||||
node.system = new System(scriptGroup, store);
|
||||
const script = await Utils.readUint8array(zip, scriptPath);
|
||||
if (script == null) {
|
||||
console.warn(`Unable to find script at ${scriptPath}`);
|
||||
return node;
|
||||
}
|
||||
run({
|
||||
runtime,
|
||||
data: script,
|
||||
system: node.system,
|
||||
log: false,
|
||||
});
|
||||
return node;
|
||||
});
|
||||
|
||||
dispatch(setMakiTree(makiTree));
|
||||
};
|
||||
}
|
||||
|
||||
export function setVolume(volume: number): ModernAction {
|
||||
return { type: "SET_VOLUME", volume };
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#root {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import React, { Suspense } from "react";
|
||||
import "./App.css";
|
||||
import * as Actions from "./Actions";
|
||||
import * as Selectors from "./Selectors";
|
||||
// import simpleSkin from "../skins/simple.wal";
|
||||
import cornerSkin from "../skins/CornerAmp_Redux.wal";
|
||||
import { useDispatch, useSelector, useStore } from "react-redux";
|
||||
import DropTarget from "./components/DropTarget";
|
||||
import { Maki } from "./MakiRenderer";
|
||||
const Dashboard = React.lazy(() => import("./Dashboard"));
|
||||
|
||||
const skinUrls = [
|
||||
cornerSkin,
|
||||
"https://archive.org/cors/winampskin_MMD3/MMD3.wal",
|
||||
"https://archive.org/cors/winampskin_The_Official_Ford_Sync_Winamp5_Skin/The_Official_Ford_Sync_Winamp5_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Godsmack_Faceless/Godsmack_-_Faceless.wal",
|
||||
"https://archive.org/cors/winampskin_Tokyo_Drift/Tokyo_Drift.wal",
|
||||
"https://archive.org/cors/winampskin_Nebular/Nebular.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Enter_the_Matrix_Skin/Enter_the_Matrix.wal",
|
||||
"https://archive.org/cors/winampskin_Reel-To-Reel_Machine_Sony_Edition/ReelToReel_Machine__Sony_Edition.wal",
|
||||
"https://archive.org/cors/winampskin_Casio-G-Shocked-V5/Casio-G-Shocked-V2.wal",
|
||||
"https://archive.org/cors/winampskin_ZDL_GOLD_STACK/ZDL_GOLD_STACK.wal",
|
||||
"https://archive.org/cors/winampskin_BLAKK/BLAKK.wal",
|
||||
"https://archive.org/cors/winampskin_Braun_CC_50/Braun_CC_50.wal",
|
||||
"https://archive.org/cors/winampskin_Walk_Hard_Winamp5_Skin/Walk_Hard_Winamp5_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Freddy_vs_Jason/Freddy_vs_Jason.wal",
|
||||
"https://archive.org/cors/winampskin_The_Official_Grind_Winamp_3_Skin/The_Official_Grind_Winamp_3_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_The_KidsWB_Winamp_3_Skin/The_KidsWB_Winamp_3_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Sailor_Moon_Modern_version_1/Sailor_Moon_Modern_version_1.wal",
|
||||
"https://archive.org/cors/winampskin_Dr_Who_--_Monsters_and_Villains/Dr_Who_--_Monsters_and_Villains.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Linkin_Park_Skin/Official_Linkin_Park_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Resin/Resin.wal",
|
||||
"https://archive.org/cors/winampskin_PAD/PAD.wal",
|
||||
"https://archive.org/cors/winampskin_MIPOD/MIPOD.wal",
|
||||
"https://archive.org/cors/winampskin_Ebonite_2.0/Ebonite_2_1.wal",
|
||||
"https://archive.org/cors/winampskin_Drone_v1dot1/Drone_v1.wal",
|
||||
"https://archive.org/cors/winampskin_Hoop_Life_Modern/Hoop_Life_WA3_version.wal",
|
||||
"https://archive.org/cors/winampskin_Austin_Powers_Goldmember_Skin/Official_Austin_Powers_3_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Coca_Cola_My_Coke_Music/Coca_Cola__My_Coke_Music.wal",
|
||||
"https://archive.org/cors/winampskin_Barracuda_Winamp/Barracuda_Winamp.wal",
|
||||
"https://archive.org/cors/winampskin_Nike_total_90_aerow/Nike_total_90_aerow.wal",
|
||||
"https://archive.org/cors/winampskin_Metallica_Metallica/Metallica_Metallica.wal",
|
||||
"https://archive.org/cors/winampskin_Epsilux/Epsilux.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Witchblade_TV_Series_Skin/Official_Witchblade_TV_Series_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_ocrana/ocrana.wal",
|
||||
"https://archive.org/cors/winampskin_Clean_AMP/Clean_AMP.wal",
|
||||
"https://archive.org/cors/winampskin_Xbox_Amp/Xbox_Amp.wal",
|
||||
"https://archive.org/cors/winampskin_Lapis_Lazuli/Lapis_Lazuli.wal",
|
||||
"https://archive.org/cors/winampskin_The_Punisher_Winamp_5_Skin/The_Punisher_Winamp_5_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_The_Chronicles_of_Riddick/The_Chronicles_of_Riddick.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Midnight_Club_2_skin/Official_Midnight_Club_2_skin.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Torque_Winamp5_Skin/Official_Torque_Winamp5_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_Official_Mad_Magazine_Skin/Official_Mad_Magazine_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_PUMA_v1.08_Speed_Boot_Winamp5_Skin/PUMA_v1.08_Speed_Boot_Winamp5_Skin.wal",
|
||||
"https://archive.org/cors/winampskin_EMP/EMP.wal",
|
||||
"https://archive.org/cors/winampskin_Devay/Devay.wal",
|
||||
];
|
||||
|
||||
function getSkinUrlFromQueryParams() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return searchParams.get("skinUrl");
|
||||
}
|
||||
|
||||
function setSkinUrlToQueryParams(skinUrl) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.set("skinUrl", skinUrl);
|
||||
const newRelativePathQuery = `${
|
||||
window.location.pathname
|
||||
}?${searchParams.toString()}`;
|
||||
window.history.pushState(null, "", newRelativePathQuery);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <h1>Loading...</h1>;
|
||||
}
|
||||
|
||||
function Modern() {
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
const root = useSelector(Selectors.getMakiTree);
|
||||
const [skinUrl, setSkinUrl] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
const defaultSkinUrl = getSkinUrlFromQueryParams() || skinUrls[0];
|
||||
setSkinUrl(defaultSkinUrl);
|
||||
dispatch(Actions.gotSkinUrl(defaultSkinUrl, store));
|
||||
}, [store, dispatch]);
|
||||
if (root == null) {
|
||||
return <Loading />;
|
||||
}
|
||||
return (
|
||||
<div style={{ width: "100vw", height: "100vh", display: "flex" }}>
|
||||
<DropTarget
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
handleDrop={(e) => {
|
||||
dispatch(Actions.gotSkinBlob(e.dataTransfer.files[0], store));
|
||||
}}
|
||||
>
|
||||
<Maki makiObject={root} />
|
||||
</DropTarget>
|
||||
<select
|
||||
style={{ position: "absolute", bottom: 0 }}
|
||||
value={skinUrl}
|
||||
onChange={(e) => {
|
||||
const newSkinUrl = e.target.value;
|
||||
setSkinUrl(newSkinUrl);
|
||||
// TODO: This should really go in a middleware somewhere.
|
||||
setSkinUrlToQueryParams(newSkinUrl);
|
||||
dispatch(Actions.gotSkinUrl(newSkinUrl, store));
|
||||
}}
|
||||
>
|
||||
{skinUrls.map((url) => (
|
||||
<option value={url} key={url}>
|
||||
{url}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
{window.location.pathname.includes("ready") ? <Dashboard /> : <Modern />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import React from "react";
|
||||
import { objects } from "./maki-interpreter/objects";
|
||||
import snapshotString from "./__tests__/__snapshots__/objects.test.js.snap";
|
||||
import methodData from "../resources/maki-skin-data.json";
|
||||
|
||||
const methodsSting =
|
||||
snapshotString["Maki classes Track unimplemented methods 1"];
|
||||
|
||||
const unimplemented = new Set(
|
||||
methodsSting.substring(1, methodsSting.length - 2).split("\n")
|
||||
);
|
||||
|
||||
const GREEN = "rgba(0, 255, 0, 0.2)";
|
||||
const RED = "rgba(255, 0, 0, 0.2)";
|
||||
|
||||
let METHOD_COUNT = 0;
|
||||
let IMPLEMENTED_METHOD_COUNT = 0;
|
||||
|
||||
const normalizedMethods = [];
|
||||
Object.keys(objects).forEach((key) => {
|
||||
const makiObject = objects[key];
|
||||
makiObject.functions.forEach((method) => {
|
||||
METHOD_COUNT++;
|
||||
const normalizedName = `${makiObject.name}.${method.name.toLowerCase()}`;
|
||||
const implemented = !unimplemented.has(normalizedName);
|
||||
if (implemented) {
|
||||
IMPLEMENTED_METHOD_COUNT++;
|
||||
}
|
||||
const foundInSkins = methodData.foundInSkins[normalizedName] || 0;
|
||||
const totalCalls = methodData.totalCalls[normalizedName] || 0;
|
||||
normalizedMethods.push({
|
||||
className: makiObject.name,
|
||||
totalCalls,
|
||||
foundInSkins,
|
||||
methodName: method.name,
|
||||
normalizedName,
|
||||
implemented,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const foundMethods = normalizedMethods.filter(
|
||||
(method) => method.foundInSkins > 0
|
||||
);
|
||||
|
||||
function PercentBox({ number, total, label }) {
|
||||
const percent = total === 0 ? 1 : number / total;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "100px",
|
||||
border: "1px solid lightgrey",
|
||||
marginRight: "10px",
|
||||
textAlign: "center",
|
||||
color: "lightgrey",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{Math.round(percent * 100)}%
|
||||
<div
|
||||
style={{
|
||||
top: "0",
|
||||
position: "absolute",
|
||||
height: "100%",
|
||||
width: `${percent * 100}%`,
|
||||
backgroundColor: GREEN,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
textDecoration: percent === 1 ? "line-through" : null,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>{" "}
|
||||
<span style={{ color: "lightgrey" }}>
|
||||
({number}/{total})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [sortKey, setSortKey] = React.useState("totalCalls");
|
||||
const [sortDirection, setSortDirection] = React.useState("ASC");
|
||||
function setOrToggleSort(key) {
|
||||
if (sortKey === key) {
|
||||
setSortDirection((dir) => (dir === "ASC" ? "DESC" : "ASC"));
|
||||
} else {
|
||||
setSortKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
const sortAscending = (a, b) => (b[sortKey] > a[sortKey] ? 1 : -1);
|
||||
const sortDecending = (a, b) => (b[sortKey] < a[sortKey] ? 1 : -1);
|
||||
const sortFunction = sortDirection === "ASC" ? sortAscending : sortDecending;
|
||||
|
||||
let filterFunction = () => true;
|
||||
if (searchQuery) {
|
||||
const normalizedQuery = searchQuery.toLowerCase();
|
||||
|
||||
filterFunction = (method) => {
|
||||
return method.normalizedName.toLowerCase().includes(normalizedQuery);
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<h1>Are Modern Skins Ready Yet?</h1>
|
||||
<PercentBox
|
||||
number={IMPLEMENTED_METHOD_COUNT}
|
||||
total={METHOD_COUNT}
|
||||
label="All Methods"
|
||||
/>
|
||||
<PercentBox
|
||||
number={foundMethods.filter((method) => method.implemented).length}
|
||||
total={foundMethods.length}
|
||||
label="Used Methods"
|
||||
/>
|
||||
<input
|
||||
placeholder={"Search..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th onClick={() => setOrToggleSort("implemented")}>Status</th>
|
||||
<th onClick={() => setOrToggleSort("className")}>Class</th>
|
||||
<th onClick={() => setOrToggleSort("methodName")}>Method Name</th>
|
||||
<th onClick={() => setOrToggleSort("foundInSkins")}>
|
||||
Found In Skins
|
||||
</th>
|
||||
<th onClick={() => setOrToggleSort("totalCalls")}>Total Calls</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{normalizedMethods
|
||||
.sort(sortFunction)
|
||||
.filter(filterFunction)
|
||||
.map(
|
||||
({
|
||||
className,
|
||||
methodName,
|
||||
foundInSkins,
|
||||
totalCalls,
|
||||
implemented,
|
||||
}) => {
|
||||
return (
|
||||
<tr key={className + methodName}>
|
||||
<td>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "10px",
|
||||
border: "1px solid lightgrey",
|
||||
backgroundColor: implemented ? GREEN : RED,
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{className}</td>
|
||||
<td>{methodName}</td>
|
||||
<td>{foundInSkins}</td>
|
||||
<td>{totalCalls}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// TODO: Merge with the Webamp emitter
|
||||
export default class Emitter {
|
||||
_hooks: { [eventName: string]: Array<(...args: any[]) => void> };
|
||||
_globalHooks: Array<(eventName: string, ...args: any[]) => void>;
|
||||
constructor() {
|
||||
this._hooks = {};
|
||||
// TODO: Rename this property
|
||||
this._globalHooks = [];
|
||||
}
|
||||
|
||||
listen(eventName: string, cb: (...args: any[]) => void) {
|
||||
if (this._hooks[eventName] == null) {
|
||||
this._hooks[eventName] = [];
|
||||
}
|
||||
this._hooks[eventName].push(cb);
|
||||
return () => {
|
||||
this._hooks[eventName] = this._hooks[eventName].filter(
|
||||
(hookCb) => hookCb !== cb
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
trigger(eventName: string, ...args: any[]) {
|
||||
this._globalHooks.map((cb) => cb(eventName, ...args));
|
||||
if (this._hooks[eventName] == null) {
|
||||
return;
|
||||
}
|
||||
this._hooks[eventName].map((cb) => cb(...args));
|
||||
}
|
||||
|
||||
listenToAll(cb: (eventName: string, ...args: any[]) => void) {
|
||||
this._globalHooks.push(cb);
|
||||
return () => {
|
||||
this._globalHooks = this._globalHooks.filter((hookCb) => hookCb !== cb);
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Note: This will cause any future trigger or hook to cause a runtime error.
|
||||
this._hooks = {};
|
||||
this._globalHooks = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,585 +0,0 @@
|
|||
import React, { useEffect, useReducer } from "react";
|
||||
import "./App.css";
|
||||
import * as Utils from "./utils";
|
||||
|
||||
function useJsUpdates(makiObject) {
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
useEffect(() => makiObject.js_listen("js_update", forceUpdate));
|
||||
}
|
||||
|
||||
function handleMouseEventDispatch(makiObject, event, eventName) {
|
||||
event.stopPropagation();
|
||||
|
||||
// In order to properly calculate the x/y coordinates like MAKI does we need
|
||||
// to find the container element and calculate based off of that
|
||||
const container = Utils.findParentOrCurrentNodeOfType(
|
||||
makiObject,
|
||||
new Set(["container"])
|
||||
);
|
||||
const clientX = event.clientX;
|
||||
const clientY = event.clientY;
|
||||
const x = clientX - (Number(container.attributes.x) || 0);
|
||||
const y = clientY - (Number(container.attributes.y) || 0);
|
||||
makiObject.js_trigger(eventName, x, y);
|
||||
|
||||
if (event.nativeEvent.type === "mousedown") {
|
||||
// We need to persist the react event so we can access the target
|
||||
event.persist();
|
||||
document.addEventListener("mouseup", function globalMouseUp(ev) {
|
||||
document.removeEventListener("mouseup", globalMouseUp);
|
||||
// Create an object that looks and acts like an event, but has mixed
|
||||
// properties from original mousedown event and new mouseup event
|
||||
const fakeEvent = {
|
||||
target: event.target,
|
||||
clientX: ev.clientX,
|
||||
clientY: ev.clientY,
|
||||
nativeEvent: {
|
||||
type: "mouseup",
|
||||
},
|
||||
stopPropagation: ev.stopPropagation.bind(ev),
|
||||
};
|
||||
handleMouseEventDispatch(
|
||||
makiObject,
|
||||
fakeEvent,
|
||||
eventName === "onLeftButtonDown" ? "onLeftButtonUp" : "onRightButtonUp"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseButtonEventDispatch(
|
||||
makiObject,
|
||||
event,
|
||||
leftEventName,
|
||||
rightEventName
|
||||
) {
|
||||
handleMouseEventDispatch(
|
||||
makiObject,
|
||||
event,
|
||||
event.button === 2 ? rightEventName : leftEventName
|
||||
);
|
||||
}
|
||||
|
||||
function GuiObjectEvents({ makiObject, children }) {
|
||||
const { alpha, ghost } = makiObject.attributes;
|
||||
if (!makiObject.isvisible()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={(e) =>
|
||||
handleMouseButtonEventDispatch(
|
||||
makiObject,
|
||||
e,
|
||||
"onLeftButtonDown",
|
||||
"onRightButtonDown"
|
||||
)
|
||||
}
|
||||
onDoubleClick={(e) =>
|
||||
handleMouseButtonEventDispatch(
|
||||
makiObject,
|
||||
e,
|
||||
"onLeftButtonDblClk",
|
||||
"onRightButtonDblClk"
|
||||
)
|
||||
}
|
||||
onMouseMove={(e) =>
|
||||
handleMouseEventDispatch(makiObject, e, "onMouseMove")
|
||||
}
|
||||
onMouseEnter={(e) =>
|
||||
handleMouseEventDispatch(makiObject, e, "onEnterArea")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
handleMouseEventDispatch(makiObject, e, "onLeaveArea")
|
||||
}
|
||||
onDragEnter={() => makiObject.js_trigger("onDragEnter")}
|
||||
onDragLeave={() => makiObject.js_trigger("onDragLeave")}
|
||||
onDragOver={(e) => handleMouseEventDispatch(makiObject, e, "onDragOver")}
|
||||
onKeyUp={(e) => makiObject.js_trigger("onKeyUp", e.keyCode)}
|
||||
onKeyDown={(e) => makiObject.js_trigger("onKeyDown", e.keyCode)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
style={{
|
||||
opacity: alpha == null ? 1 : alpha / 255,
|
||||
pointerEvents: ghost ? "none" : null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Container({ makiObject }) {
|
||||
const { id, default_x, default_y, default_visible } = makiObject.attributes;
|
||||
|
||||
const style = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (default_x !== undefined) {
|
||||
style.left = Number(default_x);
|
||||
}
|
||||
if (default_y !== undefined) {
|
||||
style.top = Number(default_y);
|
||||
}
|
||||
if (default_visible !== undefined) {
|
||||
style.display = default_visible ? "block" : "none";
|
||||
}
|
||||
|
||||
const layout = makiObject.getcurlayout();
|
||||
if (layout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-node-type="container" data-node-id={id} style={style}>
|
||||
<Maki makiObject={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({ makiObject }) {
|
||||
const {
|
||||
id,
|
||||
js_assets,
|
||||
background,
|
||||
// desktopalpha,
|
||||
drawBackground,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
minimum_h,
|
||||
maximum_h,
|
||||
minimum_w,
|
||||
maximum_w,
|
||||
// droptarget,
|
||||
} = makiObject.attributes;
|
||||
if (drawBackground && background == null) {
|
||||
console.warn("Got a Layout without a background. Rendering null", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (drawBackground) {
|
||||
const image = js_assets.background;
|
||||
if (image == null) {
|
||||
console.warn(
|
||||
"Unable to find image to render. Rendering null",
|
||||
background
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="layout"
|
||||
data-node-id={id}
|
||||
src={image.imgUrl}
|
||||
draggable={false}
|
||||
style={{
|
||||
backgroundImage: `url(${image.imgUrl})`,
|
||||
width: image.w,
|
||||
height: image.h,
|
||||
overflow: "hidden",
|
||||
// TODO: This combo of height/minHeight ect is a bit odd. How should we combine these?
|
||||
minWidth: minimum_w == null ? null : Number(minimum_w),
|
||||
minHeight: minimum_h == null ? null : Number(minimum_h),
|
||||
maxWidth: maximum_w == null ? null : Number(maximum_w),
|
||||
maxHeight: maximum_h == null ? null : Number(maximum_h),
|
||||
position: "absolute",
|
||||
}}
|
||||
>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
const params = {};
|
||||
if (x !== undefined) {
|
||||
params.left = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
params.top = Number(y);
|
||||
}
|
||||
if (w !== undefined) {
|
||||
params.width = Number(w);
|
||||
params.overflow = "hidden";
|
||||
}
|
||||
if (h !== undefined) {
|
||||
params.height = Number(h);
|
||||
params.overflow = "hidden";
|
||||
}
|
||||
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="layout"
|
||||
data-node-id={id}
|
||||
draggable={false}
|
||||
style={{
|
||||
position: "absolute",
|
||||
...params,
|
||||
}}
|
||||
>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
function Layer({ makiObject }) {
|
||||
const { id, js_assets, image, x, y, w, h } = makiObject.attributes;
|
||||
if (image == null) {
|
||||
console.warn("Got an Layer without an image. Rendering null", id);
|
||||
return null;
|
||||
}
|
||||
const img = js_assets.image;
|
||||
if (img == null) {
|
||||
console.warn("Unable to find image to render. Rendering null", image);
|
||||
return null;
|
||||
}
|
||||
const params = {};
|
||||
if (x !== undefined) {
|
||||
params.left = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
params.top = Number(y);
|
||||
}
|
||||
if (img.x !== undefined) {
|
||||
params.backgroundPositionX = -Number(img.x);
|
||||
}
|
||||
if (img.y !== undefined) {
|
||||
params.backgroundPositionY = -Number(img.y);
|
||||
}
|
||||
if (w !== undefined) {
|
||||
params.width = Number(w);
|
||||
} else if (img.w !== undefined) {
|
||||
params.width = Number(img.w);
|
||||
}
|
||||
if (h !== undefined) {
|
||||
params.height = Number(h);
|
||||
} else if (img.h !== undefined) {
|
||||
params.height = Number(img.h);
|
||||
}
|
||||
if (img.imgUrl !== undefined) {
|
||||
params.backgroundImage = `url(${img.imgUrl}`;
|
||||
}
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="Layer"
|
||||
data-node-id={id}
|
||||
draggable={false}
|
||||
style={{ position: "absolute", ...params }}
|
||||
>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
function animatedLayerOffsetAndSize(
|
||||
frameNum,
|
||||
frameSize,
|
||||
layerSize,
|
||||
imgSize,
|
||||
imgOffset
|
||||
) {
|
||||
let size, offset;
|
||||
if (frameSize !== undefined) {
|
||||
size = Number(frameSize);
|
||||
offset = -Number(frameSize) * frameNum;
|
||||
} else if (layerSize !== undefined) {
|
||||
size = Number(layerSize);
|
||||
offset = -Number(layerSize) * frameNum;
|
||||
} else {
|
||||
if (imgSize !== undefined) {
|
||||
size = Number(imgSize);
|
||||
}
|
||||
if (imgOffset !== undefined) {
|
||||
offset = -Number(imgOffset);
|
||||
}
|
||||
}
|
||||
return { offset, size };
|
||||
}
|
||||
|
||||
function AnimatedLayer({ makiObject }) {
|
||||
const { id, js_assets, x, y, w, h, framewidth, frameheight } =
|
||||
makiObject.attributes;
|
||||
const img = js_assets.image;
|
||||
if (img == null) {
|
||||
console.warn("Got an AnimatedLayer without an image. Rendering null", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
const frameNum = makiObject.getcurframe();
|
||||
|
||||
let style = {};
|
||||
if (x !== undefined) {
|
||||
style.left = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
style.top = Number(y);
|
||||
}
|
||||
|
||||
const { offset: backgroundPositionX, size: width } =
|
||||
animatedLayerOffsetAndSize(frameNum, framewidth, w, img.w, img.x);
|
||||
const { offset: backgroundPositionY, size: height } =
|
||||
animatedLayerOffsetAndSize(frameNum, frameheight, h, img.h, img.y);
|
||||
style = { ...style, width, height, backgroundPositionX, backgroundPositionY };
|
||||
|
||||
if (img.imgUrl !== undefined) {
|
||||
style.backgroundImage = `url(${img.imgUrl}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="AnimatedLayer"
|
||||
data-node-id={id}
|
||||
draggable={false}
|
||||
style={{ position: "absolute", ...style }}
|
||||
>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({ makiObject }) {
|
||||
const {
|
||||
id,
|
||||
js_assets,
|
||||
// image,
|
||||
// action,
|
||||
x,
|
||||
y,
|
||||
downImage,
|
||||
tooltip,
|
||||
ghost,
|
||||
} = makiObject.attributes;
|
||||
const [down, setDown] = React.useState(false);
|
||||
// TODO: These seem to be switching too fast
|
||||
const img = down && downImage ? js_assets.downimage : js_assets.image;
|
||||
if (img == null) {
|
||||
console.warn("Got a Button without a img. Rendering null", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="button"
|
||||
data-node-id={id}
|
||||
onMouseDown={() => {
|
||||
setDown(true);
|
||||
document.addEventListener("mouseup", () => {
|
||||
// TODO: This could be unmounted
|
||||
setDown(false);
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.button === 2) {
|
||||
makiObject.js_trigger("onRightClick");
|
||||
} else {
|
||||
makiObject.js_trigger("onLeftClick");
|
||||
}
|
||||
}}
|
||||
title={tooltip}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: Number(y),
|
||||
left: Number(x),
|
||||
backgroundPositionX: -Number(img.x),
|
||||
backgroundPositionY: -Number(img.y),
|
||||
width: Number(img.w),
|
||||
height: Number(img.h),
|
||||
backgroundImage: `url(${img.imgUrl})`,
|
||||
pointerEvents: ghost ? "none" : null,
|
||||
}}
|
||||
>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
function Popupmenu({ makiObject }) {
|
||||
const { id, x, y } = makiObject.attributes;
|
||||
|
||||
const children = makiObject.js_getCommands().map((item) => {
|
||||
if (item.id === "seperator") {
|
||||
return <li />;
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
makiObject.js_selectCommand(item.id);
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
// TODO: Actually properly style element
|
||||
return (
|
||||
<div
|
||||
data-node-type="Popmenu"
|
||||
data-node-id={id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: Number(y),
|
||||
left: Number(x),
|
||||
backgroundColor: "#000000",
|
||||
color: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
<ul>{children}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleButton({ makiObject }) {
|
||||
return <Button makiObject={makiObject} />;
|
||||
}
|
||||
|
||||
function Group({ makiObject }) {
|
||||
const { id, x, y } = makiObject.attributes;
|
||||
const style = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (x !== undefined) {
|
||||
style.left = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
style.top = Number(y);
|
||||
}
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div data-node-type="group" data-node-id={id} style={style}>
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
function Text({ makiObject }) {
|
||||
const {
|
||||
id,
|
||||
display,
|
||||
// ticker,
|
||||
// antialias,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
font,
|
||||
fontsize,
|
||||
color,
|
||||
align,
|
||||
} = makiObject.attributes;
|
||||
const params = {};
|
||||
if (x !== undefined) {
|
||||
params.left = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
params.top = Number(y);
|
||||
}
|
||||
if (w !== undefined) {
|
||||
params.width = Number(w);
|
||||
}
|
||||
if (h !== undefined) {
|
||||
params.height = Number(h);
|
||||
}
|
||||
if (color !== undefined) {
|
||||
params.color = `rgb(${color})`;
|
||||
}
|
||||
if (fontsize !== undefined) {
|
||||
params.fontSize = `${fontsize}px`;
|
||||
}
|
||||
if (align !== undefined) {
|
||||
params.textAlign = align;
|
||||
}
|
||||
// display is actually a keyword that is looked up in some sort of map
|
||||
// e.g. songname, time
|
||||
const nodeText = display;
|
||||
let fontFamily;
|
||||
if (font) {
|
||||
const js_attributes = makiObject.js_fontLookup(font.toLowerCase());
|
||||
fontFamily = js_attributes == null ? null : js_attributes.fontFamily;
|
||||
}
|
||||
const style = {
|
||||
position: "absolute",
|
||||
userSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
...params,
|
||||
fontFamily,
|
||||
};
|
||||
|
||||
return (
|
||||
<GuiObjectEvents makiObject={makiObject}>
|
||||
<div
|
||||
data-node-type="Text"
|
||||
data-node-id={id}
|
||||
draggable={false}
|
||||
style={style}
|
||||
>
|
||||
{nodeText}
|
||||
<MakiChildren makiObject={makiObject} />
|
||||
</div>
|
||||
</GuiObjectEvents>
|
||||
);
|
||||
}
|
||||
|
||||
const NODE_NAME_TO_COMPONENT = {
|
||||
container: Container,
|
||||
layout: Layout,
|
||||
layer: Layer,
|
||||
button: Button,
|
||||
togglebutton: ToggleButton,
|
||||
group: Group,
|
||||
popupmenu: Popupmenu,
|
||||
text: Text,
|
||||
animatedlayer: AnimatedLayer,
|
||||
};
|
||||
|
||||
function DummyComponent({ makiObject }) {
|
||||
console.warn("Unknown makiObject type", makiObject.name);
|
||||
return <MakiChildren makiObject={makiObject} />;
|
||||
}
|
||||
|
||||
function MakiChildren({ makiObject }) {
|
||||
return makiObject
|
||||
.js_getChildren()
|
||||
.map((childMakiObject, i) => <Maki key={i} makiObject={childMakiObject} />);
|
||||
}
|
||||
|
||||
// Given a Maki object, pick which component to use, and render it.
|
||||
export const Maki = React.memo(({ makiObject }) => {
|
||||
useJsUpdates(makiObject);
|
||||
let { name } = makiObject;
|
||||
if (name == null) {
|
||||
// name is null is likely a comment
|
||||
return null;
|
||||
}
|
||||
name = name.toLowerCase();
|
||||
|
||||
if (
|
||||
name === "groupdef" ||
|
||||
name === "elements" ||
|
||||
name === "gammaset" ||
|
||||
name === "scripts" ||
|
||||
name === "script" ||
|
||||
name === "skininfo"
|
||||
) {
|
||||
// these nodes dont need to be rendered
|
||||
return null;
|
||||
}
|
||||
const Component = NODE_NAME_TO_COMPONENT[name] || DummyComponent;
|
||||
return <Component makiObject={makiObject} />;
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { ModernAppState, MakiTree } from "./types";
|
||||
|
||||
export function getMakiTree(state: ModernAppState): MakiTree | null {
|
||||
return state.modernSkin.makiTree;
|
||||
}
|
||||
|
||||
export function getVolume(state: ModernAppState): number {
|
||||
return state.modernSkin.volume;
|
||||
}
|
||||
|
||||
export function getRightVUMeter(state: ModernAppState): number {
|
||||
return state.modernSkin.rightVUMeter;
|
||||
}
|
||||
|
||||
export function getLeftVUMeter(state: ModernAppState): number {
|
||||
return state.modernSkin.leftVUMeter;
|
||||
}
|
||||
|
|
@ -275,6 +275,27 @@ export class UIRoot {
|
|||
return found ?? null;
|
||||
}
|
||||
|
||||
loadTrueTypeFonts() {
|
||||
const cssRules = [];
|
||||
const truetypeFonts: TrueTypeFont[] = this._fonts.filter(
|
||||
(font) => font instanceof TrueTypeFont
|
||||
) as TrueTypeFont[];
|
||||
for (const ttf of truetypeFonts) {
|
||||
if(!ttf.hasUrl()) {
|
||||
continue; // some dummy ttf (eg Arial) doesn't has url.
|
||||
}
|
||||
// src: url(data:font/truetype;charset=utf-8;base64,${ttf.getBase64()}) format('truetype');
|
||||
cssRules.push(`@font-face {
|
||||
font-family: '${ttf.getId()}';
|
||||
src: url(${ttf.getBase64()}) format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}`)
|
||||
}
|
||||
const cssEl = document.getElementById("truetypefont-css");
|
||||
cssEl.textContent = cssRules.join("\n");
|
||||
}
|
||||
|
||||
dispatch(action: string, param: string | null, actionTarget: string | null) {
|
||||
switch (action.toLowerCase()) {
|
||||
case "play":
|
||||
|
|
@ -1,604 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Maki classes Track unimplemented methods 1`] = `
|
||||
"System.getparam
|
||||
System.getskinname
|
||||
System.getplayitemstring
|
||||
System.geteq
|
||||
System.geteqband
|
||||
System.geteqpreamp
|
||||
System.getstatus
|
||||
System.messagebox
|
||||
System.getplayitemlength
|
||||
System.seekto
|
||||
System.newdynamiccontainer
|
||||
System.newgroup
|
||||
System.newgroupaslayout
|
||||
System.getnumcontainers
|
||||
System.enumcontainer
|
||||
System.getwac
|
||||
System.getplayitemmetadatastring
|
||||
System.getplayitemdisplaytitle
|
||||
System.getextfamily
|
||||
System.playfile
|
||||
System.play
|
||||
System.stop
|
||||
System.pause
|
||||
System.next
|
||||
System.previous
|
||||
System.eject
|
||||
System.getposition
|
||||
System.seteqband
|
||||
System.seteqpreamp
|
||||
System.seteq
|
||||
System.removepath
|
||||
System.getpath
|
||||
System.getextension
|
||||
System.setpublicstring
|
||||
System.setpublicint
|
||||
System.getpublicstring
|
||||
System.getpublicint
|
||||
System.getviewportwidthfrompoint
|
||||
System.getviewportheightfrompoint
|
||||
System.getviewportleft
|
||||
System.getviewportleftfrompoint
|
||||
System.getviewporttop
|
||||
System.getviewporttopfrompoint
|
||||
System.debugstring
|
||||
System.ddesend
|
||||
System.onlookforcomponent
|
||||
System.getcurappleft
|
||||
System.getcurapptop
|
||||
System.getcurappwidth
|
||||
System.getcurappheight
|
||||
System.isappactive
|
||||
System.switchskin
|
||||
System.isloadingskin
|
||||
System.lockui
|
||||
System.unlockui
|
||||
System.getmainbrowser
|
||||
System.popmainbrowser
|
||||
System.navigateurl
|
||||
System.isobjectvalid
|
||||
System.setmenutransparency
|
||||
System.ongetcancelcomponent
|
||||
System.iskeydown
|
||||
System.setclipboardtext
|
||||
System.selectfile
|
||||
System.systemmenu
|
||||
System.windowmenu
|
||||
System.triggeraction
|
||||
System.showwindow
|
||||
System.hidewindow
|
||||
System.hidenamedwindow
|
||||
System.isnamedwindowvisible
|
||||
System.setatom
|
||||
System.getatom
|
||||
System.invokedebugger
|
||||
System.isvideo
|
||||
System.isvideofullscreen
|
||||
System.getidealvideowidth
|
||||
System.getidealvideoheight
|
||||
System.isminimized
|
||||
System.minimizeapplication
|
||||
System.restoreapplication
|
||||
System.activateapplication
|
||||
System.getplaylistlength
|
||||
System.getplaylistindex
|
||||
System.isdesktopalphaavailable
|
||||
System.istransparencyavailable
|
||||
System.getsonginfotext
|
||||
System.getvisband
|
||||
System.onviewportchanged
|
||||
System.onurlchange
|
||||
System.oneqfreqchanged
|
||||
System.enumembedguid
|
||||
System.getmetadatastring
|
||||
System.getcurrenttrackrating
|
||||
System.oncurrenttrackrated
|
||||
System.setcurrenttrackrating
|
||||
System.getdecodername
|
||||
System.getalbumart
|
||||
System.downloadmedia
|
||||
System.downloadurl
|
||||
System.ondownloadfinished
|
||||
System.getdownloadpath
|
||||
System.setdownloadpath
|
||||
System.enqueuefile
|
||||
System.urldecode
|
||||
System.parseatf
|
||||
System.log10
|
||||
System.ln
|
||||
System.getviewportwidthfromguiobject
|
||||
System.getmonitorwidth
|
||||
System.getmonitorwidthfrompoint
|
||||
System.getmonitorwidthfromguiobject
|
||||
System.onmousemove
|
||||
System.getviewportheightfromguiobject
|
||||
System.getmonitorheight
|
||||
System.getmonitorheightfrompoint
|
||||
System.getmonitorheightfromguiobject
|
||||
System.getmonitorleft
|
||||
System.getmonitorleftfromguiobject
|
||||
System.getmonitorleftfrompoint
|
||||
System.getmonitortop
|
||||
System.getmonitortopfromguiobject
|
||||
System.getmonitortopfrompoint
|
||||
System.getviewportleftfromguiobject
|
||||
System.getviewporttopfromguiobject
|
||||
System.navigateurlbrowser
|
||||
System.onopenurl
|
||||
System.translate
|
||||
System.getstring
|
||||
System.getlanguageid
|
||||
System.selectfolder
|
||||
System.hasvideosupport
|
||||
System.clearplaylist
|
||||
System.getsonginfotexttranslated
|
||||
System.iswa2componentvisible
|
||||
System.hidewa2component
|
||||
System.isproversion
|
||||
System.getwinampversion
|
||||
System.getbuildnumber
|
||||
System.getfilesize
|
||||
Wac.getguid
|
||||
Wac.getname
|
||||
Wac.sendcommand
|
||||
Wac.show
|
||||
Wac.hide
|
||||
Wac.isvisible
|
||||
Wac.onnotify
|
||||
Wac.setstatusbar
|
||||
Wac.getstatusbar
|
||||
Map.loadmap
|
||||
Map.getwidth
|
||||
Map.getheight
|
||||
Map.getvalue
|
||||
Map.inregion
|
||||
Map.getregion
|
||||
Map.getargbvalue
|
||||
Region.loadfrommap
|
||||
Region.offset
|
||||
Region.add
|
||||
Region.sub
|
||||
Region.stretch
|
||||
Region.copy
|
||||
Region.loadfrombitmap
|
||||
Region.getboundingboxx
|
||||
Region.getboundingboxy
|
||||
Region.getboundingboxw
|
||||
Region.getboundingboxh
|
||||
Timer.getskipped
|
||||
Group.getnumobjects
|
||||
Group.enumobject
|
||||
Group.islayout
|
||||
GuiObject.getalpha
|
||||
GuiObject.setenabled
|
||||
GuiObject.getenabled
|
||||
GuiObject.ismouseover
|
||||
GuiObject.reversetarget
|
||||
GuiObject.isgoingtotarget
|
||||
GuiObject.bringtofront
|
||||
GuiObject.bringtoback
|
||||
GuiObject.bringabove
|
||||
GuiObject.bringbelow
|
||||
GuiObject.getguix
|
||||
GuiObject.getguiy
|
||||
GuiObject.getguiw
|
||||
GuiObject.getguih
|
||||
GuiObject.getguirelatx
|
||||
GuiObject.getguirelaty
|
||||
GuiObject.getguirelatw
|
||||
GuiObject.getguirelath
|
||||
GuiObject.isactive
|
||||
GuiObject.gettopparent
|
||||
GuiObject.runmodal
|
||||
GuiObject.endmodal
|
||||
GuiObject.findobjectxy
|
||||
GuiObject.getname
|
||||
GuiObject.clienttoscreenx
|
||||
GuiObject.clienttoscreeny
|
||||
GuiObject.clienttoscreenw
|
||||
GuiObject.clienttoscreenh
|
||||
GuiObject.screentoclientx
|
||||
GuiObject.screentoclienty
|
||||
GuiObject.screentoclientw
|
||||
GuiObject.screentoclienth
|
||||
GuiObject.getautowidth
|
||||
GuiObject.getautoheight
|
||||
GuiObject.setfocus
|
||||
GuiObject.ismouseoverrect
|
||||
GuiObject.getinterface
|
||||
GuiObject.sendaction
|
||||
GuiObject.onaction
|
||||
GuiObject.onmousewheelup
|
||||
GuiObject.onmousewheeldown
|
||||
GuiObject.ondragenter
|
||||
GuiObject.ondragover
|
||||
GuiObject.ondragleave
|
||||
Button.setactivated
|
||||
Button.getactivated
|
||||
Button.setactivatednocallback
|
||||
AnimatedLayer.ispaused
|
||||
AnimatedLayer.isstopped
|
||||
AnimatedLayer.getdirection
|
||||
AnimatedLayer.setrealtime
|
||||
ToggleButton.getcurcfgval
|
||||
PopupMenu.checkcommand
|
||||
PopupMenu.addsubmenu
|
||||
PopupMenu.popatxy
|
||||
PopupMenu.getnumcommands
|
||||
PopupMenu.disablecommand
|
||||
Container.getnumlayouts
|
||||
Container.enumlayout
|
||||
Container.switchtolayout
|
||||
Container.close
|
||||
Container.toggle
|
||||
Container.isdynamic
|
||||
Container.setname
|
||||
Container.getcurlayout
|
||||
Container.getname
|
||||
Container.getguid
|
||||
Container.onaddcontent
|
||||
Layout.getscale
|
||||
Layout.setscale
|
||||
Layout.setdesktopalpha
|
||||
Layout.getdesktopalpha
|
||||
Layout.center
|
||||
Layout.snapadjust
|
||||
Layout.getsnapadjusttop
|
||||
Layout.getsnapadjustright
|
||||
Layout.getsnapadjustleft
|
||||
Layout.getsnapadjustbottom
|
||||
Layout.setredrawonresize
|
||||
Layout.beforeredock
|
||||
Layout.redock
|
||||
Layout.istransparencysafe
|
||||
Layout.islayoutanimationsafe
|
||||
List.finditem2
|
||||
Layer.setregion
|
||||
Layer.setregionfrommap
|
||||
Layer.fx_oninit
|
||||
Layer.fx_onframe
|
||||
Layer.fx_ongetpixelr
|
||||
Layer.fx_ongetpixeld
|
||||
Layer.fx_ongetpixelx
|
||||
Layer.fx_ongetpixely
|
||||
Layer.fx_ongetpixela
|
||||
Layer.fx_setenabled
|
||||
Layer.fx_getenabled
|
||||
Layer.fx_setwrap
|
||||
Layer.fx_getwrap
|
||||
Layer.fx_setrect
|
||||
Layer.fx_getrect
|
||||
Layer.fx_setbgfx
|
||||
Layer.fx_getbgfx
|
||||
Layer.fx_setclear
|
||||
Layer.fx_getclear
|
||||
Layer.fx_setspeed
|
||||
Layer.fx_getspeed
|
||||
Layer.fx_setrealtime
|
||||
Layer.fx_getrealtime
|
||||
Layer.fx_setlocalized
|
||||
Layer.fx_getlocalized
|
||||
Layer.fx_setbilinear
|
||||
Layer.fx_getbilinear
|
||||
Layer.fx_setalphamode
|
||||
Layer.fx_getalphamode
|
||||
Layer.fx_setgridsize
|
||||
Layer.fx_update
|
||||
Layer.fx_restart
|
||||
Layer.isinvalid
|
||||
Text.setalternatetext
|
||||
Text.settext
|
||||
Text.gettext
|
||||
Text.gettextwidth
|
||||
WindowHolder.getguid
|
||||
WindowHolder.getwac
|
||||
WindowHolder.setregionfrommap
|
||||
WindowHolder.setregion
|
||||
WindowHolder.setacceptwac
|
||||
WindowHolder.getcontent
|
||||
WindowHolder.getcomponentname
|
||||
ComponentBucket.getmaxheight
|
||||
ComponentBucket.getmaxwidth
|
||||
ComponentBucket.setscroll
|
||||
ComponentBucket.getscroll
|
||||
ComponentBucket.getnumchildren
|
||||
ComponentBucket.enumchildren
|
||||
Edit.settext
|
||||
Edit.setautoenter
|
||||
Edit.getautoenter
|
||||
Edit.gettext
|
||||
Edit.selectall
|
||||
Edit.enter
|
||||
Edit.setidleenabled
|
||||
Edit.getidleenabled
|
||||
Slider.getposition
|
||||
Slider.setposition
|
||||
Slider.lock
|
||||
Slider.unlock
|
||||
Vis.setmode
|
||||
Vis.setrealtime
|
||||
Vis.getrealtime
|
||||
Vis.getmode
|
||||
Vis.nextmode
|
||||
Browser.navigateurl
|
||||
Browser.back
|
||||
Browser.forward
|
||||
Browser.stop
|
||||
Browser.refresh
|
||||
Browser.home
|
||||
Browser.settargetname
|
||||
Browser.ondocumentready
|
||||
Browser.getdocumenttitle
|
||||
Browser.onnavigateerror
|
||||
Browser.setcancelieerrorpage
|
||||
Browser.scrape
|
||||
Browser.onmedialink
|
||||
GroupList.instantiate
|
||||
GroupList.getnumitems
|
||||
GroupList.enumitem
|
||||
GroupList.removeall
|
||||
GroupList.scrolltopercent
|
||||
GroupList.setredraw
|
||||
CfgGroup.cfggetint
|
||||
CfgGroup.cfgsetint
|
||||
CfgGroup.cfggetstring
|
||||
CfgGroup.cfggetfloat
|
||||
CfgGroup.cfgsetfloat
|
||||
CfgGroup.cfgsetstring
|
||||
CfgGroup.cfggetguid
|
||||
CfgGroup.cfggetname
|
||||
MouseRedir.setredirection
|
||||
MouseRedir.getredirection
|
||||
MouseRedir.setregionfrommap
|
||||
MouseRedir.setregion
|
||||
DropDownList.getitemselected
|
||||
DropDownList.setlistheight
|
||||
DropDownList.openlist
|
||||
DropDownList.closelist
|
||||
DropDownList.setitems
|
||||
DropDownList.additem
|
||||
DropDownList.delitem
|
||||
DropDownList.finditem
|
||||
DropDownList.getnumitems
|
||||
DropDownList.selectitem
|
||||
DropDownList.getitemtext
|
||||
DropDownList.getselected
|
||||
DropDownList.getselectedtext
|
||||
DropDownList.getcustomtext
|
||||
DropDownList.deleteallitems
|
||||
DropDownList.setnoitemtext
|
||||
LayoutStatus.callme
|
||||
TabSheet.getcurpage
|
||||
TabSheet.setcurpage
|
||||
GuiList.getnumitems
|
||||
GuiList.getwantautodeselect
|
||||
GuiList.setwantautodeselect
|
||||
GuiList.setautosort
|
||||
GuiList.next
|
||||
GuiList.selectcurrent
|
||||
GuiList.selectfirstentry
|
||||
GuiList.previous
|
||||
GuiList.pagedown
|
||||
GuiList.pageup
|
||||
GuiList.home
|
||||
GuiList.end
|
||||
GuiList.reset
|
||||
GuiList.addcolumn
|
||||
GuiList.getnumcolumns
|
||||
GuiList.getcolumnwidth
|
||||
GuiList.setcolumnwidth
|
||||
GuiList.getcolumnlabel
|
||||
GuiList.setcolumnlabel
|
||||
GuiList.getcolumnnumeric
|
||||
GuiList.setcolumndynamic
|
||||
GuiList.iscolumndynamic
|
||||
GuiList.setminimumsize
|
||||
GuiList.additem
|
||||
GuiList.insertitem
|
||||
GuiList.getlastaddeditempos
|
||||
GuiList.setsubitem
|
||||
GuiList.deleteallitems
|
||||
GuiList.deletebypos
|
||||
GuiList.getitemlabel
|
||||
GuiList.setitemlabel
|
||||
GuiList.getitemselected
|
||||
GuiList.isitemfocused
|
||||
GuiList.getitemfocused
|
||||
GuiList.setitemfocused
|
||||
GuiList.ensureitemvisible
|
||||
GuiList.invalidatecolumns
|
||||
GuiList.scrollabsolute
|
||||
GuiList.scrollrelative
|
||||
GuiList.scrollleft
|
||||
GuiList.scrollright
|
||||
GuiList.scrollup
|
||||
GuiList.scrolldown
|
||||
GuiList.getsubitemtext
|
||||
GuiList.getfirstitemselected
|
||||
GuiList.getnextitemselected
|
||||
GuiList.selectall
|
||||
GuiList.deselectall
|
||||
GuiList.invertselection
|
||||
GuiList.invalidateitem
|
||||
GuiList.getfirstitemvisible
|
||||
GuiList.getlastitemvisible
|
||||
GuiList.setfontsize
|
||||
GuiList.getfontsize
|
||||
GuiList.jumptonext
|
||||
GuiList.scrolltoitem
|
||||
GuiList.resort
|
||||
GuiList.getsortdirection
|
||||
GuiList.getsortcolumn
|
||||
GuiList.setsortcolumn
|
||||
GuiList.setsortdirection
|
||||
GuiList.getitemcount
|
||||
GuiList.setselectionstart
|
||||
GuiList.setselectionend
|
||||
GuiList.setselected
|
||||
GuiList.toggleselection
|
||||
GuiList.getheaderheight
|
||||
GuiList.getpreventmultipleselection
|
||||
GuiList.setpreventmultipleselection
|
||||
GuiList.moveitem
|
||||
GuiList.onrightclick
|
||||
GuiList.oncolumndblclick
|
||||
GuiList.oncolumnlabelclick
|
||||
GuiList.setitemicon
|
||||
GuiList.getitemicon
|
||||
GuiList.setshowicons
|
||||
GuiList.getshowicons
|
||||
GuiList.seticonwidth
|
||||
GuiList.seticonheight
|
||||
GuiList.geticonwidth
|
||||
GuiList.geticonheight
|
||||
GuiList.oniconleftclick
|
||||
GuiTree.onwantautocontextmenu
|
||||
GuiTree.onmousewheelup
|
||||
GuiTree.onmousewheeldown
|
||||
GuiTree.oncontextmenu
|
||||
GuiTree.onchar
|
||||
GuiTree.getnumrootitems
|
||||
GuiTree.enumrootitem
|
||||
GuiTree.jumptonext
|
||||
GuiTree.ensureitemvisible
|
||||
GuiTree.getcontentswidth
|
||||
GuiTree.getcontentsheight
|
||||
GuiTree.addtreeitem
|
||||
GuiTree.removetreeitem
|
||||
GuiTree.movetreeitem
|
||||
GuiTree.deleteallitems
|
||||
GuiTree.expanditem
|
||||
GuiTree.expanditemdeferred
|
||||
GuiTree.collapseitem
|
||||
GuiTree.collapseitemdeferred
|
||||
GuiTree.selectitem
|
||||
GuiTree.selectitemdeferred
|
||||
GuiTree.delitemdeferred
|
||||
GuiTree.hiliteitem
|
||||
GuiTree.unhiliteitem
|
||||
GuiTree.getcuritem
|
||||
GuiTree.hittest
|
||||
GuiTree.edititemlabel
|
||||
GuiTree.canceleditlabel
|
||||
GuiTree.setautoedit
|
||||
GuiTree.getautoedit
|
||||
GuiTree.getbylabel
|
||||
GuiTree.setsorted
|
||||
GuiTree.getsorted
|
||||
GuiTree.sorttreeitems
|
||||
GuiTree.getsibling
|
||||
GuiTree.setautocollapse
|
||||
GuiTree.setfontsize
|
||||
GuiTree.getfontsize
|
||||
GuiTree.getnumvisiblechilditems
|
||||
GuiTree.getnumvisibleitems
|
||||
GuiTree.enumvisibleitems
|
||||
GuiTree.enumvisiblechilditems
|
||||
GuiTree.enumallitems
|
||||
GuiTree.getitemrectx
|
||||
GuiTree.getitemrecty
|
||||
GuiTree.getitemrectw
|
||||
GuiTree.getitemrecth
|
||||
GuiTree.getitemfrompoint
|
||||
TreeItem.getnumchildren
|
||||
TreeItem.setlabel
|
||||
TreeItem.getlabel
|
||||
TreeItem.ensurevisible
|
||||
TreeItem.getnthchild
|
||||
TreeItem.getchild
|
||||
TreeItem.getchildsibling
|
||||
TreeItem.getsibling
|
||||
TreeItem.getparent
|
||||
TreeItem.editlabel
|
||||
TreeItem.hassubitems
|
||||
TreeItem.setsorted
|
||||
TreeItem.setchildtab
|
||||
TreeItem.issorted
|
||||
TreeItem.iscollapsed
|
||||
TreeItem.isexpanded
|
||||
TreeItem.invalidate
|
||||
TreeItem.isselected
|
||||
TreeItem.ishilited
|
||||
TreeItem.sethilited
|
||||
TreeItem.collapse
|
||||
TreeItem.expand
|
||||
TreeItem.gettree
|
||||
MenuButton.openmenu
|
||||
MenuButton.closemenu
|
||||
CheckBox.setchecked
|
||||
CheckBox.ischecked
|
||||
CheckBox.settext
|
||||
CheckBox.gettext
|
||||
Config.newitem
|
||||
Config.getitem
|
||||
Config.getitembyguid
|
||||
ConfigItem.getattribute
|
||||
ConfigItem.newattribute
|
||||
ConfigItem.getguid
|
||||
ConfigItem.getname
|
||||
ConfigAttribute.setdata
|
||||
ConfigAttribute.getdata
|
||||
ConfigAttribute.getparentitem
|
||||
ConfigAttribute.getattributename
|
||||
PlDir.showcurrentlyplayingentry
|
||||
PlDir.getnumitems
|
||||
PlDir.getitemname
|
||||
PlDir.refresh
|
||||
PlDir.renameitem
|
||||
PlDir.enqueueitem
|
||||
PlDir.playitem
|
||||
FeedWatcher.setfeed
|
||||
FeedWatcher.releasefeed
|
||||
FeedWatcher.onfeedchange
|
||||
Form.getcontentsheight
|
||||
Form.newcell
|
||||
Form.nextrow
|
||||
Form.deleteall
|
||||
Frame.getposition
|
||||
Frame.setposition
|
||||
Frame.onsetposition
|
||||
PlEdit.getnumtracks
|
||||
PlEdit.getcurrentindex
|
||||
PlEdit.getnumselectedtracks
|
||||
PlEdit.getnextselectedtrack
|
||||
PlEdit.showcurrentlyplayingtrack
|
||||
PlEdit.showtrack
|
||||
PlEdit.enqueuefile
|
||||
PlEdit.clear
|
||||
PlEdit.removetrack
|
||||
PlEdit.swaptracks
|
||||
PlEdit.moveup
|
||||
PlEdit.movedown
|
||||
PlEdit.moveto
|
||||
PlEdit.playtrack
|
||||
PlEdit.getrating
|
||||
PlEdit.setrating
|
||||
PlEdit.gettitle
|
||||
PlEdit.getlength
|
||||
PlEdit.getmetadata
|
||||
PlEdit.getfilename
|
||||
PlEdit.onpleditmodified
|
||||
AlbumArtLayer.refresh
|
||||
AlbumArtLayer.isloading
|
||||
AlbumArtLayer.onalbumartloaded
|
||||
BitList.getitem
|
||||
BitList.setitem
|
||||
BitList.setsize
|
||||
BitList.getsize
|
||||
Menu.setmenugroup
|
||||
Menu.getmenugroup
|
||||
Menu.setmenu
|
||||
Menu.getmenu
|
||||
Menu.spawnmenu
|
||||
Menu.cancelmenu
|
||||
Menu.setnormalid
|
||||
Menu.setdownid
|
||||
Menu.sethoverid
|
||||
Menu.onopenmenu
|
||||
Menu.onclosemenu
|
||||
Menu.nextmenu
|
||||
Menu.previousmenu"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,104 +0,0 @@
|
|||
import path from "path";
|
||||
import fs from "fs";
|
||||
import JSZip from "jszip";
|
||||
import { create } from "../store";
|
||||
import * as Actions from "../Actions";
|
||||
import * as Utils from "../utils";
|
||||
import System from "../runtime/System";
|
||||
|
||||
test("sameObject", async () => {
|
||||
const messages = await runSkin("sameObject");
|
||||
|
||||
expect(messages).toEqual([
|
||||
["empty object equal each other", "Success", 0, ""],
|
||||
["same object equal each other", "Success", 0, ""],
|
||||
["different objects do not equal each other", "Success", 0, ""],
|
||||
]);
|
||||
});
|
||||
|
||||
test("simpleClick", async () => {
|
||||
const messages = await runSkin("simpleClick");
|
||||
|
||||
expect(messages).toEqual([
|
||||
["onScriptLoaded", "Success", 0, ""],
|
||||
["play_button.onLeftClick", "Success", 0, ""],
|
||||
]);
|
||||
});
|
||||
|
||||
// Adapted from https://github.com/Stuk/jszip/issues/386#issuecomment-510802546
|
||||
function buildZipFromDirectory(dir, zip, root) {
|
||||
const list = fs.readdirSync(dir);
|
||||
|
||||
for (let file of list) {
|
||||
file = path.resolve(dir, file);
|
||||
const stat = fs.statSync(file);
|
||||
if (stat && stat.isDirectory()) {
|
||||
buildZipFromDirectory(file, zip, root);
|
||||
} else {
|
||||
const filedata = fs.readFileSync(file);
|
||||
zip.file(path.relative(root, file), filedata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves when the state of a Redux store matches the
|
||||
// given predicate
|
||||
function isInState(store, predicate) {
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
if (predicate(store.getState())) {
|
||||
resolve();
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Given a skin directory in `resources/testSkins/` loads it and returns
|
||||
// an array representing all the calls to System.messagebox.
|
||||
async function runSkin(skinDirectory) {
|
||||
const skinDirectoryPath = path.join(
|
||||
__dirname,
|
||||
"../../resources/testSkins/",
|
||||
skinDirectory
|
||||
);
|
||||
const zip = new JSZip();
|
||||
buildZipFromDirectory(skinDirectoryPath, zip, skinDirectoryPath);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
function fakeMessageBox(a, b, c, d) {
|
||||
// This function has four fake arguments because we check the arity of the
|
||||
// method in the VM to determine how many values to pop off the stack.
|
||||
}
|
||||
expect(fakeMessageBox.length).toBe(System.prototype.messagebox.length);
|
||||
|
||||
const mockMessageBox = jest.fn(fakeMessageBox);
|
||||
System.prototype.messagebox = mockMessageBox;
|
||||
|
||||
const store = create();
|
||||
store.dispatch(Actions.gotSkinZip(zip, store));
|
||||
await isInState(store, (state) => state.modernSkin.skinLoaded);
|
||||
|
||||
return mockMessageBox.mock.calls;
|
||||
}
|
||||
|
||||
// Mock out some utility functions which depend upon browser APIs which are not
|
||||
// supported by JSDOM.
|
||||
|
||||
/* eslint-disable import/namespace */
|
||||
|
||||
Utils.getSizeFromUrl = jest.fn(() => {
|
||||
// TODO: Actually compute this. Perhaps with https://www.npmjs.com/package/image-size
|
||||
// It will likely require converting data URIs generted by getUrlFromBlob into a buffer.
|
||||
return { width: 0, height: 0 };
|
||||
});
|
||||
Utils.getUrlFromBlob = jest.fn((blob) => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
// TODO: Preserve the mimetype of these files
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { getClass, getFormattedId, objects } from "../maki-interpreter/objects";
|
||||
import runtime from "../runtime";
|
||||
|
||||
test("getFormattedId() is reversable", () => {
|
||||
Object.keys(runtime).forEach((id) => {
|
||||
const formattedId = getFormattedId(id);
|
||||
const inverse = getFormattedId(formattedId);
|
||||
expect(inverse).toBe(id);
|
||||
});
|
||||
Object.keys(objects)
|
||||
.map((id) => id.toLowerCase())
|
||||
.forEach((id) => {
|
||||
const formattedId = getFormattedId(id);
|
||||
const inverse = getFormattedId(formattedId);
|
||||
expect(inverse).toBe(id);
|
||||
});
|
||||
});
|
||||
|
||||
const getMakiMethods = (obj) =>
|
||||
Object.getOwnPropertyNames(obj).filter((name) => {
|
||||
return (
|
||||
typeof obj[name] === "function" &&
|
||||
!name.startsWith("js_") &&
|
||||
!name.startsWith("_") &&
|
||||
name !== "constructor"
|
||||
);
|
||||
});
|
||||
|
||||
for (const [key, Klass] of Object.entries(runtime)) {
|
||||
const obj = getClass(key);
|
||||
describe(`${obj.name}`, () => {
|
||||
test("implements getclassname()", () => {
|
||||
expect(Klass.prototype.getclassname()).toBe(obj.name);
|
||||
});
|
||||
test("has the correct parent", () => {
|
||||
const Parent = Object.getPrototypeOf(Klass);
|
||||
if (Klass.prototype.getclassname() === "Object") {
|
||||
expect(Parent.prototype).toBe(undefined);
|
||||
return;
|
||||
}
|
||||
expect(Parent.prototype.getclassname()).toBe(obj.parent);
|
||||
});
|
||||
describe("methods have the correct arity", () => {
|
||||
obj.functions.forEach((func) => {
|
||||
const methodName = func.name.toLowerCase();
|
||||
// Once all methods are implemented this check can be removed.
|
||||
// For now we have a separate test which checks that we haven't
|
||||
// regressed on the methods we've implemented.
|
||||
const hasMethodOnSelf = Klass.prototype.hasOwnProperty(methodName);
|
||||
test(`Has the method ${obj.name}.${func.name}`, () => {
|
||||
expect(hasMethodOnSelf).toBe(true);
|
||||
});
|
||||
if (!hasMethodOnSelf) {
|
||||
return;
|
||||
}
|
||||
const actual = Klass.prototype[func.name.toLowerCase()].length;
|
||||
test(`${obj.name}.${func.name} has an arity of ${actual}`, () => {
|
||||
expect(func.parameters.length).toBe(actual);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("Maki classes", () => {
|
||||
const runtimeMethods = new Set();
|
||||
const unimplementedRuntimeMethods = new Set();
|
||||
const objectMethods = new Set();
|
||||
for (const [key, Klass] of Object.entries(runtime)) {
|
||||
const obj = getClass(key);
|
||||
getMakiMethods(Klass.prototype).forEach((methodName) => {
|
||||
runtimeMethods.add(`${obj.name}.${methodName}`);
|
||||
const methodSource = Klass.prototype[methodName].toString();
|
||||
if (methodSource.includes("unimplementedWarning")) {
|
||||
unimplementedRuntimeMethods.add(`${obj.name}.${methodName}`);
|
||||
}
|
||||
});
|
||||
obj.functions.forEach((func) => {
|
||||
objectMethods.add(`${obj.name}.${func.name.toLowerCase()}`);
|
||||
});
|
||||
}
|
||||
|
||||
test("All classes are implemented", () => {
|
||||
const getName = (Klass) => Klass.prototype.getclassname();
|
||||
const actualNames = Object.keys(runtime).map(
|
||||
(id) => `${getName(runtime[id])} (${id})`
|
||||
);
|
||||
const expectedNames = Object.keys(objects).map(
|
||||
(id) => `${objects[id].name} (${getFormattedId(id)})`
|
||||
);
|
||||
expect(new Set(actualNames)).toEqual(new Set(expectedNames));
|
||||
});
|
||||
|
||||
test("have no extra methods", () => {
|
||||
// getclassname _should_ be implemented on Object and let each class inherit
|
||||
// it. However it's far easier to implement it on each class directly, so
|
||||
// we'll allow that.
|
||||
function isntGetClassname(method) {
|
||||
return !/\.getclassname$/.test(method);
|
||||
}
|
||||
|
||||
function isntMakiMethod(method) {
|
||||
return !objectMethods.has(method);
|
||||
}
|
||||
|
||||
const extra = [...runtimeMethods]
|
||||
.filter(isntMakiMethod)
|
||||
.filter(isntGetClassname);
|
||||
|
||||
expect(extra).toEqual([]);
|
||||
});
|
||||
|
||||
test("There are no missing methods", () => {
|
||||
const missing = [...objectMethods].filter((x) => !runtimeMethods.has(x));
|
||||
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test("Track unimplemented methods", () => {
|
||||
// Write this as a newline delineated string to make it easier to other
|
||||
// tools to extract from the `.snap` file.
|
||||
const expected = Array.from(unimplementedRuntimeMethods).join("\n");
|
||||
expect(expected).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import parse from "../maki-interpreter/parser";
|
||||
import { getClass } from "../maki-interpreter/objects";
|
||||
import { VERSIONS } from "./testConstants";
|
||||
|
||||
function parseFile(relativePath) {
|
||||
const buffer = readFileSync(join(__dirname, relativePath));
|
||||
return parse(buffer);
|
||||
}
|
||||
|
||||
describe("can parse without crashing", () => {
|
||||
const versions = [
|
||||
// VERSIONS.WINAMP_3_ALPHA,
|
||||
VERSIONS.WINAMP_3_BETA,
|
||||
VERSIONS.WINAMP_3_FULL,
|
||||
VERSIONS.WINAMP_5_02,
|
||||
VERSIONS.WINAMP_5_66,
|
||||
];
|
||||
|
||||
const scripts = [
|
||||
"hello_world.maki",
|
||||
"basicTests.maki",
|
||||
"simpleFunctions.maki",
|
||||
];
|
||||
|
||||
scripts.forEach((script) => {
|
||||
describe(`script ${script}`, () => {
|
||||
versions.forEach((version) => {
|
||||
test(`compiled with compiler version ${version}`, () => {
|
||||
expect(() => {
|
||||
parseFile(`../../resources/maki_compiler/${version}/${script}`);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip("regressions", () => {
|
||||
describe("https://github.com/captbaritone/webamp/issues/898", () => {
|
||||
test("minimal", () => {
|
||||
parseFile("../../resources/fixtures/issue_898/minimal.maki");
|
||||
});
|
||||
test("real world", () => {});
|
||||
});
|
||||
describe.skip("foo", () => {
|
||||
test("CproTabs", () => {
|
||||
parseFile("../../resources/fixtures/foo/CproTabs.maki");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("standardframe.maki", () => {
|
||||
let maki;
|
||||
beforeEach(() => {
|
||||
maki = parseFile("../../resources/fixtures/standardframe.maki");
|
||||
});
|
||||
|
||||
test("can read magic", () => {
|
||||
expect(maki.magic).toBe("FG");
|
||||
});
|
||||
|
||||
test("can read classes", () => {
|
||||
expect(maki.classes.map((klass) => getClass(klass).name)).toEqual([
|
||||
"Object",
|
||||
"System",
|
||||
"Container",
|
||||
"Wac",
|
||||
"List",
|
||||
"Map",
|
||||
"PopupMenu",
|
||||
"Region",
|
||||
"Timer",
|
||||
"GuiObject",
|
||||
"Group",
|
||||
"Layout",
|
||||
"WindowHolder",
|
||||
"ComponentBucket",
|
||||
"Edit",
|
||||
"Slider",
|
||||
"Vis",
|
||||
"Browser",
|
||||
"EqVis",
|
||||
"Status",
|
||||
"Text",
|
||||
"Title",
|
||||
"Layer",
|
||||
"Button",
|
||||
"AnimatedLayer",
|
||||
"ToggleButton",
|
||||
"GroupList",
|
||||
"CfgGroup",
|
||||
"QueryList",
|
||||
"MouseRedir",
|
||||
"DropDownList",
|
||||
"LayoutStatus",
|
||||
"TabSheet",
|
||||
]);
|
||||
});
|
||||
|
||||
test("can read methods", () => {
|
||||
expect(maki.methods.map((func) => func.name)).toEqual([
|
||||
"onScriptLoaded",
|
||||
"getScriptGroup",
|
||||
"getParam",
|
||||
"getToken",
|
||||
"onSetXuiParam",
|
||||
"findObject",
|
||||
"setXmlParam",
|
||||
"setXmlParam",
|
||||
"messagebox",
|
||||
"onNotify",
|
||||
"newGroup",
|
||||
"init",
|
||||
]);
|
||||
expect(maki.methods.every((func) => func.typeOffset != null)).toBe(true);
|
||||
});
|
||||
|
||||
test("can read variables", () => {
|
||||
expect(maki.variables.length).toBe(56);
|
||||
expect(
|
||||
maki.variables.map((variable) => {
|
||||
const { typeName, type } = variable;
|
||||
if (typeName === "OBJECT") {
|
||||
return type;
|
||||
}
|
||||
return typeName;
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"d6f50f6449b793fa66baf193983eaeef",
|
||||
"INT",
|
||||
"45be95e5419120725fbb5c93fd17f1f9",
|
||||
"45be95e5419120725fbb5c93fd17f1f9",
|
||||
"45be95e5419120725fbb5c93fd17f1f9",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"5ab9fa1545579a7d5765c8aba97cc6a6",
|
||||
"698eddcd4fec8f1e44f9129b45ff09f9",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"INT",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"INT",
|
||||
"INT",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
"STRING",
|
||||
]
|
||||
`);
|
||||
maki.variables.forEach((variable) => {
|
||||
expect(variable.type).not.toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test("can read bindings", () => {
|
||||
expect(maki.bindings).toEqual([
|
||||
{ variableOffset: 0, commandOffset: 0, methodOffset: 0 },
|
||||
{ variableOffset: 0, commandOffset: 76, methodOffset: 4 },
|
||||
{ variableOffset: 2, commandOffset: 143, methodOffset: 9 },
|
||||
]);
|
||||
});
|
||||
|
||||
// [opcode, size] as output by the Perl decompiler
|
||||
// TODO: Get rid of the size values here, they are not used any more
|
||||
// prettier-ignore
|
||||
const expectedCommands = [1, 1, 24, 48, 2, 1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2,
|
||||
1, 1, 1, 1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2, 1, 1,
|
||||
1, 1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2, 1, 1, 1, 1,
|
||||
1, 24, 48, 2, 1, 33, 3, 3, 1, 1, 8, 16, 1, 25, 2, 1, 1, 1, 24, 48, 2, 1, 1, 9, 16, 1,
|
||||
1, 1, 24, 48, 2, 1, 1, 8, 1, 1, 8, 81, 16, 1, 1, 9, 16, 1, 1, 1, 24, 2, 1, 1, 8, 16, 1,
|
||||
1, 9, 16, 1, 1, 1, 64, 1, 24, 2, 18, 1, 1, 1, 1, 1, 24, 2, 1, 33, 3, 3, 3, 3, 1, 1, 1,
|
||||
1, 1, 24, 48, 2, 1, 1, 1, 1, 1, 24, 48, 2, 1, 1, 8, 1, 1, 8, 81, 1, 1, 8, 81, 1, 1, 8,
|
||||
81, 16, 1, 1, 1, 24, 2, 1, 33, 3, 1, 1, 1, 24, 48, 2, 1, 1, 8, 16, 1, 1, 1, 1, 1, 1, 64,
|
||||
1, 64, 24, 2, 1, 33, 1, 1, 1, 24, 2, 1, 1, 1, 24, 2, 1, 1, 1, 24, 2, 1, 1, 1, 24, 2, 1, 1,
|
||||
1, 24, 2, 1, 1, 1, 24, 2, 1, 1, 1, 24, 2, 1, 1, 1, 24, 2, 1, 1, 24, 2, 1, 33,
|
||||
];
|
||||
|
||||
test("can read commands", () => {
|
||||
maki.commands.forEach((command, i) => {
|
||||
const expectedOpcode = expectedCommands[i];
|
||||
if (expectedOpcode !== command.opcode) {
|
||||
throw new Error(
|
||||
`Command ${i} reported opcode ${command.opcode}. Expected ${expectedOpcode}`
|
||||
);
|
||||
}
|
||||
});
|
||||
expect(maki.commands.length).toBe(expectedCommands.length);
|
||||
});
|
||||
|
||||
// I don't know what either of these actually are.
|
||||
test("extracts version info", () => {
|
||||
expect(maki.version).toBe(1027);
|
||||
expect(maki.extraVersion).toBe(23);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export const VERSIONS = {
|
||||
WINAMP_3_ALPHA: "v1.1.0.a9 (Winamp 3 alpha 8r)",
|
||||
WINAMP_3_BETA: "v1.1.1.b3 (Winamp 3.0 build 488d)",
|
||||
WINAMP_3_FULL: "v1.1.1.b3 (Winamp 3.0 full)",
|
||||
WINAMP_5_02: "v1.1.13 (Winamp 5.02)",
|
||||
WINAMP_5_66: "v1.2.0 (Winamp 5.66)",
|
||||
};
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import * as Utils from "../utils";
|
||||
import JSZip from "jszip";
|
||||
import { promises as fsPromises } from "fs";
|
||||
import path from "path";
|
||||
|
||||
async function getSkinZip() {
|
||||
const skinBuffer = await fsPromises.readFile(
|
||||
path.join(__dirname, "../../skins/CornerAmp_Redux.wal")
|
||||
);
|
||||
return JSZip.loadAsync(skinBuffer);
|
||||
}
|
||||
|
||||
describe("getCaseInsensitiveFile", () => {
|
||||
it("gets a file independent of case", async () => {
|
||||
const zip = await getSkinZip();
|
||||
expect(Utils.getCaseInsensitveFile(zip, "SkIn.XmL")).not.toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readXml", () => {
|
||||
it("gets a file independent of case", async () => {
|
||||
const zip = await getSkinZip();
|
||||
const xml = await Utils.readXml(zip, "SkIn.XmL");
|
||||
expect(xml).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("inlineIncludes", () => {
|
||||
test("asyncTreeFlatMap", async () => {
|
||||
const playerElements = {
|
||||
name: "player-elements",
|
||||
children: [{ name: "player-elements-child" }],
|
||||
};
|
||||
const playerNormal = {
|
||||
name: "player-normal",
|
||||
children: [{ name: "player-normal-child" }],
|
||||
};
|
||||
const player = {
|
||||
name: "player",
|
||||
children: [
|
||||
{
|
||||
name: "player-elements-include",
|
||||
include: playerElements,
|
||||
},
|
||||
{
|
||||
name: "main-container",
|
||||
children: [
|
||||
{
|
||||
name: "player-normal-include",
|
||||
include: playerNormal,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const xml = {
|
||||
name: "root",
|
||||
children: [{ name: "meta" }, { name: "include player", include: player }],
|
||||
};
|
||||
|
||||
function resolveInclude(node) {
|
||||
if (node.include) {
|
||||
return node.include.children;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
const resolved = await Utils.asyncTreeFlatMap(xml, resolveInclude);
|
||||
expect(resolved).toEqual({
|
||||
name: "root",
|
||||
children: [
|
||||
{ name: "meta" },
|
||||
{ name: "player-elements-child" },
|
||||
{ name: "main-container", children: [{ name: "player-normal-child" }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("inlines the contents of included files as children of the include node", async () => {
|
||||
const zip = await getSkinZip();
|
||||
const originalFile = zip.file;
|
||||
zip.file = jest.fn((filePath) => originalFile.call(zip, filePath));
|
||||
|
||||
const xml = await Utils.readXml(zip, "SkIn.XmL");
|
||||
const resolvedXml = await Utils.inlineIncludes(xml, zip);
|
||||
expect(resolvedXml).toMatchSnapshot();
|
||||
expect(zip.file.mock.calls.map((args) => args[0])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
/SkIn\\.XmL/i,
|
||||
/xml\\\\/system-colors\\.xml/i,
|
||||
/xml\\\\/standardframe\\.xml/i,
|
||||
/xml\\\\/player\\.xml/i,
|
||||
/xml\\\\/pledit\\.xml/i,
|
||||
/xml\\\\/video\\.xml/i,
|
||||
/xml\\\\/eq\\.xml/i,
|
||||
/xml\\\\/color-presets\\.xml/i,
|
||||
/xml\\\\/color-themes\\.xml/i,
|
||||
/studio-elements\\.xml/i,
|
||||
/player-elements\\.xml/i,
|
||||
/player-normal\\.xml/i,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncFlatMap", () => {
|
||||
test("recurses", async () => {
|
||||
const start = ["parent", ["child", ["grandchild"], "sibling"], "partner"];
|
||||
expect(await Utils.asyncFlatMap(start, (v) => Promise.resolve(v))).toEqual([
|
||||
"parent",
|
||||
"child",
|
||||
"grandchild",
|
||||
"sibling",
|
||||
"partner",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncTreeFlatMap", () => {
|
||||
test("encounters children first", async () => {
|
||||
const mapper = jest.fn(async (node) => {
|
||||
if (node.replaceWithChildren) {
|
||||
return node.children;
|
||||
}
|
||||
return { ...node, name: node.name.toLowerCase() };
|
||||
});
|
||||
|
||||
const start = {
|
||||
name: "A",
|
||||
children: [
|
||||
{ name: "B" },
|
||||
{
|
||||
name: "C",
|
||||
children: [
|
||||
{ name: "E" },
|
||||
{ name: "F", replaceWithChildren: true, children: [{ name: "G" }] },
|
||||
],
|
||||
replaceWithChildren: true,
|
||||
},
|
||||
{ name: "D" },
|
||||
],
|
||||
};
|
||||
expect(await Utils.asyncTreeFlatMap(start, mapper)).toEqual({
|
||||
name: "A",
|
||||
children: [{ name: "b" }, { name: "e" }, { name: "g" }, { name: "d" }],
|
||||
});
|
||||
|
||||
const callOrder = mapper.mock.calls.map((args) => args[0].name);
|
||||
expect(callOrder).toEqual(["B", "C", "D", "E", "F", "G"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import path from "path";
|
||||
import { run } from "../maki-interpreter/virtualMachine";
|
||||
import runtime from "../runtime";
|
||||
import System from "../runtime/System";
|
||||
import fs from "fs";
|
||||
|
||||
function runScript(filePath) {
|
||||
const scriptFullPath = path.join(
|
||||
__dirname,
|
||||
"../../resources/maki_compiler/v1.2.0 (Winamp 5.66)",
|
||||
filePath
|
||||
);
|
||||
|
||||
const program = fs.readFileSync(scriptFullPath);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
function fakeMessageBox(a, b, c, d) {
|
||||
// This function has four fake arguments because we check the arity of the
|
||||
// method in the VM to determine how many values to pop off the stack.
|
||||
}
|
||||
expect(fakeMessageBox.length).toBe(System.prototype.messagebox.length);
|
||||
|
||||
const mockMessageBox = jest.fn(fakeMessageBox);
|
||||
System.prototype.messagebox = mockMessageBox;
|
||||
|
||||
const system = new System();
|
||||
|
||||
run({ runtime, data: program, system });
|
||||
|
||||
return mockMessageBox.mock.calls
|
||||
.map(([assertion, result]) => `${result}: ${assertion}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
test.skip("basicTests", () => {
|
||||
expect(runScript("basicTests.maki")).toMatchInlineSnapshot(`
|
||||
"Success: 2 + 2 = 4
|
||||
Success: 2.2 + 2.2 = 4.4
|
||||
Success: 4 + 4.4 = 4.4 + 4 (not implict casting)
|
||||
Success: #t + #t = 2
|
||||
Success: 3 - 2 = 1
|
||||
Success: 3 - -2 = 5
|
||||
Success: 3.5 - 2 = 1.5
|
||||
Success: 2 * 3 = 6
|
||||
Success: 2 * 1.5 = 3
|
||||
Success: #t * 3 = 3
|
||||
Success: #f * 3 = 0
|
||||
Success: #t * 0.25 = 0.25
|
||||
Success: 0.25 * #t = 0.25
|
||||
Success: #f * 0.25 = 0
|
||||
Success: 6 / 3 = 2
|
||||
Success: 3 / 2 = 1.5
|
||||
Success: 5 % 2 = 1
|
||||
Success: 5.5 % 2 = 1 (implict casting)
|
||||
Success: 3 & 2 = 2
|
||||
Success: 3 | 2 = 3
|
||||
Success: 2 << 1 = 4
|
||||
Success: 4 >> 1 = 2
|
||||
Success: 2.5 << 1 = 4 (implict casting)
|
||||
Success: 4.5 >> 1 = 2 (implict casting)
|
||||
Success: 1 != 2
|
||||
Success: 1 < 2
|
||||
Success: 2 > 1
|
||||
Success: [int] 4 = [float] 4.4 (autocasting types)
|
||||
Success: ! [float] 4.4 = [int] 4 (not autocasting types)
|
||||
Success: [float] 4.4 != [int] 4 (not autocasting types)
|
||||
Success: ! [int] 4 != [float] 4.4 (autocasting types)
|
||||
Success: [int] 4 <= [float] 4.4 (autocasting types)
|
||||
Success: [int] 4 >= [float] 4.4 (autocasting types)
|
||||
Success: ! [float] 4.4 <= [int] 4 (not autocasting types)
|
||||
Success: [float] 4.4 >= [int] 4 (not autocasting types)
|
||||
Success: ! [int] 4 < [float] 4.4 (autocasting types)
|
||||
Success: ! [float] 4.4 < [int] 4 (not autocasting types)
|
||||
Success: ! [int] 4 > [float] 4.4 (autocasting types)
|
||||
Success: [float] 4.4 > [int] 4 (not autocasting types)
|
||||
Success: 1++ = 1
|
||||
Success: 1++ (after incremeent) = 2
|
||||
Success: 2-- = 2
|
||||
Success: 2-- (after decrement) = 1
|
||||
Success: ++1 = 2
|
||||
Success: !#f
|
||||
Success: !0
|
||||
Success: !1 == #f
|
||||
Success: 1 == #t
|
||||
Success: 0 == #f
|
||||
Success: #t && #t
|
||||
Success: !(#t && #f)
|
||||
Success: !(#f && #f)
|
||||
Success: #t || #t
|
||||
Success: #t || #f
|
||||
Success: #f || #t
|
||||
Success: !(#f || #f)
|
||||
Success: #t || ++n (doesn't short circuit)
|
||||
Success: !(#f && ++ n) (doesn't short circuit)"
|
||||
`);
|
||||
});
|
||||
test("hello_world", () => {
|
||||
expect(runScript("hello_world.maki")).toMatchInlineSnapshot(
|
||||
`"Hello Title: Hello World"`
|
||||
);
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import React, { useCallback } from "react";
|
||||
|
||||
interface Coord {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
handleDrop(e: React.DragEvent<HTMLDivElement>, coord: Coord): void;
|
||||
}
|
||||
|
||||
function supress(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "link";
|
||||
e.dataTransfer.effectAllowed = "link";
|
||||
}
|
||||
|
||||
const DropTarget = (props: Props) => {
|
||||
const {
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
handleDrop,
|
||||
...passThroughProps
|
||||
} = props;
|
||||
|
||||
const onDrop = useCallback(
|
||||
(e: React.DragEvent<HTMLDivElement>) => {
|
||||
supress(e);
|
||||
// TODO: We could probably move this coordinate logic into the playlist.
|
||||
// I think that's the only place it gets used.
|
||||
const { currentTarget } = e;
|
||||
if (!(currentTarget instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { left: x, top: y } = currentTarget.getBoundingClientRect();
|
||||
handleDrop(e, { x, y });
|
||||
},
|
||||
[handleDrop]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
{...passThroughProps}
|
||||
onDragStart={supress}
|
||||
onDragEnter={supress}
|
||||
onDragOver={supress}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropTarget;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
|
@ -251,6 +251,7 @@
|
|||
}
|
||||
</style>
|
||||
<style id="bitmap-css"></style>
|
||||
<style id="truetypefont-css"></style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="status">Downloading JavaScript...</h1>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { create } from "./store";
|
||||
|
||||
const store = create();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
@ -47,6 +47,8 @@ async function loadSkin(skinData: Blob) {
|
|||
// This is always the same as the global singleton.
|
||||
const uiRoot = await parser.parse();
|
||||
|
||||
uiRoot.loadTrueTypeFonts();
|
||||
|
||||
const start = performance.now();
|
||||
uiRoot.enableDefaultGammaSet();
|
||||
const end = performance.now();
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
import * as Utils from "./utils";
|
||||
import MakiObject from "./runtime/MakiObject";
|
||||
import GuiObject from "./runtime/GuiObject";
|
||||
import JsWinampAbstractionLayer from "./runtime/JsWinampAbstractionLayer";
|
||||
import Layout from "./runtime/Layout";
|
||||
import Layer from "./runtime/Layer";
|
||||
import Container from "./runtime/Container";
|
||||
import JsScript from "./runtime/JsScript";
|
||||
import JsElements from "./runtime/JsElements";
|
||||
import JsGammaSet from "./runtime/JsGammaSet";
|
||||
import JsGroupDef from "./runtime/JsGroupDef";
|
||||
import Group from "./runtime/Group";
|
||||
import Button from "./runtime/Button";
|
||||
import ToggleButton from "./runtime/ToggleButton";
|
||||
import Text from "./runtime/Text";
|
||||
import Status from "./runtime/Status";
|
||||
import Slider from "./runtime/Slider";
|
||||
import Vis from "./runtime/Vis";
|
||||
import EqVis from "./runtime/EqVis";
|
||||
import AnimatedLayer from "./runtime/AnimatedLayer";
|
||||
import WindowHolder from "./runtime/WindowHolder";
|
||||
|
||||
async function prepareMakiImage(node, zip, file) {
|
||||
let { h, w } = node.attributes;
|
||||
// TODO: Escape file for regex
|
||||
const img = Utils.getCaseInsensitveFile(zip, file);
|
||||
if (img === undefined) {
|
||||
return {};
|
||||
}
|
||||
const imgBlob = await img.async("blob");
|
||||
const imgUrl = await Utils.getUrlFromBlob(imgBlob);
|
||||
if (w === undefined || h === undefined) {
|
||||
const { width, height } = await Utils.getSizeFromUrl(imgUrl);
|
||||
w = width;
|
||||
h = height;
|
||||
}
|
||||
|
||||
return {
|
||||
h,
|
||||
w,
|
||||
imgUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const noop = (node, parent) => new GuiObject(node, parent);
|
||||
|
||||
const parsers = {
|
||||
groupdef: (node, parent) => new JsGroupDef(node, parent),
|
||||
skininfo: noop,
|
||||
guiobject: noop,
|
||||
version: noop,
|
||||
name: noop,
|
||||
comment: noop,
|
||||
syscmds: noop,
|
||||
author: noop,
|
||||
email: noop,
|
||||
homepage: noop,
|
||||
screenshot: noop,
|
||||
container: (node, parent) => new Container(node, parent),
|
||||
scripts: noop,
|
||||
gammaset: (node, parent) => new JsGammaSet(node, parent),
|
||||
color: noop,
|
||||
layer: (node, parent) => new Layer(node, parent),
|
||||
layoutstatus: noop,
|
||||
hideobject: noop,
|
||||
button: (node, parent) => new Button(node, parent),
|
||||
group: (node, parent) => new Group(node, parent),
|
||||
layout: (node, parent) => new Layout(node, parent),
|
||||
sendparams: noop,
|
||||
elements: (node, parent) => new JsElements(node, parent),
|
||||
bitmap: noop,
|
||||
eqvis: (node, parent) => new EqVis(node, parent),
|
||||
slider: (node, parent) => new Slider(node, parent),
|
||||
gammagroup: noop,
|
||||
truetypefont: async (node, parent, zip) => {
|
||||
const { file } = node.attributes;
|
||||
const font = Utils.getCaseInsensitveFile(zip, file);
|
||||
if (!font) {
|
||||
console.warn(`Unable to find font file ${file}`);
|
||||
return new MakiObject(node, parent);
|
||||
}
|
||||
const fontBlob = await font.async("blob");
|
||||
const fontUrl = await Utils.getUrlFromBlob(fontBlob);
|
||||
const fontFamily = `font-${Utils.getId()}-${file.replace(/\./, "_")}`;
|
||||
try {
|
||||
await Utils.loadFont(fontUrl, fontFamily);
|
||||
} catch {
|
||||
console.warn(`Failed to load font ${fontFamily}`);
|
||||
return new MakiObject(node, parent);
|
||||
}
|
||||
return new MakiObject(node, parent, { fontFamily });
|
||||
},
|
||||
component: (node, parent) => new WindowHolder(node, parent),
|
||||
text: (node, parent) => new Text(node, parent),
|
||||
togglebutton: (node, parent) => new ToggleButton(node, parent),
|
||||
status: (node, parent) => new Status(node, parent),
|
||||
bitmapfont: noop,
|
||||
vis: (node, parent) => new Vis(node, parent),
|
||||
"wasabi:titlebar": noop,
|
||||
"colorthemes:list": noop,
|
||||
"wasabi:standardframe:status": noop,
|
||||
"wasabi:standardframe:nostatus": noop,
|
||||
"wasabi:button": noop,
|
||||
accelerators: noop,
|
||||
accelerator: noop,
|
||||
cursor: noop,
|
||||
elementalias: noop,
|
||||
grid: noop,
|
||||
rect: noop,
|
||||
animatedlayer: (node, parent) => new AnimatedLayer(node, parent),
|
||||
nstatesbutton: noop,
|
||||
songticker: noop,
|
||||
menu: noop,
|
||||
albumart: noop,
|
||||
playlistplus: noop,
|
||||
script: (node, parent) => new JsScript(node, parent),
|
||||
};
|
||||
|
||||
async function parseChildren(node, children, zip) {
|
||||
if (node.type === "comment") {
|
||||
return;
|
||||
}
|
||||
if (node.name == null) {
|
||||
console.error(node);
|
||||
throw new Error("Unknown node");
|
||||
}
|
||||
|
||||
const resolvedChildren = await Promise.all(
|
||||
children.map(async (child) => {
|
||||
if (child.type === "comment") {
|
||||
return;
|
||||
}
|
||||
if (child.type === "text") {
|
||||
// TODO: Handle text
|
||||
return new MakiObject({ ...child }, node, undefined);
|
||||
}
|
||||
if (child.name == null) {
|
||||
console.error(child);
|
||||
throw new Error("Unknown node");
|
||||
}
|
||||
const childName = child.name.toLowerCase();
|
||||
if (childName == null) {
|
||||
console.error(node);
|
||||
throw new Error("Unknown node");
|
||||
}
|
||||
|
||||
let childParser = parsers[childName];
|
||||
if (childParser == null) {
|
||||
console.warn(`Missing parser in initialize for ${childName}`);
|
||||
childParser = noop;
|
||||
}
|
||||
const parsedChild = await childParser(child, node, zip);
|
||||
child.maki = parsedChild;
|
||||
if (child.children != null && child.children.length > 0) {
|
||||
await parseChildren(parsedChild, child.children, zip);
|
||||
}
|
||||
return parsedChild;
|
||||
})
|
||||
);
|
||||
// remove comments other trimmed nodes
|
||||
const filteredChildren = resolvedChildren.filter(
|
||||
(item) => item !== undefined
|
||||
);
|
||||
|
||||
node.js_addChildren(filteredChildren);
|
||||
}
|
||||
|
||||
async function nodeImageLookup(node, root, zip) {
|
||||
const imageAttributes = Utils.imageAttributesFromNode(node);
|
||||
if (!imageAttributes || imageAttributes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!node.attributes.js_assets) {
|
||||
node.attributes.js_assets = {};
|
||||
}
|
||||
await Promise.all(
|
||||
imageAttributes.map(async (attribute) => {
|
||||
const image = node.attributes[attribute];
|
||||
if (!image || !Utils.isString(image)) {
|
||||
return;
|
||||
}
|
||||
let img;
|
||||
if (image.endsWith(".png")) {
|
||||
img = await prepareMakiImage(node, zip, image);
|
||||
} else {
|
||||
const elementNode = Utils.findXmlElementById(node, image, root);
|
||||
if (elementNode) {
|
||||
img = await prepareMakiImage(
|
||||
elementNode,
|
||||
zip,
|
||||
elementNode.attributes.file
|
||||
);
|
||||
|
||||
const { x, y } = elementNode.attributes;
|
||||
img.x = x !== undefined ? x : 0;
|
||||
img.y = y !== undefined ? y : 0;
|
||||
} else {
|
||||
console.warn("Unable to find image:", image);
|
||||
}
|
||||
}
|
||||
node.attributes.js_assets[attribute.toLowerCase()] = img;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function applyImageLookups(root, zip) {
|
||||
await Utils.asyncTreeFlatMap(root, async (node) => {
|
||||
await nodeImageLookup(node, root, zip);
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
async function applyGroupDefs(root) {
|
||||
await Utils.asyncTreeFlatMap(root, async (node) => {
|
||||
switch (node.name) {
|
||||
case "group": {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
const groupdef = node.js_groupdefLookup(node.attributes.id);
|
||||
if (!groupdef) {
|
||||
console.warn(
|
||||
"Unable to find groupdef. Rendering null",
|
||||
node.attributes.id
|
||||
);
|
||||
return {};
|
||||
}
|
||||
node.children = groupdef.children;
|
||||
// Do we need to copy the items instead of just changing the parent?
|
||||
node.children.forEach((item) => {
|
||||
item.parent = node;
|
||||
});
|
||||
node.attributes = {
|
||||
...node.attributes,
|
||||
...groupdef.attributes,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initialize(zip, skinXml) {
|
||||
const xmlRoot = skinXml.children[0];
|
||||
await applyImageLookups(xmlRoot, zip);
|
||||
const root = new JsWinampAbstractionLayer(xmlRoot, null, undefined);
|
||||
await parseChildren(root, xmlRoot.children, zip);
|
||||
await applyGroupDefs(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
export default initialize;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { XmlTree } from "./types";
|
||||
|
||||
export default async function initializeStateTree(
|
||||
xmlTree: XmlTree
|
||||
): Promise<XmlTree> {
|
||||
return xmlTree;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
|
@ -1,79 +0,0 @@
|
|||
const COMMANDS = {
|
||||
1: { name: "push", short: "", arg: "var", in: "0", out: "1" },
|
||||
2: { name: "pop", short: "pop", in: "1", out: "0" },
|
||||
3: {
|
||||
name: "popTo",
|
||||
short: "popTo",
|
||||
arg: "var",
|
||||
in: "0",
|
||||
out: "0",
|
||||
// note in fact popTo takes one
|
||||
// argument but it is not visible to the parser because popTo
|
||||
// is always at the start of a function
|
||||
},
|
||||
8: { name: "eq", short: "==", in: "2", out: "1" },
|
||||
9: { name: "heq", short: "!=", in: "2", out: "1" },
|
||||
10: { name: "gt", short: ">", in: "2", out: "1" },
|
||||
11: { name: "gtq", short: ">=", in: "2", out: "1" },
|
||||
12: { name: "le", short: "<", in: "2", out: "1" },
|
||||
13: { name: "leq", short: "<=", in: "2", out: "1" },
|
||||
|
||||
16: { name: "jumpIf", short: "if", arg: "line", in: "1", out: "0" },
|
||||
17: { name: "jumpIfNot", arg: "line", in: "1", out: "0" },
|
||||
18: { name: "jump", arg: "line", in: "0", out: "0" },
|
||||
|
||||
24: { name: "call", arg: "objFunc", in: "0", out: "1" },
|
||||
25: { name: "callGlobal", arg: "func", in: "0", out: "1" },
|
||||
|
||||
33: {
|
||||
name: "ret",
|
||||
short: "return",
|
||||
in: "1",
|
||||
out: "0", // note: we claim that return
|
||||
// pops one argument from the stack, which ist not the full truth.
|
||||
},
|
||||
|
||||
40: { name: "complete", short: "complete", in: "0", out: "0" },
|
||||
|
||||
48: { name: "mov", short: "=", in: "2", out: "1" },
|
||||
|
||||
56: { name: "postinc", short: "++", post: 1, in: "1", out: "1" },
|
||||
57: { name: "postdec", short: "--", post: 1, in: "1", out: "1" },
|
||||
58: { name: "preinc", short: "++", in: "1", out: "1" },
|
||||
59: { name: "predec", short: "--", in: "1", out: "1" },
|
||||
|
||||
64: { name: "add", short: "+", in: "2", out: "1" },
|
||||
65: { name: "sub", short: "-", in: "2", out: "1" },
|
||||
66: { name: "mul", short: "*", in: "2", out: "1" },
|
||||
67: { name: "div", short: "/", in: "2", out: "1" },
|
||||
68: { name: "mod", short: "%", in: "2", out: "1" },
|
||||
|
||||
72: { name: "and", short: "&", in: "2", out: "1" },
|
||||
73: { name: "or", short: "|", in: "2", out: "1" },
|
||||
74: { name: "not", short: "!", in: "1", out: "1" },
|
||||
76: { name: "negative", short: "-", in: "1", out: "1" },
|
||||
|
||||
80: { name: "logAnd", short: "&&", in: "2", out: "1" },
|
||||
81: { name: "logOr", short: "||", in: "2", out: "1" },
|
||||
|
||||
// The decompiler has these next two as 90 and 91.
|
||||
88: { name: "lshift", short: "<<", in: "2", out: "1" },
|
||||
89: { name: "rshift", short: ">>", in: "2", out: "1" },
|
||||
|
||||
90: { name: "lshift", short: "<<", in: "2", out: "1" },
|
||||
91: { name: "rshift", short: ">>", in: "2", out: "1" },
|
||||
|
||||
96: { name: "new", arg: "obj", in: "0", out: "1" },
|
||||
97: { name: "delete", short: "delete", in: "1", out: "1" },
|
||||
|
||||
112: { name: "strangeCall", arg: "objFunc", in: "0", out: "1" },
|
||||
|
||||
// Mystery opcode
|
||||
// "255": { name: "MYSTERY", short: "WAT", in: "0", out: "0" },
|
||||
|
||||
300: { name: "blockStart", short: "{", in: "0", out: "0" },
|
||||
|
||||
301: { name: "blockEnd", short: "}", in: "0", out: "0" },
|
||||
};
|
||||
|
||||
module.exports = { COMMANDS };
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
import Variable from "./variable";
|
||||
import { isPromise, unimplementedWarning } from "../utils";
|
||||
|
||||
export function interpret(start, program) {
|
||||
const interpreter = new Interpreter(program);
|
||||
return interpreter.interpret(start);
|
||||
}
|
||||
|
||||
class Interpreter {
|
||||
constructor(program) {
|
||||
const { commands, methods, variables, classes } = program;
|
||||
this.commands = commands;
|
||||
this.methods = methods;
|
||||
this.variables = variables;
|
||||
this.classes = classes;
|
||||
|
||||
this.stack = [];
|
||||
this.callStack = [];
|
||||
}
|
||||
|
||||
interpret(start) {
|
||||
// Instruction Pointer
|
||||
let ip = start;
|
||||
while (ip < this.commands.length) {
|
||||
const command = this.commands[ip];
|
||||
|
||||
switch (command.opcode) {
|
||||
// push
|
||||
case 1: {
|
||||
const offsetIntoVariables = command.arg;
|
||||
this.stack.push(this.variables[offsetIntoVariables]);
|
||||
break;
|
||||
}
|
||||
// pop
|
||||
case 2: {
|
||||
this.stack.pop();
|
||||
break;
|
||||
}
|
||||
// popTo
|
||||
case 3: {
|
||||
const aValue = this.popStackValue();
|
||||
const offsetIntoVariables = command.arg;
|
||||
const toVar = this.variables[offsetIntoVariables];
|
||||
toVar.setValue(aValue);
|
||||
break;
|
||||
}
|
||||
// ==
|
||||
case 8: {
|
||||
const a = this.stack.pop();
|
||||
const b = this.stack.pop();
|
||||
switch (a.type) {
|
||||
case "INT":
|
||||
case "FLOAT":
|
||||
case "DOUBLE":
|
||||
case "BOOLEAN": {
|
||||
break;
|
||||
}
|
||||
case "STRING": {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected type: ${a}`);
|
||||
}
|
||||
let aValue = this.getValue(a);
|
||||
const bValue = this.getValue(b);
|
||||
|
||||
aValue = this.coerceTypes__DEPRECATED(a, b);
|
||||
const result = Variable.newInt(bValue === aValue);
|
||||
this.stack.push(result);
|
||||
break;
|
||||
}
|
||||
// !=
|
||||
case 9: {
|
||||
this.twoArgCoercingOperator((b, a) => b !== a);
|
||||
break;
|
||||
}
|
||||
// >
|
||||
case 10: {
|
||||
this.twoArgCoercingOperator((b, a) => b > a);
|
||||
break;
|
||||
}
|
||||
// >=
|
||||
case 11: {
|
||||
this.twoArgCoercingOperator((b, a) => b >= a);
|
||||
break;
|
||||
}
|
||||
// <
|
||||
case 12: {
|
||||
this.twoArgCoercingOperator((b, a) => b < a);
|
||||
break;
|
||||
}
|
||||
// <=
|
||||
case 13: {
|
||||
this.twoArgCoercingOperator((b, a) => b <= a);
|
||||
break;
|
||||
}
|
||||
// jumpIf
|
||||
case 16: {
|
||||
const value = this.popStackValue();
|
||||
// This seems backwards. Seems like we're doing a "jump if not"
|
||||
if (value) {
|
||||
break;
|
||||
}
|
||||
ip = command.arg - 1;
|
||||
break;
|
||||
}
|
||||
// jumpIfNot
|
||||
case 17: {
|
||||
const value = this.popStackValue();
|
||||
// This seems backwards. Same as above
|
||||
if (!value) {
|
||||
break;
|
||||
}
|
||||
ip = command.arg - 1;
|
||||
break;
|
||||
}
|
||||
// jump
|
||||
case 18: {
|
||||
ip = command.arg - 1;
|
||||
break;
|
||||
}
|
||||
// call
|
||||
// strangeCall (seems to behave just like regular call)
|
||||
case 24:
|
||||
case 112: {
|
||||
const methodOffset = command.arg;
|
||||
const method = this.methods[methodOffset];
|
||||
let methodName = method.name;
|
||||
const classesOffset = method.typeOffset;
|
||||
methodName = methodName.toLowerCase();
|
||||
|
||||
const klass = this.classes[classesOffset];
|
||||
if (!klass) {
|
||||
throw new Error("Need to add a missing class to runtime");
|
||||
}
|
||||
// This is a bit awkward. Because the variables are stored on the stack
|
||||
// before the object, we have to find the number of arguments without
|
||||
// actually having access to the object instance.
|
||||
if (!klass.prototype[methodName]) {
|
||||
throw new Error(
|
||||
`Need to add missing function (${methodName}) to ${klass.name}`
|
||||
);
|
||||
}
|
||||
let argCount = klass.prototype[methodName].length;
|
||||
|
||||
const methodArgs = [];
|
||||
while (argCount--) {
|
||||
const aValue = this.popStackValue();
|
||||
methodArgs.push(aValue);
|
||||
}
|
||||
const obj = this.popStackValue();
|
||||
let value = obj[methodName](...methodArgs);
|
||||
if (isPromise(value)) {
|
||||
throw new Error("Did not expect maki method to return promise");
|
||||
}
|
||||
if (value === null) {
|
||||
// variables[1] holds global NULL value
|
||||
value = this.variables[1];
|
||||
}
|
||||
this.stack.push(value);
|
||||
break;
|
||||
}
|
||||
// callGlobal
|
||||
case 25: {
|
||||
this.callStack.push(ip);
|
||||
const offset = command.arg;
|
||||
|
||||
ip = offset - 1; // -1 because we ++ after the switch
|
||||
break;
|
||||
}
|
||||
// return
|
||||
case 33: {
|
||||
ip = this.callStack.pop();
|
||||
// TODO: Stack protection?
|
||||
break;
|
||||
}
|
||||
// complete
|
||||
case 40: {
|
||||
// noop for now
|
||||
unimplementedWarning("OPCODE: complete");
|
||||
break;
|
||||
}
|
||||
// mov
|
||||
case 48: {
|
||||
const a = this.stack.pop();
|
||||
const b = this.stack.pop();
|
||||
let aValue = a instanceof Variable ? a.getValue() : a;
|
||||
if (b.type === "INT") {
|
||||
aValue = Math.floor(aValue);
|
||||
}
|
||||
b.setValue(aValue);
|
||||
this.stack.push(aValue);
|
||||
break;
|
||||
}
|
||||
// postinc
|
||||
case 56: {
|
||||
const a = this.stack.pop();
|
||||
const aValue = a.getValue();
|
||||
a.setValue(aValue + 1);
|
||||
this.stack.push(aValue);
|
||||
break;
|
||||
}
|
||||
// postdec
|
||||
case 57: {
|
||||
const a = this.stack.pop();
|
||||
const aValue = a.getValue();
|
||||
a.setValue(aValue - 1);
|
||||
this.stack.push(aValue);
|
||||
break;
|
||||
}
|
||||
// preinc
|
||||
case 58: {
|
||||
const a = this.stack.pop();
|
||||
const aValue = a.getValue() + 1;
|
||||
a.setValue(aValue);
|
||||
this.stack.push(aValue);
|
||||
break;
|
||||
}
|
||||
// predec
|
||||
case 59: {
|
||||
const a = this.stack.pop();
|
||||
const aValue = a.getValue() - 1;
|
||||
a.setValue(aValue);
|
||||
this.stack.push(aValue);
|
||||
break;
|
||||
}
|
||||
// + (add)
|
||||
case 64: {
|
||||
this.twoArgOperator((b, a) => b + a);
|
||||
break;
|
||||
}
|
||||
// - (subtract)
|
||||
case 65: {
|
||||
this.twoArgOperator((b, a) => b - a);
|
||||
break;
|
||||
}
|
||||
// * (multiply)
|
||||
case 66: {
|
||||
this.twoArgOperator((b, a) => b * a);
|
||||
break;
|
||||
}
|
||||
// / (divide)
|
||||
case 67: {
|
||||
this.twoArgOperator((b, a) => b / a);
|
||||
break;
|
||||
}
|
||||
// % (mod)
|
||||
case 68: {
|
||||
const a = this.stack.pop();
|
||||
const b = this.stack.pop();
|
||||
const aValue = a instanceof Variable ? a.getValue() : a;
|
||||
let bValue = b instanceof Variable ? b.getValue() : b;
|
||||
// Need to coerce LHS if not int, RHS is always int (enforced by compiler)
|
||||
if (b.type === "FLOAT" || b.type === "DOUBLE") {
|
||||
bValue = Math.floor(bValue);
|
||||
}
|
||||
this.stack.push(bValue % aValue);
|
||||
break;
|
||||
}
|
||||
// & (binary and)
|
||||
case 72: {
|
||||
this.twoArgOperator((b, a) => b & a);
|
||||
break;
|
||||
}
|
||||
// | (binary or)
|
||||
case 73: {
|
||||
this.twoArgOperator((b, a) => b | a);
|
||||
break;
|
||||
}
|
||||
// ! (not)
|
||||
case 74: {
|
||||
const aValue = this.popStackValue();
|
||||
this.stack.push(aValue ? 0 : 1);
|
||||
break;
|
||||
}
|
||||
// - (negative)
|
||||
case 76: {
|
||||
const aValue = this.popStackValue();
|
||||
this.stack.push(-aValue);
|
||||
break;
|
||||
}
|
||||
// logAnd (&&)
|
||||
case 80: {
|
||||
this.twoArgOperator((b, a) => b && a);
|
||||
break;
|
||||
}
|
||||
// logOr ||
|
||||
case 81: {
|
||||
this.twoArgOperator((b, a) => b || a);
|
||||
break;
|
||||
}
|
||||
// <<
|
||||
case 88: {
|
||||
this.twoArgOperator((b, a) => b << a);
|
||||
break;
|
||||
}
|
||||
// >>
|
||||
case 89: {
|
||||
this.twoArgOperator((b, a) => b >> a);
|
||||
break;
|
||||
}
|
||||
// new
|
||||
case 96: {
|
||||
const classesOffset = command.arg;
|
||||
const Klass = this.classes[classesOffset];
|
||||
const system = this.variables[0].getValue();
|
||||
const klassInst = new Klass(null, system.getscriptgroup());
|
||||
this.stack.push(klassInst);
|
||||
break;
|
||||
}
|
||||
// delete
|
||||
case 97: {
|
||||
const aValue = this.popStackValue();
|
||||
aValue.js_delete();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled opcode ${command.opcode}`);
|
||||
}
|
||||
|
||||
ip++;
|
||||
}
|
||||
}
|
||||
|
||||
getValue(v) {
|
||||
return v instanceof Variable ? v.getValue() : v;
|
||||
}
|
||||
|
||||
popStackValue() {
|
||||
const v = this.stack.pop();
|
||||
return this.getValue(v);
|
||||
}
|
||||
|
||||
twoArgCoercingOperator(operator) {
|
||||
const a = this.stack.pop();
|
||||
const b = this.stack.pop();
|
||||
let aValue = this.getValue(a);
|
||||
const bValue = this.getValue(b);
|
||||
|
||||
aValue = this.coerceTypes__DEPRECATED(a, b);
|
||||
this.stack.push(operator(bValue, aValue));
|
||||
}
|
||||
|
||||
twoArgOperator(operator) {
|
||||
const aValue = this.popStackValue();
|
||||
const bValue = this.popStackValue();
|
||||
|
||||
this.stack.push(operator(bValue, aValue));
|
||||
}
|
||||
|
||||
coerceTypes__DEPRECATED(var1, var2) {
|
||||
if (var2.type === "INT") {
|
||||
if (var1.type === "FLOAT" || var1.type === "DOUBLE") {
|
||||
return Math.floor(this.getValue(var1));
|
||||
}
|
||||
}
|
||||
|
||||
return this.getValue(var1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
{
|
||||
"593DBA22D0774976B952F4713655400B": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getItem",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getItemByGuid",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_guid"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "newItem",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"item_name"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"item_guid"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "Config"
|
||||
},
|
||||
"D40302823AAB4d87878D12326FADFCD5": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "ConfigAttribute",
|
||||
"name": "getAttribute",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "ConfigAttribute",
|
||||
"name": "newAttribute",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"default_value"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getGuid",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"attr_name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getName",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "ConfigItem"
|
||||
},
|
||||
"24DEC283B76E4a368CCC9E24C46B6C73": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "",
|
||||
"name": "setData",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"value"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getData",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "onDataChanged",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "ConfigItem",
|
||||
"name": "getParentItem",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getAttributeName",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "ConfigAttribute"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
{
|
||||
"345BEEBC0229492190BE6CB6A49A79D9": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumTracks",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getCurrentIndex",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumSelectedTracks",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNextSelectedTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"i"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showCurrentlyPlayingTrack",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "enqueueFile",
|
||||
"parameters": [
|
||||
[
|
||||
"string",
|
||||
"file"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "clear",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "removeTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "swapTracks",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item1"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"item2"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveUp",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveDown",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "moveTo",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"pos"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "playTrack",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getRating",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "setRating",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"int",
|
||||
"rating"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getTitle",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getLength",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getMetaData",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"metadatastring"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getFileName",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "onPleditModified",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"name": "PlEdit"
|
||||
},
|
||||
"61A7ABAD7D7941f6B1D0E1808603A4F4": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "int",
|
||||
"name": "getNumItems",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "getItemName",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "showCurrentlyPlayingEntry",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "refresh",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "renameItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"name"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "enqueueItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "playItem",
|
||||
"parameters": [
|
||||
[
|
||||
"int",
|
||||
"item"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "PlDir"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,63 +0,0 @@
|
|||
const std = require("./std.json");
|
||||
|
||||
const NAME_TO_DEF = {};
|
||||
|
||||
Object.values(std).forEach((value) => {
|
||||
NAME_TO_DEF[value.name] = value;
|
||||
});
|
||||
|
||||
function getMethod(className, methodName) {
|
||||
return NAME_TO_DEF[className].functions.find(({ name }) => {
|
||||
return name === methodName;
|
||||
});
|
||||
}
|
||||
|
||||
// Between myself and the author of the decompiler, a number of manual tweaks
|
||||
// have been made to our current object definitions. This function recreates
|
||||
// those tweaks so we can have an apples to apples comparison.
|
||||
|
||||
/*
|
||||
* From object.js
|
||||
*
|
||||
* > The std.mi has this set as void, but we checked in Winamp and confirmed it
|
||||
* > returns 0/1
|
||||
*/
|
||||
getMethod("Timer", "isRunning").result = "boolean";
|
||||
|
||||
/*
|
||||
* From Object.pm
|
||||
*
|
||||
* > note, std.mi does not have this parameter!
|
||||
*/
|
||||
getMethod("ToggleButton", "onToggle").parameters[0][1] = "onoff";
|
||||
|
||||
// Some methods are not compatible with the type signature of their parent class
|
||||
getMethod("GuiTree", "onChar").parameters[0][0] = "string";
|
||||
getMethod("GuiList", "onSetVisible").parameters[0][0] = "boolean";
|
||||
|
||||
// I'm not sure how to get these to match
|
||||
getMethod("Wac", "onNotify").parameters = getMethod(
|
||||
"Object",
|
||||
"onNotify"
|
||||
).parameters;
|
||||
getMethod("Wac", "onNotify").result = "int";
|
||||
|
||||
/*
|
||||
|
||||
Here's the error we get without that patch:
|
||||
|
||||
__generated__/makiInterfaces.ts:254:18 - error TS2430: Interface 'Wac' incorrectly extends interface 'MakiObject'.
|
||||
Types of property 'onnotify' are incompatible.
|
||||
Type '(notifstr: string, a: number, b: number) => void' is not assignable to type '(command: string, param: string, a: number, b: number) => number'.
|
||||
Types of parameters 'a' and 'param' are incompatible.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
|
||||
254 export interface Wac extends MakiObject {
|
||||
~~~
|
||||
|
||||
|
||||
Found 1 error.
|
||||
|
||||
*/
|
||||
|
||||
module.exports = std;
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
const stdPatched = require("./objectData/stdPatched");
|
||||
const pldir = require("./objectData/pldir.json");
|
||||
const config = require("./objectData/config.json");
|
||||
|
||||
const objects = { ...stdPatched, ...pldir, ...config };
|
||||
|
||||
// TODO: We could probably just fix the keys used in this file to already be normalized
|
||||
// We might even want to normalize the to match the formatting we get out the file. That could
|
||||
// avoid the awkward regex inside `getClass()`.
|
||||
const normalizedObjects = {};
|
||||
Object.keys(objects).forEach((key) => {
|
||||
normalizedObjects[key.toLowerCase()] = objects[key];
|
||||
});
|
||||
|
||||
const objectsByName = {};
|
||||
Object.values(objects).forEach((object) => {
|
||||
objectsByName[object.name] = object;
|
||||
});
|
||||
|
||||
Object.values(normalizedObjects).forEach((object) => {
|
||||
const parentClass = objectsByName[object.parent];
|
||||
if (parentClass == null) {
|
||||
if (object.parent === "@{00000000-0000-0000-0000-000000000000}@") {
|
||||
} else {
|
||||
throw new Error(`Could not find parent class named ${object.parent}`);
|
||||
}
|
||||
}
|
||||
object.parentClass = parentClass;
|
||||
});
|
||||
|
||||
function getFormattedId(id) {
|
||||
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
|
||||
const formattedId = id.replace(
|
||||
/(........)(....)(....)(..)(..)(..)(..)(..)(..)(..)(..)/,
|
||||
"$1$3$2$7$6$5$4$11$10$9$8"
|
||||
);
|
||||
return formattedId.toLowerCase();
|
||||
}
|
||||
|
||||
function getClass(id) {
|
||||
return normalizedObjects[getFormattedId(id)];
|
||||
}
|
||||
|
||||
function getObjectFunction(klass, functionName) {
|
||||
const method = klass.functions.find((func) => {
|
||||
// TODO: This could probably be normalized at load time, or evern sooner.
|
||||
return func.name.toLowerCase() === functionName.toLowerCase();
|
||||
});
|
||||
if (method != null) {
|
||||
return method;
|
||||
}
|
||||
if (klass.parentClass == null) {
|
||||
throw new Error(`Could not find method ${functionName} on ${klass.name}.`);
|
||||
}
|
||||
return getObjectFunction(klass.parentClass, functionName);
|
||||
}
|
||||
|
||||
function getFunctionObject(klass, functionName) {
|
||||
const method = klass.functions.find((func) => {
|
||||
// TODO: This could probably be normalized at load time, or evern sooner.
|
||||
return func.name.toLowerCase() === functionName.toLowerCase();
|
||||
});
|
||||
if (method != null) {
|
||||
return klass;
|
||||
}
|
||||
if (klass.parentClass == null) {
|
||||
throw new Error(`Could not find method ${functionName} on ${klass.name}.`);
|
||||
}
|
||||
return getFunctionObject(klass.parentClass, functionName);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
objects,
|
||||
getFormattedId,
|
||||
getClass,
|
||||
getObjectFunction,
|
||||
getFunctionObject,
|
||||
};
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
import { COMMANDS } from "./constants";
|
||||
import Variable from "./variable";
|
||||
const MAGIC = "FG";
|
||||
|
||||
const PRIMITIVE_TYPES = {
|
||||
5: "BOOLEAN",
|
||||
2: "INT",
|
||||
3: "FLOAT",
|
||||
4: "DOUBLE",
|
||||
6: "STRING",
|
||||
};
|
||||
|
||||
// TODO: Don't depend upon COMMANDS
|
||||
function opcodeToArgType(opcode) {
|
||||
const command = COMMANDS[opcode];
|
||||
if (command == null) {
|
||||
throw new Error(`Unknown opcode ${opcode}`);
|
||||
}
|
||||
|
||||
switch (command.arg) {
|
||||
case "func":
|
||||
case "line":
|
||||
return "COMMAND_OFFSET";
|
||||
case "var":
|
||||
case "objFunc":
|
||||
case "obj":
|
||||
return "VARIABLE_OFFSET";
|
||||
default:
|
||||
return "NONE";
|
||||
}
|
||||
}
|
||||
|
||||
// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
|
||||
// file. When we want to run in the browser, we can refactor this class to use a
|
||||
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
|
||||
class MakiFile {
|
||||
constructor(data) {
|
||||
this._arr = new Uint8Array(data);
|
||||
this._i = 0;
|
||||
}
|
||||
|
||||
readInt32LE() {
|
||||
const offset = this._i >>> 0;
|
||||
this._i += 4;
|
||||
|
||||
return (
|
||||
this._arr[offset] |
|
||||
(this._arr[offset + 1] << 8) |
|
||||
(this._arr[offset + 2] << 16) |
|
||||
(this._arr[offset + 3] << 24)
|
||||
);
|
||||
}
|
||||
|
||||
readUInt32LE() {
|
||||
const int = this.peekUInt32LE();
|
||||
this._i += 4;
|
||||
return int;
|
||||
}
|
||||
|
||||
peekUInt32LE() {
|
||||
const offset = this._i >>> 0;
|
||||
|
||||
return (
|
||||
(this._arr[offset] |
|
||||
(this._arr[offset + 1] << 8) |
|
||||
(this._arr[offset + 2] << 16)) +
|
||||
this._arr[offset + 3] * 0x1000000
|
||||
);
|
||||
}
|
||||
|
||||
readUInt16LE() {
|
||||
const offset = this._i >>> 0;
|
||||
this._i += 2;
|
||||
return this._arr[offset] | (this._arr[offset + 1] << 8);
|
||||
}
|
||||
|
||||
readUInt8() {
|
||||
const int = this._arr[this._i];
|
||||
this._i++;
|
||||
return int;
|
||||
}
|
||||
|
||||
readStringOfLength(length) {
|
||||
let ret = "";
|
||||
const end = Math.min(this._arr.length, this._i + length);
|
||||
|
||||
for (let i = this._i; i < end; ++i) {
|
||||
ret += String.fromCharCode(this._arr[i]);
|
||||
}
|
||||
this._i += length;
|
||||
return ret;
|
||||
}
|
||||
|
||||
readString() {
|
||||
return this.readStringOfLength(this.readUInt16LE());
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
return this._i;
|
||||
}
|
||||
}
|
||||
|
||||
function readMagic(makiFile) {
|
||||
const magic = makiFile.readStringOfLength(MAGIC.length);
|
||||
if (magic !== MAGIC) {
|
||||
throw new Error(
|
||||
`Magic "${magic}" does not mach "${MAGIC}". Is this a maki file?`
|
||||
);
|
||||
}
|
||||
return magic;
|
||||
}
|
||||
|
||||
function readVersion(makiFile) {
|
||||
// No idea what we're actually expecting here.
|
||||
return makiFile.readUInt16LE();
|
||||
}
|
||||
|
||||
function readClasses(makiFile) {
|
||||
let count = makiFile.readUInt32LE();
|
||||
const classes = [];
|
||||
while (count--) {
|
||||
let identifier = "";
|
||||
let chunks = 4;
|
||||
while (chunks--) {
|
||||
identifier += makiFile.readUInt32LE().toString(16).padStart(8, "0");
|
||||
}
|
||||
classes.push(identifier);
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
function readMethods(makiFile) {
|
||||
let count = makiFile.readUInt32LE();
|
||||
const methods = [];
|
||||
while (count--) {
|
||||
const classCode = makiFile.readUInt16LE();
|
||||
// Offset into our parsed types
|
||||
const typeOffset = classCode & 0xff;
|
||||
// This is probably the second half of a uint32
|
||||
makiFile.readUInt16LE();
|
||||
const name = makiFile.readString();
|
||||
methods.push({ name, typeOffset });
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
function readVariables({ makiFile, classes }) {
|
||||
let count = makiFile.readUInt32LE();
|
||||
const variables = [];
|
||||
while (count--) {
|
||||
const typeOffset = makiFile.readUInt8();
|
||||
const object = makiFile.readUInt8();
|
||||
const subClass = makiFile.readUInt16LE();
|
||||
const uinit1 = makiFile.readUInt16LE();
|
||||
const uinit2 = makiFile.readUInt16LE();
|
||||
makiFile.readUInt16LE(); // uinit3
|
||||
makiFile.readUInt16LE(); //uinit4
|
||||
const global = makiFile.readUInt8();
|
||||
makiFile.readUInt8(); // system
|
||||
|
||||
if (subClass) {
|
||||
const variable = variables[typeOffset];
|
||||
if (variable == null) {
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
variables.push(
|
||||
new Variable({ type: variable, typeName: "SUBCLASS", global: !!global })
|
||||
);
|
||||
} else if (object) {
|
||||
const klass = classes[typeOffset];
|
||||
if (klass == null) {
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
variables.push(
|
||||
new Variable({ type: klass, typeName: "OBJECT", global: !!global })
|
||||
);
|
||||
} else {
|
||||
const typeName = PRIMITIVE_TYPES[typeOffset];
|
||||
if (typeName == null) {
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
let value = null;
|
||||
|
||||
switch (typeName) {
|
||||
// BOOLEAN
|
||||
case PRIMITIVE_TYPES[5]:
|
||||
value = uinit1;
|
||||
break;
|
||||
// INT
|
||||
case PRIMITIVE_TYPES[2]:
|
||||
value = uinit1;
|
||||
break;
|
||||
case PRIMITIVE_TYPES[3]:
|
||||
case PRIMITIVE_TYPES[4]:
|
||||
const exponent = (uinit2 & 0xff80) >> 7;
|
||||
const mantisse = ((0x80 | (uinit2 & 0x7f)) << 16) | uinit1;
|
||||
value = mantisse * 2.0 ** (exponent - 0x96);
|
||||
break;
|
||||
case PRIMITIVE_TYPES[6]:
|
||||
// This will likely get set by constants later on.
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid primitive type");
|
||||
}
|
||||
const variable = new Variable({
|
||||
type: typeName,
|
||||
typeName,
|
||||
global: !!global,
|
||||
});
|
||||
variable.setValue(value);
|
||||
variables.push(variable);
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
function readConstants({ makiFile, variables }) {
|
||||
let count = makiFile.readUInt32LE();
|
||||
while (count--) {
|
||||
const i = makiFile.readUInt32LE();
|
||||
const variable = variables[i];
|
||||
// TODO: Assert this is of type string.
|
||||
const value = makiFile.readString();
|
||||
// TODO: Don't mutate
|
||||
variable.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
function readBindings(makiFile) {
|
||||
let count = makiFile.readUInt32LE();
|
||||
const bindings = [];
|
||||
while (count--) {
|
||||
const variableOffset = makiFile.readUInt32LE();
|
||||
const methodOffset = makiFile.readUInt32LE();
|
||||
const binaryOffset = makiFile.readUInt32LE();
|
||||
bindings.push({ variableOffset, binaryOffset, methodOffset });
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
function decodeCode({ makiFile }) {
|
||||
const length = makiFile.readUInt32LE();
|
||||
const start = makiFile.getPosition();
|
||||
|
||||
const commands = [];
|
||||
while (makiFile.getPosition() < start + length) {
|
||||
commands.push(parseComand({ start, makiFile, length }));
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
// TODO: Refactor this to consume bytes directly off the end of MakiFile
|
||||
function parseComand({ start, makiFile, length }) {
|
||||
const pos = makiFile.getPosition() - start;
|
||||
const opcode = makiFile.readUInt8();
|
||||
const command = {
|
||||
offset: pos,
|
||||
start,
|
||||
opcode,
|
||||
arg: null,
|
||||
argType: opcodeToArgType(opcode),
|
||||
};
|
||||
|
||||
if (command.argType === "NONE") {
|
||||
return command;
|
||||
}
|
||||
|
||||
let arg = null;
|
||||
switch (command.argType) {
|
||||
case "COMMAND_OFFSET":
|
||||
// Note in the perl code here: "todo, something strange going on here..."
|
||||
arg = makiFile.readInt32LE() + 5 + pos;
|
||||
break;
|
||||
case "VARIABLE_OFFSET":
|
||||
arg = makiFile.readUInt32LE();
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid argType");
|
||||
}
|
||||
|
||||
command.arg = arg;
|
||||
|
||||
// From perl: look forward for a stack protection block
|
||||
// (why do I have to look FORWARD. stupid nullsoft)
|
||||
if (
|
||||
// Is there another UInt32 to read?
|
||||
length > pos + 5 + 4 &&
|
||||
makiFile.peekUInt32LE() >= 0xffff0000 &&
|
||||
makiFile.peekUInt32LE() <= 0xffff000f
|
||||
) {
|
||||
command.foo = true;
|
||||
command.stackProtection = makiFile.readUInt32LE();
|
||||
}
|
||||
|
||||
// TODO: What even is this?
|
||||
if (opcode === 112 /* strangeCall */) {
|
||||
makiFile.readUInt8();
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
function parse(data) {
|
||||
const makiFile = new MakiFile(data);
|
||||
|
||||
const magic = readMagic(makiFile);
|
||||
// TODO: What format is this? Does it even change between compiler versions?
|
||||
// Maybe it's the std.mi version?
|
||||
const version = readVersion(makiFile);
|
||||
// Not sure what we are skipping over here. Just some UInt 32.
|
||||
// Maybe it's additional version info?
|
||||
const extraVersion = makiFile.readUInt32LE();
|
||||
const classes = readClasses(makiFile);
|
||||
const methods = readMethods(makiFile);
|
||||
const variables = readVariables({ makiFile, classes });
|
||||
readConstants({ makiFile, variables });
|
||||
const bindings = readBindings(makiFile);
|
||||
const commands = decodeCode({ makiFile });
|
||||
|
||||
// TODO: Assert that we are at the end of the maki file
|
||||
|
||||
// Map binary offsets to command indexes.
|
||||
// Some bindings/functions ask us to jump to a place in the binary data and
|
||||
// start executing. However, we want to do all the parsing up front, and just
|
||||
// return a list of commands. This map allows anything that mentions a binary
|
||||
// offset to find the command they should jump to.
|
||||
const offsetToCommand = {};
|
||||
commands.forEach((command, i) => {
|
||||
if (command.offset != null) {
|
||||
offsetToCommand[command.offset] = i;
|
||||
}
|
||||
});
|
||||
|
||||
const resolvedBindings = bindings.map((binding) => {
|
||||
return Object.assign({}, binding, {
|
||||
commandOffset: offsetToCommand[binding.binaryOffset],
|
||||
binaryOffset: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
const resolvedCommands = commands.map((command) => {
|
||||
if (command.argType === "COMMAND_OFFSET") {
|
||||
return Object.assign({}, command, { arg: offsetToCommand[command.arg] });
|
||||
}
|
||||
return command;
|
||||
});
|
||||
return {
|
||||
magic,
|
||||
classes,
|
||||
methods,
|
||||
variables,
|
||||
bindings: resolvedBindings,
|
||||
commands: resolvedCommands,
|
||||
version,
|
||||
extraVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export default parse;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import Variable from "./variable";
|
||||
import runtime from "../runtime";
|
||||
|
||||
// Debug utility to pretty print a value/variable
|
||||
function printValue(value) {
|
||||
if (!(value instanceof Variable)) {
|
||||
return value;
|
||||
}
|
||||
const variable = value;
|
||||
let type = "UNKOWN";
|
||||
switch (variable.typeName) {
|
||||
case "OBJECT":
|
||||
const obj = runtime[variable.type];
|
||||
if (obj == null) {
|
||||
type = "Unknown object";
|
||||
} else {
|
||||
type = obj.getclassname();
|
||||
}
|
||||
break;
|
||||
case "STRING":
|
||||
const str = variable.getValue();
|
||||
type = `STRING(${str})`;
|
||||
break;
|
||||
case "INT":
|
||||
type = "INT";
|
||||
break;
|
||||
case "FLOAT":
|
||||
type = "FLOAT";
|
||||
break;
|
||||
case "DOUBLE":
|
||||
type = "DOUBLE";
|
||||
break;
|
||||
case "BOOLEAN":
|
||||
type = "BOOLEAN";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown variable type ${variable.typeName}`);
|
||||
}
|
||||
return `Variable(${type})`;
|
||||
}
|
||||
|
||||
function printCommand({ i, command, stack, variables }) {
|
||||
console.log(
|
||||
`${i} (${command.start} + ${command.offset})`,
|
||||
command.command.name.toUpperCase(),
|
||||
command.opcode,
|
||||
printValue(variables[command.arg])
|
||||
);
|
||||
stack.forEach((value, j) => {
|
||||
const name = printValue(value, { runtime });
|
||||
console.log(" ", j + 1, name);
|
||||
});
|
||||
}
|
||||
|
||||
export default { printCommand };
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Maki Interpreter
|
||||
|
||||
This interpreter is part of our broader experiment in getting "modern" Winamp skins working in the browser. For more information about the project read the [Modern Winamp Skins Readme](../../README.md).
|
||||
|
||||
Maki is a compiled language, so this interpreter parses/evaluates the compiled byte code. Most of the hard work of figuring out how to write the parser has already been done as part of Ralf Engels' [Maki Decompiler](http://www.rengels.de/maki_decompiler/).
|
||||
|
||||
## Architecture
|
||||
|
||||
- [Parser](./parser.js): Given a Blob containing the binary data of a `.maki` file, returns a JSON serializeable representation of the script.
|
||||
- [Runtime](../runtime/index.ts): A JavaScript object mapping class' unique ids to JavaScript implementations of those classes.
|
||||
- [Interpreter](./interpreter.js): Given a parsed `.maki` file, and a command offset, exectues the `.maki` script starting at that command, and returing the value returned from executing that function.
|
||||
- [Virtual Machine](./virtualMachine.js): This ties all of the above pieces together. Given a Buffer containing a `.maki` script, a Runtime, and an instance of `System`, it parses the `.maki` file, and then binds the runtime into the parsed `.maki` program. This consists of populating the `System` variable, and binding the class ids defined in the script to the actual JavaScript implementations of those classes. Finally it kicks off execution by triggering `System.onScriptLoaded()`.
|
||||
|
||||
## Structure of a `.maki` file
|
||||
|
||||
The bytecode contained in a .maki file takes the following form (I think. I'm still trying to grock it). These are my notes trying to write down what I understand so far. Most of this was infered from reading the
|
||||
|
||||
1. The "magic" string "FG". This might be some kind attempt to validate that this is realy a `.maki` file?
|
||||
2. A version number (which we currently ignore)
|
||||
3. Some 32 bit something. We ignore this.
|
||||
4. Types
|
||||
1. A 32 bit number defines how many types there are.
|
||||
2. Each type consists of four 32 bit numbers.
|
||||
5. Function names
|
||||
1. A 32 bit number defines how many function names there are.
|
||||
2. Each funtion name consists of a 16 bit class code
|
||||
3. A 16 bit "dummy" (not sure what this is) and a name.
|
||||
4. The name is defined by a 16 bit number showing how long the name is, followed by that many ascii bytes.
|
||||
6. Variables
|
||||
1. A 32 bit number defines how many variables there are.
|
||||
2. Each variable consists of:
|
||||
3. A byte of what "type" it is.
|
||||
4. A byte of what object it refers to.
|
||||
5. 16 bits of what subclass it refers to.
|
||||
6. Four uinits (what are those?) each 16 bits long.
|
||||
7. A byte representing "global" (what? Maybe a boolean?)
|
||||
8. A byte representing "syste" (What? Maybe a boolean?)
|
||||
7. Constants
|
||||
1. A 32 bit number defines how many constants there are.
|
||||
2. Each constant consists of:
|
||||
3. A 32 bit number representing its number (is this just an ID?)
|
||||
4. The value is defined by a 16 bit number showing how long the name is, followed by that many ascii bytes.
|
||||
8. Functions
|
||||
1. A 32 bit number defines how many functions there are.
|
||||
2. Each constant consists of:
|
||||
3. A 32 bit number representing its variable number (is this just an ID?)
|
||||
4. A 32 bit number representing its function number (is this just an ID?)
|
||||
5. A 32 bit number representing its offset (offset into what?)
|
||||
9. Function Code
|
||||
1. A 32 bit number defines how many commands there are.
|
||||
2. Each command consists of:
|
||||
3. A byte representing that command's opcode
|
||||
4.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
const objectData = require("../objectData/stdPatched");
|
||||
|
||||
const makiObjectNames = new Set(
|
||||
Object.values(objectData).map((obj) => obj.name)
|
||||
);
|
||||
|
||||
const BASE_OBJECT = "@{00000000-0000-0000-0000-000000000000}@";
|
||||
|
||||
function mapType(makiType) {
|
||||
switch (makiType) {
|
||||
case "Object":
|
||||
return "IMakiObject";
|
||||
case "Map":
|
||||
return "IMakiMap";
|
||||
}
|
||||
if (makiObjectNames.has(makiType)) {
|
||||
return `I${makiType}`;
|
||||
}
|
||||
|
||||
const lowerCaseType = makiType.toLowerCase();
|
||||
switch (lowerCaseType) {
|
||||
case "":
|
||||
return "void";
|
||||
case "string":
|
||||
return "string";
|
||||
case "int":
|
||||
case "float":
|
||||
case "double":
|
||||
return "number";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "any":
|
||||
return "any";
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled Maki type "${makiType}"`);
|
||||
}
|
||||
|
||||
function argumentTypeForParameter(param) {
|
||||
const [type, name] = param;
|
||||
return `${name}: ${mapType(type)}`;
|
||||
}
|
||||
|
||||
function methodTypeForFunction(func) {
|
||||
const args = func.parameters.map(argumentTypeForParameter).join(", ");
|
||||
return `${func.name.toLowerCase()}(${args}): ${mapType(func.result)};`;
|
||||
}
|
||||
|
||||
function interfaceForObject(object) {
|
||||
const methods = object.functions.map(methodTypeForFunction);
|
||||
let ext = "";
|
||||
if (object.parent && object.parent !== BASE_OBJECT) {
|
||||
const parentType = mapType(object.parent);
|
||||
ext = ` extends ${parentType}`;
|
||||
}
|
||||
return `export interface ${mapType(object.name)}${ext} {
|
||||
${methods.map((method) => ` ${method}`).join("\n")}
|
||||
}`;
|
||||
}
|
||||
|
||||
const interfaceFile = Object.values(objectData)
|
||||
.map(interfaceForObject)
|
||||
.join("\n\n");
|
||||
|
||||
console.log(interfaceFile);
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const JSZip = require("jszip");
|
||||
const Utils = require("../../utils");
|
||||
const glob = require("glob");
|
||||
|
||||
function findWals(parentDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob("**/cPro2_Aluminum_1_1.wal", { cwd: parentDir }, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(files.map((filePath) => path.join(parentDir, filePath)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sumCountObjects(obj1, obj2) {
|
||||
return Object.keys(obj2).reduce((summaryObj, key) => {
|
||||
if (summaryObj[key] == null) {
|
||||
summaryObj[key] = obj2[key];
|
||||
} else {
|
||||
summaryObj[key] += obj2[key];
|
||||
}
|
||||
return summaryObj;
|
||||
}, Object.assign({}, obj1));
|
||||
}
|
||||
|
||||
async function getAttributeDataFromWal(absolutePath) {
|
||||
const buffer = fs.readFileSync(absolutePath);
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const skinXml = await Utils.readXml(zip, "skin.xml");
|
||||
if (skinXml == null) {
|
||||
return [];
|
||||
}
|
||||
const rawXmlTree = await Utils.inlineIncludes(skinXml, zip);
|
||||
|
||||
const nodeTypes = [];
|
||||
Utils.mapTreeBreadth(rawXmlTree, (node) => {
|
||||
if (node.type === "element") {
|
||||
nodeTypes.push({
|
||||
name: node.name.toLowerCase(),
|
||||
attributes:
|
||||
node.attributes == null
|
||||
? []
|
||||
: Object.keys(node.attributes).map((attr) => attr.toLowerCase()),
|
||||
});
|
||||
}
|
||||
return node;
|
||||
});
|
||||
return nodeTypes;
|
||||
}
|
||||
|
||||
async function main(parentDir) {
|
||||
const errors = [];
|
||||
const paths = await findWals(parentDir);
|
||||
const attributeData = [];
|
||||
|
||||
// Originally we moved to doing one skin at a time because we thougth it was
|
||||
// causing memory issues, now we know it's not, but it's still faster to do
|
||||
// one at a time for some reason. ¯\_(ツ)_/¯
|
||||
for (const walPath of paths) {
|
||||
try {
|
||||
console.error(`Working on ${walPath}`);
|
||||
attributeData.push(...(await getAttributeDataFromWal(walPath)));
|
||||
} catch (e) {
|
||||
const errorLine = e.toString().split("\n")[0];
|
||||
errors.push({ [errorLine]: 1 });
|
||||
// TODO: Investigate these.
|
||||
console.error(`Error getting call data from ${walPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
const summary = attributeData.reduce((sum, attrs) => {
|
||||
if (sum[attrs.name] == null) {
|
||||
sum[attrs.name] = {
|
||||
count: 0,
|
||||
attributes: {},
|
||||
};
|
||||
}
|
||||
sum[attrs.name].count++;
|
||||
const nodeAttrs = sum[attrs.name].attributes;
|
||||
attrs.attributes.forEach((attr) => {
|
||||
if (nodeAttrs[attr] == null) {
|
||||
nodeAttrs[attr] = 1;
|
||||
} else {
|
||||
nodeAttrs[attr]++;
|
||||
}
|
||||
});
|
||||
return sum;
|
||||
}, {});
|
||||
|
||||
if (errors.length) {
|
||||
console.error(JSON.stringify(errors.reduce(sumCountObjects, {}), null, 2));
|
||||
}
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
}
|
||||
|
||||
main("/Volumes/Mobile Backup/skins/skins/random/Winamp Skins/Skins/");
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const parse = require("../parser").default;
|
||||
const JSZip = require("jszip");
|
||||
const { getClass, getFunctionObject, getFormattedId } = require("../objects");
|
||||
const glob = require("glob");
|
||||
|
||||
const CALL_OPCODES = new Set([24, 112]);
|
||||
const SKIN_FILENAME_BLACKLIST = new Set(["Fuzzy_Muffins.wal"]);
|
||||
|
||||
function findWals(parentDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob("**/*.wal", { cwd: parentDir }, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(files.map((filePath) => path.join(parentDir, filePath)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sumCountObjects(obj1, obj2) {
|
||||
return Object.keys(obj2).reduce((summaryObj, key) => {
|
||||
if (summaryObj[key] == null) {
|
||||
summaryObj[key] = obj2[key];
|
||||
} else {
|
||||
summaryObj[key] += obj2[key];
|
||||
}
|
||||
return summaryObj;
|
||||
}, Object.assign({}, obj1));
|
||||
}
|
||||
|
||||
async function getCallCountsFromWal(absolutePath) {
|
||||
const buffer = fs.readFileSync(absolutePath);
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const files = zip.file(/\.maki$/);
|
||||
const buffers = await Promise.all(
|
||||
files.map((file) => file.async("nodebuffer"))
|
||||
);
|
||||
return buffers.map(getCallCountsFromMaki).reduce(sumCountObjects, {});
|
||||
}
|
||||
|
||||
function getCallCountsFromMaki(buffer) {
|
||||
const maki = parse(buffer);
|
||||
return maki.commands
|
||||
.filter((command) => CALL_OPCODES.has(command.opcode))
|
||||
.map((command) => {
|
||||
const method = maki.methods[command.arg];
|
||||
const classId = maki.classes[method.typeOffset];
|
||||
const klass = getClass(classId);
|
||||
if (klass == null) {
|
||||
throw new Error(
|
||||
`Unknown class ID: ${getFormattedId(classId)} aka ${classId} (raw)`
|
||||
);
|
||||
}
|
||||
const parentClass = getFunctionObject(klass, method.name);
|
||||
return `${parentClass.name}.${method.name.toLowerCase()}`;
|
||||
})
|
||||
.map((methodName) => ({ [methodName]: 1 }))
|
||||
.reduce(sumCountObjects, {});
|
||||
}
|
||||
|
||||
function setObjectValuesToOne(obj) {
|
||||
const newObj = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] != null) {
|
||||
newObj[key] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
async function main(parentDir) {
|
||||
const errors = [];
|
||||
const paths = await findWals(parentDir);
|
||||
const callCounts = [];
|
||||
|
||||
// Originally we moved to doing one skin at a time because we thougth it was
|
||||
// causing memory issues, now we know it's not, but it's still faster to do
|
||||
// one at a time for some reason. ¯\_(ツ)_/¯
|
||||
for (const walPath of paths) {
|
||||
try {
|
||||
const fileName = path.basename(walPath);
|
||||
if (SKIN_FILENAME_BLACKLIST.has(fileName)) {
|
||||
continue;
|
||||
}
|
||||
console.error(`Working on ${walPath}`);
|
||||
callCounts.push(await getCallCountsFromWal(walPath));
|
||||
} catch (e) {
|
||||
const errorLine = e.toString().split("\n")[0];
|
||||
errors.push({ [errorLine]: 1 });
|
||||
// TODO: Investigate these.
|
||||
console.error(`Error getting calld data from ${walPath}`, e);
|
||||
}
|
||||
}
|
||||
const totalCalls = callCounts.reduce(sumCountObjects);
|
||||
const foundInSkins = callCounts
|
||||
.map(setObjectValuesToOne)
|
||||
.reduce(sumCountObjects);
|
||||
|
||||
if (errors.length) {
|
||||
console.error(JSON.stringify(errors.reduce(sumCountObjects, {}), null, 2));
|
||||
}
|
||||
const result = { totalCalls, foundInSkins };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main("/Volumes/Mobile Backup/skins/skins/");
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const JSZip = require("jszip");
|
||||
const glob = require("glob");
|
||||
|
||||
function findWals(parentDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// options is optional
|
||||
glob("**/*.wal", { cwd: parentDir }, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main(skinPath) {
|
||||
// Find all the .wal files in the path recursively
|
||||
const wals = await findWals(skinPath);
|
||||
const zips = await Promise.all(
|
||||
wals.map(async (skin) => {
|
||||
const walPath = path.join(skinPath, skin);
|
||||
|
||||
const buffer = fs.readFileSync(walPath);
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const makis = await Promise.all(
|
||||
zip.file(/\.maki$/i).map(async (file) => {
|
||||
return {
|
||||
name: file.name,
|
||||
buffer: await file.async("nodebuffer"),
|
||||
};
|
||||
})
|
||||
);
|
||||
return {
|
||||
path: walPath,
|
||||
makis,
|
||||
};
|
||||
})
|
||||
);
|
||||
console.log(zips);
|
||||
// For each skin
|
||||
// extract it
|
||||
// find all `.maki` files
|
||||
// get its md5
|
||||
//
|
||||
// create a directory for its
|
||||
// For each sk
|
||||
}
|
||||
|
||||
main("/Volumes/Mobile Backup/skins/skins/dump/Compact-Utility");
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { parseFile } from "./parse-mi";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const compilers = path.join(__dirname, "../../../resources/maki_compiler/");
|
||||
|
||||
const lib566 = path.join(compilers, "v1.2.0 (Winamp 5.66)/lib/");
|
||||
|
||||
const files = {
|
||||
pldir: path.join(lib566, "pldir.mi"),
|
||||
config: path.join(lib566, "config.mi"),
|
||||
std: path.join(lib566, "std.mi"),
|
||||
};
|
||||
|
||||
Object.keys(files).forEach((name) => {
|
||||
const sourcePath = files[name];
|
||||
const types = parseFile(sourcePath);
|
||||
const destinationPath = path.join(__dirname, `../objectData/${name}.json`);
|
||||
|
||||
fs.writeFileSync(destinationPath, JSON.stringify(types, null, 2));
|
||||
});
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Based on extract_objects.pl from Ralf Engels<ralf.engels@gmx.de> Maki decompiler
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
function parseFile(filePath) {
|
||||
const mi = fs.readFileSync(filePath, "utf8");
|
||||
const lines = mi.split("\n");
|
||||
|
||||
const objects = {};
|
||||
lines.forEach((line, lineNumber) => {
|
||||
const classDefinitionMatch =
|
||||
/\s*extern\s+class\s*\@\{(........\s?-\s?....\s?-\s?....\s?-\s?....\s?-\s?............)\}\s*\@\s*(.*?)\s+(.*?);/.exec(
|
||||
line
|
||||
);
|
||||
if (classDefinitionMatch) {
|
||||
const id = classDefinitionMatch[1].replace(/[-\s]/g, "");
|
||||
const parent = classDefinitionMatch[2];
|
||||
const name = classDefinitionMatch[3]
|
||||
.replace(/^_predecl /, "")
|
||||
.replace(/^&/, "");
|
||||
objects[name.toLowerCase()] = { id, name, parent, functions: [] };
|
||||
}
|
||||
|
||||
const methodMatch = /\s*extern(\s+.*)?\s+(.*)\.(.*)\((.*)\);/.exec(line);
|
||||
|
||||
if (methodMatch) {
|
||||
const result = methodMatch[1] == null ? "" : methodMatch[1].trim();
|
||||
const className = methodMatch[2].toLowerCase();
|
||||
const name = methodMatch[3].trim();
|
||||
const rawArgs = methodMatch[4].split(/\s*,\s*/);
|
||||
const parameters = rawArgs.filter(Boolean).map((rawArg) => {
|
||||
const argMatch = /^\s*(.*\s+)?(.*)\s*/.exec(rawArg);
|
||||
if (argMatch == null) {
|
||||
throw new Error(`Could not find args in ${rawArg} in ${line}`);
|
||||
}
|
||||
const type = argMatch[1];
|
||||
if (type == null) {
|
||||
// console.warn(`Could not find args name in ${fileName}:${lineNum} "${line}"`);
|
||||
return [argMatch[2], "unknown_arg_name"];
|
||||
}
|
||||
return [type.trim(), argMatch[2]];
|
||||
});
|
||||
if (objects[className] == null) {
|
||||
throw new Error(
|
||||
`"${className} not defined in ${filePath}:${lineNumber}. I have ${JSON.stringify(
|
||||
Object.keys(objects)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
objects[className].functions.push({ result, name, parameters });
|
||||
}
|
||||
});
|
||||
|
||||
const objectIds = {};
|
||||
Object.keys(objects).forEach((normalizedName) => {
|
||||
const { id, parent, functions, name } = objects[normalizedName];
|
||||
objectIds[id] = { parent, functions, name };
|
||||
});
|
||||
|
||||
return objectIds;
|
||||
}
|
||||
|
||||
module.exports = { parseFile };
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import std from "../objectData/std.json";
|
||||
import config from "../objectData/config.json";
|
||||
import pldir from "../objectData/pldir.json";
|
||||
import { parseFile } from "../tools/parse-mi";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* This file basically ensures that `yarn extract-object-types` has been run.
|
||||
*/
|
||||
|
||||
const compilers = path.join(__dirname, "../../../resources/maki_compiler/");
|
||||
|
||||
const lib566 = path.join(compilers, "v1.2.0 (Winamp 5.66)/lib/");
|
||||
|
||||
test("std.mi", () => {
|
||||
const parsedObjects = parseFile(path.join(lib566, "std.mi"));
|
||||
expect(parsedObjects).toEqual(std);
|
||||
});
|
||||
|
||||
test("config.mi", () => {
|
||||
const parsedObjects = parseFile(path.join(lib566, "config.mi"));
|
||||
expect(parsedObjects).toEqual(config);
|
||||
});
|
||||
|
||||
test("pldir.mi", () => {
|
||||
const parsedObjects = parseFile(path.join(lib566, "pldir.mi"));
|
||||
expect(parsedObjects).toEqual(pldir);
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import Emitter from "../Emitter";
|
||||
|
||||
class Variable {
|
||||
constructor({ type, typeName, global }) {
|
||||
this.type = type;
|
||||
this.typeName = typeName;
|
||||
this.global = global;
|
||||
this._emitter = new Emitter();
|
||||
this._unsubscribeFromValue = null;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
if (this._unsubscribeFromValue != null) {
|
||||
this._unsubscribeFromValue();
|
||||
}
|
||||
if (this.global && this.typeName === "OBJECT" && value !== 0) {
|
||||
this._unsubscribeFromValue = value.js_listenToAll(
|
||||
(eventName, ...args) => {
|
||||
this._emitter.trigger(eventName.toLowerCase(), ...args);
|
||||
}
|
||||
);
|
||||
}
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
hook(eventName, cb) {
|
||||
this._emitter.listen(eventName.toLowerCase(), cb);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._unsubscribeFromValue) {
|
||||
this._unsubscribeFromValue();
|
||||
}
|
||||
this._emitter.dispose();
|
||||
}
|
||||
|
||||
static newInt(value) {
|
||||
const result = new Variable({
|
||||
type: "INT",
|
||||
typeName: "WHAT",
|
||||
global: false,
|
||||
});
|
||||
result.setValue(value);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default Variable;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import parse from "./parser";
|
||||
import { getClass, getFormattedId } from "./objects";
|
||||
import { interpret } from "./interpreter";
|
||||
|
||||
export function run({ runtime, data, system, log }) {
|
||||
const program = parse(data);
|
||||
|
||||
// Replace class hashes with actual JavaScript classes from the runtime
|
||||
program.classes = program.classes.map((hash) => {
|
||||
const resolved = runtime[hash];
|
||||
if (resolved == null && log) {
|
||||
const klass = getClass(hash);
|
||||
console.warn(
|
||||
`Class missing from runtime: ${hash}`,
|
||||
klass == null
|
||||
? `(formatted ID: ${getFormattedId(hash)})`
|
||||
: `expected ${klass.name}`
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
|
||||
// Bind top level hooks.
|
||||
program.bindings.forEach((binding) => {
|
||||
const { commandOffset, variableOffset, methodOffset } = binding;
|
||||
const variable = program.variables[variableOffset];
|
||||
const method = program.methods[methodOffset];
|
||||
// const logger = log ? printCommand : logger;
|
||||
// TODO: Handle disposing of this.
|
||||
// TODO: Handle passing in variables.
|
||||
variable.hook(method.name, (...args) => {
|
||||
interpret(commandOffset, program, args.reverse());
|
||||
});
|
||||
});
|
||||
|
||||
// Set the System global
|
||||
// TODO: We could confirm that this variable has the "system" flag set.
|
||||
program.variables[0].setValue(system);
|
||||
system.onscriptloaded();
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
78
packages/webamp-modern/src/maki/objectData/application.json
Normal file
78
packages/webamp-modern/src/maki/objectData/application.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"B8E867B027154da7A5BA53DBA1FCFEAC": {
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetApplicationName",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetVersionString",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetVersionNumberString",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "int",
|
||||
"name": "GetBuildNumber",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetGUID",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetCommandLine",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "Shutdown",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "CancelShutdown",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "boolean",
|
||||
"name": "IsShuttingDown",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetApplicationPath",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetSettingsPath",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "String",
|
||||
"name": "GetWorkingPath",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"result": "",
|
||||
"name": "SetWorkingPath",
|
||||
"parameters": [
|
||||
[
|
||||
"String",
|
||||
"working_path"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "Application"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"B2AD3F2B31ED4e31BC6DE9951CD555BB": {
|
||||
"name": "WinampConfig",
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
|
|
@ -13,10 +12,10 @@
|
|||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"name": "WinampConfig"
|
||||
},
|
||||
"FC17844EC72B4518A068A8F930A5BA80": {
|
||||
"name": "WinampConfigGroup",
|
||||
"parent": "Object",
|
||||
"functions": [
|
||||
{
|
||||
|
|
@ -83,6 +82,7 @@
|
|||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"name": "WinampConfigGroup"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue