681 lines
22 KiB
JavaScript
681 lines
22 KiB
JavaScript
import { Core } from /**/"./core/Core.js";
|
|
import { Debugger } from /**/"./debugger/Debugger.js";
|
|
import { Disassembler } from /**/"./core/Disassembler.js";
|
|
import { Toolkit } from /**/"./toolkit/Toolkit.js";
|
|
|
|
// Front-end emulator application
|
|
class App extends Toolkit.App {
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(bundle) {
|
|
super({
|
|
style: {
|
|
display : "grid",
|
|
gridTemplateRows: "max-content auto"
|
|
},
|
|
visibility: true,
|
|
visible : false
|
|
});
|
|
|
|
// Configure instance fields
|
|
this.bundle = bundle;
|
|
this.debugMode = true;
|
|
this.dualMode = false;
|
|
this.text = null;
|
|
}
|
|
|
|
async init() {
|
|
|
|
// Theme
|
|
Object.assign(document.body.style, { margin:"0", overflow:"hidden" });
|
|
this.stylesheet(/**/"web/theme/kiosk.css", false);
|
|
this.stylesheet(/**/"web/theme/vbemu.css", false);
|
|
this._theme = "auto";
|
|
this.themes = {
|
|
dark : this.stylesheet(/**/"web/theme/dark.css" ),
|
|
light : this.stylesheet(/**/"web/theme/light.css" ),
|
|
virtual: this.stylesheet(/**/"web/theme/virtual.css")
|
|
};
|
|
|
|
// Watch for dark mode preference changes
|
|
this.isDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
this.isDark.addEventListener("change", e=>this.onDark());
|
|
this.onDark();
|
|
|
|
// Locales
|
|
await this.addLocale(/**/"web/locale/en-US.json");
|
|
for (let id of [].concat(navigator.languages, ["en-US"])) {
|
|
if (this.setLocale(id))
|
|
break;
|
|
}
|
|
this.setTitle("{app.title}", true);
|
|
|
|
// Element
|
|
document.body.append(this.element);
|
|
window.addEventListener("resize", e=>{
|
|
this.element.style.height = window.innerHeight + "px";
|
|
this.element.style.width = window.innerWidth + "px";
|
|
});
|
|
window.dispatchEvent(new Event("resize"));
|
|
this.addEventListener("keydown", e=>this.onKeyDown(e));
|
|
|
|
// Menus
|
|
this.menuBar = new Toolkit.MenuBar(this);
|
|
this.menuBar.setLabel("{menu._}", true);
|
|
this.add(this.menuBar);
|
|
this.initFileMenu ();
|
|
this.initEmulationMenu();
|
|
this.initDebugMenu (0, this.debugMode);
|
|
this.initDebugMenu (1, this.debugMode && this.dualMode);
|
|
this.initThemeMenu ();
|
|
|
|
// Fallback for bubbled key events
|
|
document.body.addEventListener("focusout", e=>this.onBlur(e));
|
|
window .addEventListener("keydown" , e=>this.onKey (e));
|
|
window .addEventListener("keyup" , e=>this.onKey (e));
|
|
|
|
// Temporary: Faux game mode display
|
|
this.display = new Toolkit.Component(this, {
|
|
class : "tk display",
|
|
style : { position: "relative" },
|
|
visible: !this.debugMode
|
|
});
|
|
this.image1 = new Toolkit.Component(this, { style: {
|
|
background: "#000000",
|
|
position : "absolute"
|
|
}});
|
|
this.display.add(this.image1);
|
|
this.image2 = new Toolkit.Component(this, { style: {
|
|
background: "#000000",
|
|
position : "absolute"
|
|
}});
|
|
this.display.add(this.image2);
|
|
this.display.addEventListener("resize", e=>this.onDisplay());
|
|
this.add(this.display);
|
|
|
|
// Temporary: Faux debug mode display
|
|
this.desktop = new Toolkit.Desktop(this, {
|
|
visible: this.debugMode
|
|
});
|
|
this.add(this.desktop);
|
|
|
|
// Emulation core
|
|
this.core = await new Core().init();
|
|
let sims = (await this.core.create(2)).sims;
|
|
this.core.onsubscription = (k,m)=>this.onSubscription(k, m);
|
|
|
|
// Debugging managers
|
|
this.dasm = new Disassembler();
|
|
this.debug = new Array(sims.length);
|
|
for (let x = 0; x < sims.length; x++) {
|
|
let dbg = this.debug[x] = new Debugger(this, sims[x], x);
|
|
if (x == 0 && !this.dualMode) {
|
|
dbg.cpu .substitute("#", "");
|
|
dbg.memory.substitute("#", "");
|
|
}
|
|
this.desktop.add(dbg.cpu);
|
|
this.desktop.add(dbg.memory);
|
|
}
|
|
|
|
// Reveal the application
|
|
this.visible = true;
|
|
this.restoreFocus();
|
|
}
|
|
|
|
// Initialize File menu
|
|
initFileMenu() {
|
|
let bar = this.menuBar;
|
|
let item = bar.file = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.file._}");
|
|
bar.add(item);
|
|
|
|
let menu = item.menu = new Toolkit.Menu(this);
|
|
|
|
item = bar.file.loadROM0 = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.file.loadROM}");
|
|
item.substitute("#", this.dualMode ? " 1" : "", false);
|
|
item.addEventListener("action", e=>this.onLoadROM(0));
|
|
menu.add(item);
|
|
|
|
item = bar.file.loadROM1 = new Toolkit.MenuItem(this,
|
|
{ visible: this.dualMode });
|
|
item.setText("{menu.file.loadROM}");
|
|
item.substitute("#", " 2", false);
|
|
item.addEventListener("action", e=>this.onLoadROM(1));
|
|
menu.add(item);
|
|
|
|
item = bar.file.dualMode = new Toolkit.MenuItem(this,
|
|
{ checked: this.dualMode, type: "checkbox" });
|
|
item.setText("{menu.file.dualMode}");
|
|
item.addEventListener("action", e=>this.onDualMode());
|
|
menu.add(item);
|
|
|
|
item = bar.file.debugMode = new Toolkit.MenuItem(this,
|
|
{ checked: this.debugMode, disabled: true, type: "checkbox" });
|
|
item.setText("{menu.file.debugMode}");
|
|
item.addEventListener("action", e=>this.onDebugMode());
|
|
menu.add(item);
|
|
|
|
menu.addSeparator();
|
|
|
|
item = new Toolkit.MenuItem(this);
|
|
item.setText("Export source...", false);
|
|
item.addEventListener("action", ()=>this.bundle.save());
|
|
menu.add(item);
|
|
}
|
|
|
|
// Initialize Emulation menu
|
|
initEmulationMenu() {
|
|
let bar = this.menuBar;
|
|
let item = bar.emulation = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.emulation._}");
|
|
bar.add(item);
|
|
|
|
let menu = item.menu = new Toolkit.Menu(this);
|
|
|
|
item = bar.emulation.run =
|
|
new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.emulation.run}");
|
|
menu.add(item);
|
|
|
|
item = bar.emulation.reset0 = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.emulation.reset}");
|
|
item.substitute("#", this.dualMode ? " 1" : "", false);
|
|
item.addEventListener("action", e=>this.onReset(0));
|
|
menu.add(item);
|
|
|
|
item = bar.emulation.reset1 = new Toolkit.MenuItem(this,
|
|
{ visible: this.dualMode });
|
|
item.setText("{menu.emulation.reset}");
|
|
item.substitute("#", " 2", false);
|
|
item.addEventListener("action", e=>this.onReset(1));
|
|
menu.add(item);
|
|
|
|
item = bar.emulation.linkSims = new Toolkit.MenuItem(this,
|
|
{ disabled: true, type: "checkbox", visible: this.dualMode });
|
|
item.setText("{menu.emulation.linkSims}");
|
|
menu.add(item);
|
|
}
|
|
|
|
// Initialize Debug menu
|
|
initDebugMenu(index, visible) {
|
|
let bar = this.menuBar;
|
|
let item = bar["debug" + index] = new Toolkit.MenuItem(this,
|
|
{ visible: visible }), top = item;
|
|
item.setText("{menu.debug._}");
|
|
item.substitute("#",
|
|
index == 0 ? this.dualMode ? "1" : "" : " 2", false);
|
|
bar.add(item);
|
|
|
|
let menu = item.menu = new Toolkit.Menu(this);
|
|
|
|
item = top.console = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.console}");
|
|
menu.add(item);
|
|
|
|
item = top.memory = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.debug.memory}");
|
|
item.addEventListener("action",
|
|
e=>this.showWindow(this.debug[index].memory));
|
|
menu.add(item);
|
|
|
|
item = top.cpu = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.debug.cpu}");
|
|
item.addEventListener("action",
|
|
e=>this.showWindow(this.debug[index].cpu));
|
|
menu.add(item);
|
|
|
|
item=top.breakpoints = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.breakpoints}");
|
|
menu.add(item);
|
|
|
|
menu.addSeparator();
|
|
|
|
item = top.palettes = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.palettes}");
|
|
menu.add(item);
|
|
|
|
item = top.characters = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.characters}");
|
|
menu.add(item);
|
|
|
|
item = top.bgMaps = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.bgMaps}");
|
|
menu.add(item);
|
|
|
|
item = top.backgrounds = new Toolkit.MenuItem(this, { disabled:true });
|
|
item.setText("{menu.debug.backgrounds}");
|
|
menu.add(item);
|
|
|
|
item = top.objects = new Toolkit.MenuItem(this, { disabled: true });
|
|
item.setText("{menu.debug.objects}");
|
|
menu.add(item);
|
|
|
|
item = top.frameBuffers = new Toolkit.MenuItem(this, {disabled: true});
|
|
item.setText("{menu.debug.frameBuffers}");
|
|
menu.add(item);
|
|
}
|
|
|
|
// Initialize Theme menu
|
|
initThemeMenu() {
|
|
let bar = this.menuBar;
|
|
let item = bar.theme = new Toolkit.MenuItem(this);
|
|
item.setText("{menu.theme._}");
|
|
bar.add(item);
|
|
|
|
let menu = item.menu = new Toolkit.Menu(this);
|
|
|
|
item = bar.theme.auto = new Toolkit.MenuItem(this,
|
|
{ checked: true, type: "checkbox" });
|
|
item.setText("{menu.theme.auto}");
|
|
item.theme = "auto";
|
|
item.addEventListener("action", e=>this.theme = "auto");
|
|
menu.add(item);
|
|
|
|
item = bar.theme.light = new Toolkit.MenuItem(this,
|
|
{ checked: false, type: "checkbox" });
|
|
item.setText("{menu.theme.light}");
|
|
item.theme = "light";
|
|
item.addEventListener("action", e=>this.theme = "light");
|
|
menu.add(item);
|
|
|
|
item = bar.theme.dark = new Toolkit.MenuItem(this,
|
|
{ checked: false, type: "checkbox" });
|
|
item.setText("{menu.theme.dark}");
|
|
item.theme = "dark";
|
|
item.addEventListener("action", e=>this.theme = "dark");
|
|
menu.add(item);
|
|
|
|
item = bar.theme.light = new Toolkit.MenuItem(this,
|
|
{ checked: false, type: "checkbox" });
|
|
item.setText("{menu.theme.virtual}");
|
|
item.theme = "virtual";
|
|
item.addEventListener("action", e=>this.theme = "virtual");
|
|
menu.add(item);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// All elements have lost focus
|
|
onBlur(e) {
|
|
if (
|
|
e.relatedTarget == null ||
|
|
e.relatedTarget == document.body
|
|
) this.restoreFocus();
|
|
}
|
|
|
|
// Dark mode preference changed
|
|
onDark() {
|
|
if (this._theme != "auto")
|
|
return;
|
|
let isDark = this.isDark.matches;
|
|
this.themes.light.disabled = isDark;
|
|
this.themes.dark .disabled = !isDark;
|
|
}
|
|
|
|
// Game mode display resized
|
|
onDisplay() {
|
|
let bounds = this.display.element.getBoundingClientRect();
|
|
let width = Math.max(1, bounds.width);
|
|
let height = Math.max(1, bounds.height);
|
|
let scale, x1, y1, x2, y2;
|
|
|
|
// Single mode
|
|
if (!this.dualMode) {
|
|
this.image2.visible = false;
|
|
scale = Math.max(1, Math.min(
|
|
Math.floor(width / 384),
|
|
Math.floor(height / 224)
|
|
));
|
|
x1 = Math.max(0, Math.floor((width - 384 * scale) / 2));
|
|
y1 = Math.max(0, Math.floor((height - 224 * scale) / 2));
|
|
x2 = y2 = 0;
|
|
}
|
|
|
|
// Dual mode
|
|
else {
|
|
this.image2.visible = true;
|
|
|
|
// Horizontal orientation
|
|
if (true) {
|
|
scale = Math.max(1, Math.min(
|
|
Math.floor(width / 768),
|
|
Math.floor(height / 224)
|
|
));
|
|
let gap = Math.max(0, width - 768 * scale);
|
|
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
|
|
x1 = gap;
|
|
x2 = Math.max(384 * scale, width - 384 * scale - gap);
|
|
y1 = y2 = Math.max(0, Math.floor((height - 224 * scale) / 2));
|
|
}
|
|
|
|
// Vertical orientation
|
|
else {
|
|
scale = Math.max(1, Math.min(
|
|
Math.floor(width / 384),
|
|
Math.floor(height / 448)
|
|
));
|
|
let gap = Math.max(0, height - 448 * scale);
|
|
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
|
|
x1 = x2 = Math.max(0, Math.floor((width - 384 * scale) / 2));
|
|
y1 = gap;
|
|
y2 = Math.max(224 * scale, height - 224 * scale - gap);
|
|
}
|
|
|
|
}
|
|
|
|
width = 384 * scale + "px";
|
|
height = 224 * scale + "px";
|
|
Object.assign(this.image1.element.style,
|
|
{ left: x1+"px", top: y1+"px", width: width, height: height });
|
|
Object.assign(this.image2.element.style,
|
|
{ left: x2+"px", top: y2+"px", width: width, height: height });
|
|
}
|
|
|
|
// File -> Debug mode
|
|
onDebugMode() {
|
|
this.debugMode =!this.debugMode;
|
|
this.display.visible =!this.debugMode;
|
|
this.desktop.visible = this.debugMode;
|
|
this.configMenus();
|
|
this.onDisplay();
|
|
}
|
|
|
|
// Emulation -> Dual mode
|
|
onDualMode() {
|
|
this.setDualMode(!this.dualMode);
|
|
this.configMenus();
|
|
this.onDisplay();
|
|
}
|
|
|
|
// Key press
|
|
onKeyDown(e) {
|
|
|
|
// Take no action
|
|
if (!e.altKey || e.key != "F10" ||
|
|
this.menuBar.contains(document.activeElement))
|
|
return;
|
|
|
|
// Move focus into the menu bar
|
|
this.menuBar.focus();
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// File -> Load ROM
|
|
async onLoadROM(index) {
|
|
|
|
// Add a file picker to the document
|
|
let file = document.createElement("input");
|
|
file.type = "file";
|
|
file.style.position = "absolute";
|
|
file.style.visibility = "hidden";
|
|
document.body.appendChild(file);
|
|
|
|
// Prompt the user to select a file
|
|
await new Promise(resolve=>{
|
|
file.addEventListener("input", resolve);
|
|
file.click();
|
|
});
|
|
file.remove();
|
|
|
|
// No file was selected
|
|
file = file.files[0];
|
|
if (!file)
|
|
return;
|
|
|
|
// Load the file
|
|
let data = null;
|
|
try { data = new Uint8Array(await file.arrayBuffer()); }
|
|
catch {
|
|
alert(this.localize("{menu.file.loadROMError}"));
|
|
return;
|
|
}
|
|
|
|
// Attempt to process the file as an ISX binary
|
|
try { data = Debugger.isx(data).toROM(); } catch { }
|
|
|
|
// Error checking
|
|
if (
|
|
data.length < 1024 ||
|
|
data.length > 0x1000000 ||
|
|
(data.length & data.length - 1)
|
|
) {
|
|
alert(this.localize("{menu.file.loadROMInvalid}"));
|
|
return;
|
|
}
|
|
|
|
// Load the ROM into simulation memory
|
|
let rep = await this.core.setROM(this.debug[index].sim, data,
|
|
{ refresh: true });
|
|
if (!rep.success) {
|
|
alert(this.localize("{menu.file.loadROMError}"));
|
|
return;
|
|
}
|
|
this.debug[index].followPC(0xFFFFFFF0);
|
|
}
|
|
|
|
// Emulation -> Reset
|
|
async onReset(index) {
|
|
await this.core.reset(this.debug[index].sim, { refresh: true });
|
|
this.debug[index].followPC(0xFFFFFFF0);
|
|
}
|
|
|
|
// Core subscription
|
|
onSubscription(key, msg) {
|
|
let target = this.debug; // Handler object
|
|
for (let x = 1; x < key.length - 1; x++)
|
|
target = target[key[x]];
|
|
target[key[key.length - 1]].call(target, msg);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// Specify document title
|
|
setTitle(title, localize) {
|
|
this.setString("text", title, localize);
|
|
}
|
|
|
|
// Specify the color theme
|
|
get theme() { return this._theme; }
|
|
set theme(theme) {
|
|
switch (theme) {
|
|
case "light": case "dark": case "virtual":
|
|
this._theme = theme;
|
|
for (let entry of Object.entries(this.themes))
|
|
entry[1].disabled = entry[0] != theme;
|
|
break;
|
|
default:
|
|
this._theme = "auto";
|
|
this.themes["virtual"].disabled = true;
|
|
this.onDark();
|
|
}
|
|
for (let item of this.menuBar.theme.menu.children)
|
|
item.checked = item.theme == theme;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Configure components for automatic localization, or localize a message
|
|
localize(a, b) {
|
|
|
|
// Default behavior
|
|
if (a && a != this)
|
|
return super.localize(a, b);
|
|
|
|
// Update localization strings
|
|
if (this.text != null) {
|
|
let text = this.text;
|
|
document.title = !text[1] ? text[0] :
|
|
this.localize(text[0], this.substitutions);
|
|
}
|
|
|
|
}
|
|
|
|
// Return focus to the most recent focused element
|
|
restoreFocus() {
|
|
|
|
// Focus was successfully restored
|
|
if (super.restoreFocus())
|
|
return true;
|
|
|
|
// Select the foremost visible window
|
|
let wnd = this.desktop.getActiveWindow();
|
|
if (wnd) {
|
|
wnd.focus();
|
|
return true;
|
|
}
|
|
|
|
// Select the menu bar
|
|
this.menuBar.focus();
|
|
return true;
|
|
}
|
|
|
|
// Perform a Run Next command on one of the simulations
|
|
runNext(index, options) {
|
|
let debugs = [ this.debug[index] ];
|
|
if (this.dualMode)
|
|
debugs.push(this.debug[index ^ 1]);
|
|
|
|
let ret = this.core.runNext(debugs.map(d=>d.sim), options);
|
|
|
|
if (ret instanceof Promise) ret.then(msg=>{
|
|
for (let x = 0; x < debugs.length; x++)
|
|
debugs[x].followPC(msg.pcs[x]);
|
|
});
|
|
}
|
|
|
|
// Perform a Single Step command on one of the simulations
|
|
singleStep(index, options) {
|
|
let debugs = [ this.debug[index] ];
|
|
if (this.dualMode)
|
|
debugs.push(this.debug[index ^ 1]);
|
|
|
|
let ret = this.core.singleStep(debugs.map(d=>d.sim), options);
|
|
|
|
if (ret instanceof Promise) ret.then(msg=>{
|
|
for (let x = 0; x < debugs.length; x++)
|
|
debugs[x].followPC(msg.pcs[x]);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// Configure menu item visibility
|
|
configMenus() {
|
|
let bar = this.menuBar;
|
|
bar.file.debugMode .checked = this.debugMode;
|
|
bar.file.loadROM1 .visible = this.dualMode;
|
|
bar.emulation.reset1 .visible = this.dualMode;
|
|
bar.emulation.linkSims.visible = this.dualMode;
|
|
bar.debug0 .visible = this.debugMode;
|
|
bar.debug1 .visible = this.debugMode && this.dualMode;
|
|
}
|
|
|
|
// Specify whether dual mode is active
|
|
setDualMode(dualMode) {
|
|
|
|
// Update state
|
|
if (dualMode == this.dualMode)
|
|
return;
|
|
this.dualMode = dualMode;
|
|
|
|
// Working variables
|
|
let index = dualMode ? " 1" : "";
|
|
|
|
// Update menus
|
|
let bar = this.menuBar;
|
|
bar.file.loadROM0 .substitute("#", index, false);
|
|
bar.debug0 .substitute("#", index, false);
|
|
bar.emulation.reset0.substitute("#", index, false);
|
|
bar.file.dualMode .checked = this.dualMode;
|
|
|
|
// Update sim 1 debug windows
|
|
let dbg = this.debug[0];
|
|
dbg.cpu .substitute("#", index);
|
|
dbg.memory.substitute("#", index);
|
|
|
|
// Re-show any sim 2 debug windows that were previously visible
|
|
if (dualMode) {
|
|
for (let wnd of this.desktop.children) {
|
|
if (wnd.index == 1 && wnd.wasVisible)
|
|
wnd.visible = true;
|
|
}
|
|
}
|
|
|
|
// Hide any visible sim 2 debug windows
|
|
else for (let wnd of this.desktop.children) {
|
|
if (wnd.index == 0)
|
|
continue;
|
|
wnd.wasVisible = wnd.visible;
|
|
wnd.visible = false;
|
|
}
|
|
|
|
}
|
|
|
|
// Ensure a debugger window is visible
|
|
showWindow(wnd) {
|
|
let adjust = false;
|
|
|
|
// The window is already visible
|
|
if (wnd.visible) {
|
|
|
|
// The window is already in the foreground
|
|
if (wnd == this.desktop.getActiveWindow())
|
|
;//adjust = true;
|
|
|
|
// Bring the window to the foreground
|
|
else this.desktop.bringToFront(wnd);
|
|
|
|
}
|
|
|
|
// The window is not visible
|
|
else {
|
|
adjust = !wnd.shown;
|
|
wnd.visible = true;
|
|
this.desktop.bringToFront(wnd);
|
|
}
|
|
|
|
// Adjust the window position
|
|
if (adjust) {
|
|
let bounds = this.desktop.element.getBoundingClientRect();
|
|
wnd.left = Math.max(0, (bounds.width - wnd.outerWidth ) / 2);
|
|
wnd.top = Math.max(0, (bounds.height - wnd.outerHeight) / 2);
|
|
}
|
|
|
|
// Transfer focus into the window
|
|
wnd.focus();
|
|
}
|
|
|
|
// Install a stylesheet. Returns the resulting <link> element
|
|
stylesheet(filename, disabled = true) {
|
|
let ret = document.createElement("link");
|
|
ret.href = filename;
|
|
ret.rel = "stylesheet";
|
|
ret.type = "text/css";
|
|
ret.disabled = disabled;
|
|
document.head.append(ret);
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Program Methods /////////////////////////////
|
|
|
|
// Program entry point
|
|
static main(bundle) {
|
|
new App(bundle).init();
|
|
}
|
|
|
|
}
|
|
|
|
export { App };
|