save VM state

This commit is contained in:
Rachel Powers
2024-04-19 20:06:19 -07:00
parent c63a1b3a4f
commit 9a374a4f73
22 changed files with 896 additions and 368 deletions

View File

@@ -52,14 +52,14 @@ export class App extends BaseElement {
// return this.renderRoot.querySelector("ace-ic10") as IC10Editor;
// }
vm!: VirtualMachine;
session!: Session;
vm: VirtualMachine;
session: Session;
constructor() {
super();
window.App = this;
this.session = new Session();
this.vm = new VirtualMachine();
this.session = new Session(this);
this.vm = new VirtualMachine(this);
window.App.set(this);
}
protected createRenderRoot(): HTMLElement | DocumentFragment {
@@ -81,7 +81,7 @@ export class App extends BaseElement {
snap="512px 50%"
snap-threshold="15"
>
<ace-ic10 slot="start" style=""></ace-ic10>
<ace-ic10 slot="start"></ace-ic10>
<div slot="end"><vm-ui></vm-ui></div>
</sl-split-panel>
</div>
@@ -107,8 +107,4 @@ export class App extends BaseElement {
}
}
declare global {
interface Window {
App?: App;
}
}

View File

@@ -268,11 +268,12 @@ export class IC10Editor extends BaseElement {
this.initializeEditor();
}
initializeEditor() {
async initializeEditor() {
let editor = this.editor;
const that = this;
window.App!.session.onLoad(((e: CustomEvent) => {
const app = await window.App.get();
app.session.onLoad(((e: CustomEvent) => {
const session = e.detail;
const updated_ids: number[] = [];
for (const [id, _] of session.programs) {
@@ -286,10 +287,10 @@ export class IC10Editor extends BaseElement {
}
}
}) as EventListener);
window.App!.session.loadFromFragment();
app.session.loadFromFragment();
window.App!.session.onActiveLine(((e: CustomEvent) => {
const session = window.App?.session!;
app.session.onActiveLine(((e: CustomEvent) => {
const session = app.session;
const id: number = e.detail;
const active_line = session.getActiveLine(id);
if (typeof active_line !== "undefined") {
@@ -587,7 +588,7 @@ export class IC10Editor extends BaseElement {
if (session) {
session.on("change", () => {
var val = session.getValue();
window.App?.session.setProgramCode(session_id, val);
window.App.get().then(app => app.session.setProgramCode(session_id, val));
});
}
}

View File

@@ -1,65 +0,0 @@
import { IC10Editor } from "./editor";
import { Session } from "./session";
import { VirtualMachine } from "./virtual_machine";
import { docReady, openFile, saveFile } from "./utils";
// import { makeRequest } from "./utils";
// const dbPromise = makeRequest({ method: "GET", url: "/data/database.json"});
// const dbPromise = fetch("/data/database.json").then(resp => resp.json());
// docReady(() => {
// App.vm = new VirtualMachine();
//
// dbPromise.then((db) => App.vm.setupDeviceDatabase(db));
//
// const init_session_id = App.vm.devices.get(0).id;
//
// // App.editor = new IC10Editor(init_session_id);
//
// // setupLspWorker().then((worker) => {
// // App.editor.setupLsp(worker);
// // });
//
// // Menu
// document.getElementById("mainMenuShare").addEventListener(
// "click",
// (_event) => {
// const link = document.getElementById("shareLinkText") as HTMLInputElement;
// link.setAttribute("value", window.location.href);
// link.setSelectionRange(0, 0);
// },
// { capture: true },
// );
// document.getElementById("shareLinkCopyButton").addEventListener(
// "click",
// (event) => {
// event.preventDefault();
// const link = document.getElementById("shareLinkText") as HTMLInputElement;
// link.select();
// link.setSelectionRange(0, 99999);
// navigator.clipboard.writeText(link.value);
// },
// { capture: true },
// );
// document.getElementById("mainMenuOpenFile").addEventListener(
// "click",
// (_event) => {
// openFile(App.editor.editor);
// },
// { capture: true },
// );
// document.getElementById("mainMenuSaveAs").addEventListener(
// "click",
// (_event) => {
// saveFile(App.editor.editor.getSession().getValue());
// },
// { capture: true },
// );
// document.getElementById("mainMenuKeyboardShortcuts").addEventListener(
// "click",
// (_event) => {
// App.editor.editor.execCommand("showKeyboardShortcuts");
// },
// { capture: true },
// );
// });

View File

@@ -1,10 +1,79 @@
import "@popperjs/core";
import "../scss/styles.scss";
import { Dropdown, Modal } from "bootstrap";
import "./app";
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `main.js` file does the single async import, so
// that no one else needs to worry about it again.
// import("./index")
// .catch(e => console.error("Error importing `index.ts`:", e));
class DeferedApp {
app: App;
private resolvers: ((value: App) => void)[];
constructor() {
this.app = undefined;
this.resolvers = [];
}
get(): Promise<App> {
const that = this;
return new Promise(resolve => {
if (typeof that.app !== "undefined") {
that.resolvers.push(resolve);
} else {
resolve(that.app);
}
})
}
set(app: App) {
this.app = app;
while(this.resolvers.length) {
this.resolvers.shift()(this.app);
}
}
}
class DeferedVM {
vm: VirtualMachine;
private resolvers: ((value: VirtualMachine) => void)[];
constructor() {
this.vm = undefined;
this.resolvers = [];
}
get(): Promise<VirtualMachine> {
const that = this;
return new Promise(resolve => {
if (typeof that.vm !== "undefined") {
that.resolvers.push(resolve);
} else {
resolve(that.vm);
}
})
}
set(vm: VirtualMachine) {
this.vm = vm;
while(this.resolvers.length) {
this.resolvers.shift()(this.vm);
}
}
}
declare global {
interface Window
{
App: DeferedApp;
VM: DeferedVM;
}
}
window.App = new DeferedApp();
window.VM = new DeferedVM();
import type { App } from "./app";
import type { VirtualMachine } from "./virtual_machine";
import("./app");

View File

@@ -60,22 +60,28 @@ j ra
`;
import type { ICError } from "ic10emu_wasm";
import type { ICError, FrozenVM } from "ic10emu_wasm";
import { App } from "./app";
export class Session extends EventTarget {
_programs: Map<number, string>;
_errors: Map<number, ICError[]>;
_activeIC: number;
_activeLines: Map<number, number>;
_activeLine: number;
_save_timeout?: ReturnType<typeof setTimeout>;
constructor() {
private _programs: Map<number, string>;
private _errors: Map<number, ICError[]>;
private _activeIC: number;
private _activeLines: Map<number, number>;
private _save_timeout?: ReturnType<typeof setTimeout>;
private _vm_state: FrozenVM;
private app: App;
constructor(app: App) {
super();
this.app = app;
this._programs = new Map();
this._errors = new Map();
this._save_timeout = undefined;
this._activeIC = 1;
this._activeLines = new Map();
this._vm_state = undefined;
this.loadFromFragment();
const that = this;
@@ -84,11 +90,11 @@ export class Session extends EventTarget {
});
}
get programs() {
get programs(): Map<number, string> {
return this._programs;
}
set programs(programs) {
set programs(programs: Iterable<[number, string]>) {
this._programs = new Map([...programs]);
this._fireOnLoad();
}
@@ -124,10 +130,6 @@ export class Session extends EventTarget {
}
}
set activeLine(line: number) {
this._activeLine = line;
}
setProgramCode(id: number, code: string) {
this._programs.set(id, code);
this.save();
@@ -178,18 +180,18 @@ export class Session extends EventTarget {
if (this._save_timeout) clearTimeout(this._save_timeout);
this._save_timeout = setTimeout(() => {
this.saveToFragment();
if (window.App!.vm) {
window.App!.vm.updateCode();
if (this.app.vm) {
this.app.vm.updateCode();
}
this._save_timeout = undefined;
}, 1000);
}
async saveToFragment() {
const toSave = { programs: Array.from(this._programs) };
const toSave = { vmState: this.app.vm.saveVMState(), activeIC: this.activeIC };
const bytes = new TextEncoder().encode(JSON.stringify(toSave));
try {
const c_bytes = await compress(bytes);
const c_bytes = await compress(bytes, defaultCompression);
const fragment = base64url_encode(c_bytes);
window.history.replaceState(null, "", `#${fragment}`);
} catch (e) {
@@ -216,21 +218,77 @@ export class Session extends EventTarget {
this._programs = new Map([[1, txt]]);
this, this._fireOnLoad();
return;
}
try {
this._programs = new Map(data.programs);
this._fireOnLoad();
return;
} catch (e) {
console.log("Bad session data:", e);
} else if ("programs" in data) {
try {
this._programs = new Map(data.programs);
this._fireOnLoad();
return;
} catch (e) {
console.log("Bad session data:", e);
}
} else if ("vmState" in data && "activeIC" in data) {
try {
this._programs = new Map();
const state = data.vmState as FrozenVM;
// assign first so it's present when the
// vm setting the programs list fires events
this._activeIC = data.activeIC;
this.app.vm.restoreVMState(state);
this.programs = this.app.vm.getPrograms();
// assign again to fire event
this.activeIC = data.activeIC;
this._fireOnLoad();
return;
} catch (e) {
console.log("Bad session data:", e);
}
} else {
console.log("Bad session data:", data);
}
}
}
}
}
const byteToHex: string[] = [];
for (let n = 0; n <= 0xff; ++n) {
const hexOctet = n.toString(16).padStart(2, "0");
byteToHex.push(hexOctet);
}
function bufToHex(arrayBuffer: ArrayBuffer): string {
const buff = new Uint8Array(arrayBuffer);
const hexOctets = new Array(buff.length);
for (let i = 0; i < buff.length; ++i) hexOctets[i] = byteToHex[buff[i]];
return hexOctets.join("");
}
export type CompressionFormat = "gzip" | "deflate" | "deflate-raw";
const defaultCompression = "gzip";
function guessFormat(bytes: ArrayBuffer): CompressionFormat {
const header = bufToHex(bytes.slice(0, 8));
if (
header.startsWith("789c") ||
header.startsWith("7801") ||
header.startsWith("78DA")
) {
return "deflate";
} else if (header.startsWith("1f8b08")) {
return "gzip";
} else {
return "deflate-raw";
}
}
async function decompressFragment(c_bytes: ArrayBuffer) {
try {
const bytes = await decompress(c_bytes);
const format = guessFormat(c_bytes);
console.log("Decompressing fragment with:", format);
const bytes = await decompress(c_bytes, format);
return bytes;
} catch (e) {
console.log("Error decompressing content fragment:", e);
@@ -290,9 +348,12 @@ async function concatUintArrays(arrays: Uint8Array[]) {
return new Uint8Array(buffer);
}
async function compress(bytes: ArrayBuffer) {
async function compress(
bytes: ArrayBuffer,
format: CompressionFormat = defaultCompression,
) {
const s = new Blob([bytes]).stream();
const cs = s.pipeThrough(new CompressionStream("deflate-raw"));
const cs = s.pipeThrough(new CompressionStream(format));
const chunks: Uint8Array[] = [];
for await (const chunk of streamAsyncIterator(cs)) {
chunks.push(chunk);
@@ -300,9 +361,12 @@ async function compress(bytes: ArrayBuffer) {
return await concatUintArrays(chunks);
}
async function decompress(bytes: ArrayBuffer) {
async function decompress(
bytes: ArrayBuffer,
format: CompressionFormat = defaultCompression,
) {
const s = new Blob([bytes]).stream();
const ds = s.pipeThrough(new DecompressionStream("deflate-raw"));
const ds = s.pipeThrough(new DecompressionStream(format));
const chunks: Uint8Array[] = [];
for await (const chunk of streamAsyncIterator(ds)) {
chunks.push(chunk);

View File

@@ -19,6 +19,11 @@ function replacer(key: any, value: any) {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else if (Number.isNaN(value)) {
return {
dataType: 'Number',
value: "NaN",
}
} else {
return value;
}
@@ -28,6 +33,8 @@ function reviver(_key: any, value: any) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
} else if (value.dataType === 'Number') {
return parseFloat(value.value)
}
}
return value;

View File

@@ -79,14 +79,14 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
connectedCallback(): void {
const root = super.connectedCallback();
window.VM?.addEventListener(
window.VM.get().then(vm => vm.addEventListener(
"vm-device-modified",
this._handleDeviceModified.bind(this),
);
window.VM?.addEventListener(
));
window.VM.get().then(vm => vm.addEventListener(
"vm-devices-update",
this._handleDevicesModified.bind(this),
);
));
this.updateDevice();
return root;
}
@@ -106,7 +106,7 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
}
updateDevice() {
this.device = window.VM!.devices.get(this.deviceID)!;
this.device = window.VM.vm.devices.get(this.deviceID)!;
const name = this.device.name ?? null;
if (this.name !== name) {
@@ -189,16 +189,16 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
class VMActiveICMixinClass extends VMDeviceMixin(superClass) {
constructor() {
super();
this.deviceID = window.App!.session.activeIC;
this.deviceID = window.App.app.session.activeIC;
}
connectedCallback(): void {
const root = super.connectedCallback();
window.VM?.addEventListener(
window.VM.get().then(vm => vm.addEventListener(
"vm-run-ic",
this._handleDeviceModified.bind(this),
);
window.App?.session.addEventListener(
));
window.App.app.session.addEventListener(
"session-active-ic",
this._handleActiveIC.bind(this),
);
@@ -209,7 +209,7 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
const id = e.detail;
if (this.deviceID !== id) {
this.deviceID = id;
this.device = window.VM!.devices.get(this.deviceID)!;
this.device = window.VM.vm.devices.get(this.deviceID)!;
}
this.updateDevice();
}

View File

@@ -68,7 +68,7 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
@query(".active-ic-select") accessor activeICSelect: SlSelect;
protected render() {
const ics = Array.from(window.VM!.ics);
const ics = Array.from(window.VM.vm.ics);
return html`
<sl-card class="card">
<div class="controls" slot="header">
@@ -170,13 +170,13 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
}
_handleRunClick() {
window.VM?.run();
window.VM.get().then(vm => vm.run());
}
_handleStepClick() {
window.VM?.step();
window.VM.get().then(vm => vm.step());
}
_handleResetClick() {
window.VM?.reset();
window.VM.get().then(vm => vm.reset());
}
updateIC(): void {
@@ -194,6 +194,6 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
_handleChangeActiveIC(e: CustomEvent) {
const select = e.target as SlSelect;
const icId = parseInt(select.value as string);
window.App!.session.activeIC = icId;
window.App.app.session.activeIC = icId;
}
}

View File

@@ -45,6 +45,7 @@ import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js"
import { DeviceDB, DeviceDBEntry } from "./device_db";
import { connectionFromDeviceDBConnection } from "./utils";
import { SlDialog } from "@shoelace-style/shoelace";
import { repeat } from "lit/directives/repeat.js";
@customElement("vm-device-card")
export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
@@ -166,7 +167,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
connectedCallback(): void {
super.connectedCallback();
window.VM!.addEventListener(
window.VM.vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
@@ -182,7 +183,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
}
renderHeader(): HTMLTemplateResult {
const activeIc = window.VM?.activeIC;
const activeIc = window.VM.vm.activeIC;
const thisIsActiveIc = activeIc.id === this.deviceID;
const badges: HTMLTemplateResult[] = [];
if (this.deviceID == activeIc?.id) {
@@ -197,30 +198,67 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
}, this);
return html`
<sl-tooltip content="${this.prefabName}">
<img class="image" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<img
class="image"
src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
</sl-tooltip>
<div class="header-name">
<sl-input id="vmDeviceCard${this.deviceID}Id" class="device-id" size="small" pill value=${this.deviceID}
@sl-change=${this._handleChangeID}>
<sl-input
id="vmDeviceCard${this.deviceID}Id"
class="device-id"
size="small"
pill
value=${this.deviceID}
@sl-change=${this._handleChangeID}
>
<span slot="prefix">Id</span>
<sl-copy-button slot="suffix" value=${this.deviceID}></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}Name" class="device-name" size="small" pill placeholder="${this.prefabName}"
@sl-change=${this._handleChangeName}>
<sl-input
id="vmDeviceCard${this.deviceID}Name"
class="device-name"
size="small"
pill
placeholder="${this.prefabName}"
@sl-change=${this._handleChangeName}
>
<span slot="prefix">Name</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}Name.value"></sl-copy-button>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.deviceID}Name.value"
></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash"
value="${this.nameHash}" disabled>
<sl-input
id="vmDeviceCard${this.deviceID}NameHash"
size="small"
pill
class="device-name-hash"
value="${this.nameHash}"
disabled
>
<span slot="prefix">Hash</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}NameHash.value"></sl-copy-button>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.deviceID}NameHash.value"
></sl-copy-button>
</sl-input>
${badges.map((badge) => badge)}
</div>
<div class="ms-auto mt-auto mb-auto me-2">
<sl-tooltip content=${thisIsActiveIc ? "Removing the selected Active IC is disabled" : "Remove Device"}>
<sl-icon-button class="remove-button" name="trash" label="Remove Device" ?disabled=${thisIsActiveIc} @click=${this._handleDeviceRemoveButton}></sl-icon-button>
<sl-tooltip
content=${thisIsActiveIc
? "Removing the selected Active IC is disabled"
: "Remove Device"}
>
<sl-icon-button
class="remove-button"
name="trash"
label="Remove Device"
?disabled=${thisIsActiveIc}
@click=${this._handleDeviceRemoveButton}
></sl-icon-button>
</sl-tooltip>
</div>
`;
@@ -231,12 +269,20 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
return html`
${fields.map(([name, field], _index, _fields) => {
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${field.value}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
return html` <sl-input
id="${inputIdBase}${name}"
key="${name}"
value="${field.value}"
size="small"
@sl-change=${this._handleChangeField}
>
<span slot="prefix">${name}</span>
<sl-copy-button
slot="suffix"
from="${inputIdBase}${name}.value"
></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`;
}
@@ -268,21 +314,20 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ);
return html`
<sl-card class="slot-card">
<img slot="header" class="slot-header image" src="${slotImg}"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<img slot="header" class="slot-header image" src="${slotImg}" onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<span slot="header" class="slot-header">${slotIndex} : ${slot.typ}</span>
${
typeof slot.occupant !== "undefined"
? html`
<span slot="header" class="slot-header">
Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash}
</span>
<span slot="header" class="slot-header">
Quantity: ${slot.occupant.quantity}/
${slot.occupant.max_quantity}
</span>
`
: ""
typeof slot.occupant !== "undefined"
? html`
<span slot="header" class="slot-header">
Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash}
</span>
<span slot="header" class="slot-header">
Quantity: ${slot.occupant.quantity}/
${slot.occupant.max_quantity}
</span>
`
: ""
}
<div class="slot-fields">
${fields.map(
@@ -313,7 +358,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
}
renderNetworks(): HTMLTemplateResult {
const vmNetworks = window.VM!.networks;
const vmNetworks = window.VM.vm.networks;
const networks = this.connections.map((connection, index, _conns) => {
const conn =
typeof connection === "object" ? connection.CableNetwork : null;
@@ -337,7 +382,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
}
renderPins(): HTMLTemplateResult {
const pins = this.pins;
const visibleDevices = window.VM!.visibleDevices(this.deviceID);
const visibleDevices = window.VM.vm.visibleDevices(this.deviceID);
const pinsHtml = pins?.map(
(pin, index) =>
html`
@@ -371,25 +416,41 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
<sl-tab slot="nav" panel="pins" ?disabled=${!this.pins}>Pins</sl-tab>
<sl-tab-panel name="fields" active>${this.renderFields()}</sl-tab-panel>
<sl-tab-panel name="fields" active
>${this.renderFields()}</sl-tab-panel
>
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
<sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel>
<sl-tab-panel name="networks">${this.renderNetworks()}</sl-tab-panel>
<sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel>
</sl-tab-group>
</ic10-details>
<sl-dialog class="remove-device-dialog" no-header @sl-request-close=${this._preventOverlayClose}>
<sl-dialog
class="remove-device-dialog"
no-header
@sl-request-close=${this._preventOverlayClose}
>
<div class="remove-dialog-body">
<img class="dialog-image mt-auto mb-auto me-2" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<img
class="dialog-image mt-auto mb-auto me-2"
src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
<div class="flex-g">
<p><strong>Are you sure you want to remove this device?</strong></p>
<span>Id ${this.deviceID} : ${this.name ?? this.prefabName}</span>
</div>
</div>
<div slot="footer">
<sl-button variant="primary" autofocus @click=${this._closeRemoveDialog}>Close</sl-button>
<sl-button variant="danger" @click=${this._removeDialogRemove}>Remove</sl-button>
<sl-button
variant="primary"
autofocus
@click=${this._closeRemoveDialog}
>Close</sl-button
>
<sl-button variant="danger" @click=${this._removeDialogRemove}
>Remove</sl-button
>
</div>
</sl-dialog>
`;
@@ -398,22 +459,24 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
@query(".remove-device-dialog") removeDialog: SlDialog;
_preventOverlayClose(event: CustomEvent) {
if (event.detail.source === 'overlay') {
if (event.detail.source === "overlay") {
event.preventDefault();
}
}
_closeRemoveDialog() {
this.removeDialog.hide()
this.removeDialog.hide();
}
_handleChangeID(e: CustomEvent) {
const input = e.target as SlInput;
const val = parseIntWithHexOrBinary(input.value);
if (!isNaN(val)) {
if (!window.VM.changeDeviceId(this.deviceID, val)) {
input.value = this.deviceID.toString();
}
window.VM.get().then(vm => {
if (!vm.changeDeviceId(this.deviceID, val)) {
input.value = this.deviceID.toString();
}
});
} else {
input.value = this.deviceID.toString();
}
@@ -422,20 +485,24 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
_handleChangeName(e: CustomEvent) {
const input = e.target as SlInput;
const name = input.value.length === 0 ? undefined : input.value;
if (!window.VM?.setDeviceName(this.deviceID, name)) {
input.value = this.name;
};
this.updateDevice();
window.VM.get().then(vm => {
if (!vm.setDeviceName(this.deviceID, name)) {
input.value = this.name;
}
this.updateDevice();
});
}
_handleChangeField(e: CustomEvent) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
if (!window.VM?.setDeviceField(this.deviceID, field, val, true)) {
input.value = this.fields.get(field).value.toString();
}
this.updateDevice();
window.VM.get().then((vm) => {
if (!vm.setDeviceField(this.deviceID, field, val, true)) {
input.value = this.fields.get(field).value.toString();
}
this.updateDevice();
});
}
_handleChangeSlotField(e: CustomEvent) {
@@ -443,26 +510,30 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
const slot = parseInt(input.getAttribute("slotIndex")!);
const field = input.getAttribute("key")! as SlotLogicType;
const val = parseNumber(input.value);
if (!window.VM?.setDeviceSlotField(this.deviceID, slot, field, val, true)) {
input.value = this.device.getSlotField(slot, field).toString();
}
this.updateDevice();
window.VM.get().then((vm) => {
if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) {
input.value = this.device.getSlotField(slot, field).toString();
}
this.updateDevice();
});
}
_handleDeviceRemoveButton(_e: Event) {
this.removeDialog.show()
this.removeDialog.show();
}
_removeDialogRemove() {
this.removeDialog.hide()
window.VM.removeDevice(this.deviceID)
this.removeDialog.hide();
window.VM.get().then((vm) => vm.removeDevice(this.deviceID));
}
_handleChangeConnection(e: CustomEvent) {
const select = e.target as SlSelect;
const conn = parseInt(select.getAttribute("key")!);
const val = select.value ? parseInt(select.value as string) : undefined;
window.VM.setDeviceConnection(this.deviceID, conn, val);
window.VM.get().then((vm) =>
vm.setDeviceConnection(this.deviceID, conn, val),
);
this.updateDevice();
}
@@ -470,7 +541,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
const select = e.target as SlSelect;
const pin = parseInt(select.getAttribute("key")!);
const val = select.value ? parseInt(select.value as string) : undefined;
window.VM.setDevicePin(this.deviceID, pin, val);
window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val));
this.updateDevice();
}
}
@@ -507,14 +578,16 @@ export class VMDeviceList extends BaseElement {
constructor() {
super();
this.devices = [...window.VM!.deviceIds];
this.devices = [...window.VM.vm.deviceIds];
}
connectedCallback(): void {
const root = super.connectedCallback();
window.VM?.addEventListener(
"vm-devices-update",
this._handleDevicesUpdate.bind(this),
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-devices-update",
this._handleDevicesUpdate.bind(this),
),
);
return root;
}
@@ -528,9 +601,12 @@ export class VMDeviceList extends BaseElement {
}
protected render(): HTMLTemplateResult {
const deviceCards: HTMLTemplateResult[] = this.filteredDeviceIds.map(
(id, _index, _ids) =>
html`<vm-device-card .deviceID=${id} class="device-list-card" > </vm-device-card>`,
const deviceCards = repeat(
this.filteredDeviceIds,
(id) => id,
(id) =>
html`<vm-device-card .deviceID=${id} class="device-list-card">
</vm-device-card>`,
);
const result = html`
<div class="header">
@@ -538,7 +614,12 @@ export class VMDeviceList extends BaseElement {
Devices:
<sl-badge variant="neutral" pill>${this.devices.length}</sl-badge>
</span>
<sl-input class="device-filter-input" placeholder="Filter Devices" clearable @sl-input=${this._handleFilterInput}>
<sl-input
class="device-filter-input"
placeholder="Filter Devices"
clearable
@sl-input=${this._handleFilterInput}
>
<sl-icon slot="suffix" name="search"></sl-icon>"
</sl-input>
<vm-add-device-button class="ms-auto"></vm-add-device-button>
@@ -588,7 +669,7 @@ export class VMDeviceList extends BaseElement {
if (this._filter) {
const datapoints: [string, number][] = [];
for (const device_id of this.devices) {
const device = window.VM.devices.get(device_id);
const device = window.VM.vm.devices.get(device_id);
if (device) {
if (typeof device.name !== "undefined") {
datapoints.push([device.name, device.id]);
@@ -720,9 +801,11 @@ export class VMAddDeviceButton extends BaseElement {
connectedCallback(): void {
const root = super.connectedCallback();
window.VM!.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
),
);
return root;
}
@@ -734,7 +817,11 @@ export class VMAddDeviceButton extends BaseElement {
renderSearchResults(): HTMLTemplateResult {
const renderedResults: HTMLTemplateResult[] = this._searchResults?.map(
(result) => html`
<vm-device-template prefab_name=${result.name} class="card" @add-device-template=${this._handleDeviceAdd}>
<vm-device-template
prefab_name=${result.name}
class="card"
@add-device-template=${this._handleDeviceAdd}
>
</vm-device-template>
`,
);
@@ -747,20 +834,33 @@ export class VMAddDeviceButton extends BaseElement {
render() {
return html`
<sl-button variant="neutral" outline pill @click=${this._handleAddButtonClick}>
<sl-button
variant="neutral"
outline
pill
@click=${this._handleAddButtonClick}
>
Add Device
</sl-button>
<sl-drawer class="add-device-drawer" placement="bottom" no-header>
<sl-input class="device-search-input" autofocus placeholder="Search For Device" clearable
@sl-input=${this._handleSearchInput}>
<sl-input
class="device-search-input"
autofocus
placeholder="Search For Device"
clearable
@sl-input=${this._handleSearchInput}
>
<span slot="prefix">Search Structures</span>
<sl-icon slot="suffix" name="search"></sl-icon>"
</sl-input>
<div class="search-results">${this.renderSearchResults()}</div>
<sl-button slot="footer" variant="primary" @click=${()=> {
this.drawer.hide();
<sl-button
slot="footer"
variant="primary"
@click=${() => {
this.drawer.hide();
}}
>
>
Close
</sl-button>
</sl-drawer>
@@ -780,7 +880,7 @@ export class VMAddDeviceButton extends BaseElement {
_handleAddButtonClick() {
this.drawer.show();
(this.drawer.querySelector('.device-search-input') as SlInput).select();
(this.drawer.querySelector(".device-search-input") as SlInput).select();
}
}
@@ -826,7 +926,8 @@ export class VmDeviceTemplate extends BaseElement {
constructor() {
super();
this.deviceDB = window.VM!.db;
const that = this;
window.VM.get().then((vm) => (that.deviceDB = vm.db));
}
get deviceDB(): DeviceDB {
@@ -900,9 +1001,11 @@ export class VmDeviceTemplate extends BaseElement {
connectedCallback(): void {
super.connectedCallback();
window.VM!.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
),
);
}
@@ -914,12 +1017,18 @@ export class VmDeviceTemplate extends BaseElement {
const fields = Object.entries(this.fields);
return html`
${fields.map(([name, field], _index, _fields) => {
return html`
<sl-input key="${name}" value="${field.value}" size="small" @sl-change=${this._handleChangeField} ?disabled=${name==="PrefabHash"} >
<span slot="prefix">${name}</span>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`;
return html`
<sl-input
key="${name}"
value="${field.value}"
size="small"
@sl-change=${this._handleChangeField}
?disabled=${name === "PrefabHash"}
>
<span slot="prefix">${name}</span>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`;
})}
`;
}
@@ -947,25 +1056,33 @@ export class VmDeviceTemplate extends BaseElement {
return html``;
}
renderNetworks(): HTMLTemplateResult {
const vmNetworks = window.VM!.networks;
renderNetworks() {
const vm = window.VM.vm;
const vmNetworks = vm.networks;
const connections = this.connections;
return html`
<div class="networks">
${connections.map((connection, index, _conns) => {
const conn =
typeof connection === "object" ? connection.CableNetwork : null;
return html`
<sl-select hoist placement="top" clearable key=${index} value=${conn?.net} ?disabled=${conn===null}
@sl-change=${this._handleChangeConnection}>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
const conn =
typeof connection === "object" ? connection.CableNetwork : null;
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn?.net}
?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}
>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
})}
</div>
`;
@@ -990,16 +1107,23 @@ export class VmDeviceTemplate extends BaseElement {
<sl-card class="template-card">
<div class="header" slot="header">
<sl-tooltip content="${device?.name}">
<img class="image" src="img/stationpedia/${device?.name}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<img
class="image"
src="img/stationpedia/${device?.name}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
</sl-tooltip>
<div class="vstack">
<span class="prefab-title">${device.title}</span>
<span class="prefab-name"><small>${device?.name}</small></span>
<span class="prefab-hash"><small>${device?.hash}</small></span>
</div>
<sl-button class="ms-auto mt-auto mb-auto" pill variant="success" @click=${this._handleAddButtonClick}>Add <sl-icon slot="prefix"
name="plus-lg"></sl-icon>
<sl-button
class="ms-auto mt-auto mb-auto"
pill
variant="success"
@click=${this._handleAddButtonClick}
>Add <sl-icon slot="prefix" name="plus-lg"></sl-icon>
</sl-button>
</div>
<div class="card-body">
@@ -1013,7 +1137,9 @@ export class VmDeviceTemplate extends BaseElement {
<sl-tab-panel name="fields">${this.renderFields()}</sl-tab-panel>
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
<!-- <sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel> -->
<sl-tab-panel name="networks">${this.renderNetworks()}</sl-tab-panel>
<sl-tab-panel name="networks"
>${this.renderNetworks()}</sl-tab-panel
>
<!-- <sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel> -->
</sl-tab-group>
</div>
@@ -1032,7 +1158,7 @@ export class VmDeviceTemplate extends BaseElement {
connections: this.connections,
fields: this.fields,
};
window.VM.addDeviceFromTemplate(template);
window.VM.vm.addDeviceFromTemplate(template);
// reset state for new device
this.setupState();

View File

@@ -1,13 +1,16 @@
import { DeviceRef, DeviceTemplate, LogicType, SlotLogicType, VMRef, init } from "ic10emu_wasm";
import {
DeviceRef,
DeviceTemplate,
FrozenVM,
LogicType,
SlotLogicType,
VMRef,
init,
} from "ic10emu_wasm";
import { DeviceDB } from "./device_db";
import "./base_device";
declare global {
interface Window {
VM?: VirtualMachine;
}
}
import { fromJson, toJson } from "../utils";
import { App } from "../app";
export interface ToastMessage {
variant: "warning" | "danger" | "success" | "primary" | "neutral";
icon: string;
@@ -24,11 +27,13 @@ class VirtualMachine extends EventTarget {
accessor db: DeviceDB;
dbPromise: Promise<{ default: DeviceDB }>;
constructor() {
super();
const vm = init();
private app: App;
window.VM = this;
constructor(app: App) {
super();
this.app = app;
const vm = init();
window.VM.set(this);
this.ic10vm = vm;
@@ -74,7 +79,7 @@ class VirtualMachine extends EventTarget {
}
get activeIC() {
return this._ics.get(window.App!.session.activeIC);
return this._ics.get(this.app.session.activeIC);
}
visibleDevices(source: number) {
@@ -126,7 +131,7 @@ class VirtualMachine extends EventTarget {
}
updateCode() {
const progs = window.App!.session.programs;
const progs = this.app.session.programs;
for (const id of progs.keys()) {
const attempt = Date.now().toString(16);
const ic = this._ics.get(id);
@@ -136,13 +141,13 @@ class VirtualMachine extends EventTarget {
console.time(`CompileProgram_${id}_${attempt}`);
this.ics.get(id)!.setCodeInvalid(progs.get(id)!);
const compiled = this.ics.get(id)?.program!;
window.App?.session.setProgramErrors(id, compiled.errors);
this.app.session.setProgramErrors(id, compiled.errors);
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: id }),
);
} catch (err) {
this.handleVmError(err);
} finally{
} finally {
console.timeEnd(`CompileProgram_${id}_${attempt}`);
}
}
@@ -205,7 +210,7 @@ class VirtualMachine extends EventTarget {
new CustomEvent("vm-device-modified", { detail: device.id }),
);
if (typeof device.ic !== "undefined") {
window.App!.session.setActiveLine(device.id, device.ip!);
this.app.session.setActiveLine(device.id, device.ip!);
}
}
@@ -225,8 +230,8 @@ class VirtualMachine extends EventTarget {
try {
this.ic10vm.changeDeviceId(old_id, new_id);
this.updateDevices();
if (window.App.session.activeIC === old_id) {
window.App.session.activeIC = new_id;
if (this.app.session.activeIC === old_id) {
this.app.session.activeIC = new_id;
}
return true;
} catch (err) {
@@ -264,16 +269,23 @@ class VirtualMachine extends EventTarget {
if (device) {
try {
device.setName(name);
this.dispatchEvent(new CustomEvent("vm-device-modified", { detail: id }));
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: id }),
);
return true;
} catch(e) {
} catch (e) {
this.handleVmError(e);
}
}
return false;
}
setDeviceField(id: number, field: LogicType, val: number, force?: boolean): boolean {
setDeviceField(
id: number,
field: LogicType,
val: number,
force?: boolean,
): boolean {
force = force ?? false;
const device = this._devices.get(id);
if (device) {
@@ -288,7 +300,13 @@ class VirtualMachine extends EventTarget {
return false;
}
setDeviceSlotField(id: number, slot: number, field: SlotLogicType, val: number, force?: boolean): boolean {
setDeviceSlotField(
id: number,
slot: number,
field: SlotLogicType,
val: number,
force?: boolean,
): boolean {
force = force ?? false;
const device = this._devices.get(id);
if (device) {
@@ -303,18 +321,22 @@ class VirtualMachine extends EventTarget {
return false;
}
setDeviceConnection(id: number, conn: number, val: number | undefined): boolean {
setDeviceConnection(
id: number,
conn: number,
val: number | undefined,
): boolean {
const device = this._devices.get(id);
if (typeof device !== "undefined") {
try {
this.ic10vm.setDeviceConnection(id, conn, val);
this.updateDevice(device);
return true
return true;
} catch (err) {
this.handleVmError(err);
}
}
return false
return false;
}
setDevicePin(id: number, pin: number, val: number | undefined): boolean {
@@ -367,6 +389,33 @@ class VirtualMachine extends EventTarget {
return false;
}
}
saveVMState(): FrozenVM {
return this.ic10vm.saveVMState();
}
restoreVMState(state: FrozenVM) {
try {
this.ic10vm.restoreVMState(state);
this._devices = new Map();
this._ics = new Map();
this.updateDevices();
} catch (e) {
this.handleVmError(e);
}
}
getPrograms() {
const programs: [number, string][] = Array.from(this._ics.entries()).map(
([id, ic]) => [id, ic.code],
);
return programs;
}
}
export interface VMState {
activeIC: number;
vm: FrozenVM;
}
export { VirtualMachine };

View File

@@ -104,6 +104,6 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
const input = e.target as SlInput;
const index = parseInt(input.getAttribute("key")!);
const val = parseNumber(input.value);
window.VM!.setRegister(index, val);
window.VM.vm.setRegister(index, val);
}
}

View File

@@ -87,6 +87,6 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
const input = e.target as SlInput;
const index = parseInt(input.getAttribute("key")!);
const val = parseNumber(input.value);
window.VM!.setStack(index, val);
window.VM.get().then(vm => vm.setStack(index, val));
}
}

View File

@@ -66,7 +66,7 @@ export class VMUI extends BaseElement {
connectedCallback(): void {
super.connectedCallback();
window.VM.addEventListener("vm-message", this._handleVMMessage.bind(this));
window.VM.get().then(vm => vm.addEventListener("vm-message", this._handleVMMessage.bind(this)));
}
_handleVMMessage(e: CustomEvent) {

View File

@@ -6,7 +6,7 @@ export function connectionFromDeviceDBConnection(conn: DeviceDBConnection): Conn
if (CableNetworkTypes.includes(conn.typ)) {
return {
CableNetwork: {
net: window.VM?.ic10vm.defaultNetwork,
net: window.VM.vm.ic10vm.defaultNetwork,
typ: conn.typ
}
};