import { DeviceRef, DeviceTemplate, FrozenVM, LogicType, SlotLogicType, SlotOccupantTemplate, VMRef, init, } from "ic10emu_wasm"; import { DeviceDB } from "./device_db"; import "./base_device"; import "./device"; import { App } from "app"; export interface ToastMessage { variant: "warning" | "danger" | "success" | "primary" | "neutral"; icon: string; title: string; msg: string; id: string; } class VirtualMachine extends EventTarget { ic10vm: VMRef; _devices: Map; _ics: Map; db: DeviceDB; dbPromise: Promise<{ default: DeviceDB }>; private app: App; constructor(app: App) { super(); this.app = app; const vm = init(); window.VM.set(this); this.ic10vm = vm; this._devices = new Map(); this._ics = new Map(); this.dbPromise = import("../../../data/database.json", { assert: { type: "json" }, }) as Promise<{ default: DeviceDB }>; this.dbPromise.then((module) => this.setupDeviceDatabase(module.default as DeviceDB), ); this.updateDevices(); this.updateCode(); } get devices() { return this._devices; } get deviceIds() { const ids = Array.from(this.ic10vm.devices); ids.sort(); return ids; } get ics() { return this._ics; } get icIds() { return Array.from(this.ic10vm.ics); } get networks() { return Array.from(this.ic10vm.networks); } get defaultNetwork() { return this.ic10vm.defaultNetwork; } get activeIC() { return this._ics.get(this.app.session.activeIC); } visibleDevices(source: number) { const ids = Array.from(this.ic10vm.visibleDevices(source)); return ids.map((id, _index) => this._devices.get(id)!); } updateDevices() { var update_flag = false; const device_ids = this.ic10vm.devices; for (const id of device_ids) { if (!this._devices.has(id)) { this._devices.set(id, this.ic10vm.getDevice(id)!); update_flag = true; } } for (const id of this._devices.keys()) { if (!device_ids.includes(id)) { this._devices.delete(id); update_flag = true; } } for (const [id, device] of this._devices) { if (typeof device.ic !== "undefined") { if (!this._ics.has(id)) { this._ics.set(id, device); update_flag = true; } } } for (const id of this._ics.keys()) { if (!this._devices.has(id)) { this._ics.delete(id); update_flag = true; } } if (update_flag) { const ids = Array.from(device_ids); ids.sort(); this.dispatchEvent( new CustomEvent("vm-devices-update", { detail: ids, }), ); this.app.session.save(); } } updateCode() { const progs = this.app.session.programs; for (const id of progs.keys()) { const attempt = Date.now().toString(16); const ic = this._ics.get(id); const prog = progs.get(id); if (ic && prog && ic.code !== prog) { try { console.time(`CompileProgram_${id}_${attempt}`); this.ics.get(id)!.setCodeInvalid(progs.get(id)!); const compiled = this.ics.get(id)?.program!; this.app.session.setProgramErrors(id, compiled.errors); this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: id }), ); } catch (err) { this.handleVmError(err); } finally { console.timeEnd(`CompileProgram_${id}_${attempt}`); } } } this.update(false); } step() { const ic = this.activeIC; if (ic) { try { ic.step(false); } catch (err) { this.handleVmError(err); } this.update(); this.dispatchEvent( new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }), ); } } run() { const ic = this.activeIC; if (ic) { try { ic.run(false); } catch (err) { this.handleVmError(err); } this.update(); this.dispatchEvent( new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }), ); } } reset() { const ic = this.activeIC; if (ic) { ic.reset(); this.update(); } } update(save: boolean = true) { this.updateDevices(); this.ic10vm.lastOperationModified.forEach((id, _index, _modifiedIds) => { if (this.devices.has(id)) { this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: id }), ); } }, this); this.updateDevice(this.activeIC, save); if (save) this.app.session.save(); } updateDevice(device: DeviceRef, save: boolean = true) { this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: device.id }), ); if (typeof device.ic !== "undefined") { this.app.session.setActiveLine(device.id, device.ip!); } if (save) this.app.session.save(); } handleVmError(err: Error) { console.log("Error in Virtual Machine", err); const message: ToastMessage = { variant: "danger", icon: "bug", title: `Error in Virtual Machine ${err.name}`, msg: err.message, id: Date.now().toString(16), }; this.dispatchEvent(new CustomEvent("vm-message", { detail: message })); } changeDeviceId(old_id: number, new_id: number): boolean { try { this.ic10vm.changeDeviceId(old_id, new_id); this.updateDevices(); if (this.app.session.activeIC === old_id) { this.app.session.activeIC = new_id; } return true; } catch (err) { this.handleVmError(err); return false; } } setRegister(index: number, val: number): boolean { const ic = this.activeIC!; try { ic.setRegister(index, val); this.updateDevice(ic); return true; } catch (err) { this.handleVmError(err); return false; } } setStack(addr: number, val: number): boolean { const ic = this.activeIC!; try { ic!.setStack(addr, val); this.updateDevice(ic); return true; } catch (err) { this.handleVmError(err); return false; } } setDeviceName(id: number, name: string): boolean { const device = this._devices.get(id); if (device) { try { device.setName(name); this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: id }), ); this.app.session.save(); return true; } catch (e) { this.handleVmError(e); } } return false; } setDeviceField( id: number, field: LogicType, val: number, force?: boolean, ): boolean { force = force ?? false; const device = this._devices.get(id); if (device) { try { device.setField(field, val, force); this.updateDevice(device); return true; } catch (err) { this.handleVmError(err); } } return false; } setDeviceSlotField( id: number, slot: number, field: SlotLogicType, val: number, force?: boolean, ): boolean { force = force ?? false; const device = this._devices.get(id); if (device) { try { device.setSlotField(slot, field, val, false); this.updateDevice(device); return true; } catch (err) { this.handleVmError(err); } } return false; } 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; } catch (err) { this.handleVmError(err); } } return false; } setDevicePin(id: number, pin: number, val: number | undefined): boolean { const device = this._devices.get(id); if (typeof device !== "undefined") { try { this.ic10vm.setPin(id, pin, val); this.updateDevice(device); return true; } catch (err) { this.handleVmError(err); } } return false; } setupDeviceDatabase(db: DeviceDB) { this.db = db; console.log("Loaded Device Database", this.db); this.dispatchEvent( new CustomEvent("vm-device-db-loaded", { detail: this.db }), ); } addDeviceFromTemplate(template: DeviceTemplate): boolean { try { console.log("adding device", template); const id = this.ic10vm.addDeviceFromTemplate(template); this._devices.set(id, this.ic10vm.getDevice(id)!); const device_ids = this.ic10vm.devices; this.dispatchEvent( new CustomEvent("vm-devices-update", { detail: Array.from(device_ids), }), ); this.app.session.save(); return true; } catch (err) { this.handleVmError(err); return false; } } removeDevice(id: number): boolean { try { this.ic10vm.removeDevice(id); this.updateDevices(); return true; } catch (err) { this.handleVmError(err); return false; } } setDeviceSlotOccupant(id: number, index: number, template: SlotOccupantTemplate): boolean { const device = this._devices.get(id); if (typeof device !== "undefined") { try { console.log("setting slot occupant", template); this.ic10vm.setSlotOccupant(id, index, template); this.updateDevice(device); return true; } catch (err) { this.handleVmError(err); } } 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 { VirtualMachine };