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:
Fathony Luthfillah 2022-03-31 09:35:19 +07:00 committed by GitHub
parent 69122b7c89
commit 10555e093f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
228 changed files with 421 additions and 30603 deletions

View file

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

View file

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

View file

@ -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}'"
},

View file

@ -1 +0,0 @@
build/

View file

@ -1 +0,0 @@
build/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es2015",
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true
}
}

View file

@ -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"]
}
}
}

View file

@ -1,6 +1 @@
*.min.js
built/
coverage/
**/node_modules/
examples/webpack/bundle.js
pacakges/tweetBot/env/
build/

View file

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

View file

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

View file

@ -1,6 +1 @@
**/node_modules
/built
/coverage
/examples/webpack/bundle.js
**/__diff_output__/
build/

View file

@ -1,6 +0,0 @@
package.json
**/*.min.css
**/base-skin.css
built/*
coverage/*
examples/webpack/bundle.js

View file

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

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
#root {
image-rendering: pixelated;
}

View file

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

View file

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

View file

@ -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 = [];
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -251,6 +251,7 @@
}
</style>
<style id="bitmap-css"></style>
<style id="truetypefont-css"></style>
</head>
<body>
<h1 id="status">Downloading JavaScript...</h1>

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
import { XmlTree } from "./types";
export default async function initializeStateTree(
xmlTree: XmlTree
): Promise<XmlTree> {
return xmlTree;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"
}
}

View file

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