From 9a374a4f73074efc2a3389939a5de78044ec512c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:06:19 -0700 Subject: [PATCH] save VM state --- Cargo.lock | 1 + ic10emu/Cargo.toml | 1 + ic10emu/src/device.rs | 114 ++++++- ic10emu/src/interpreter.rs | 70 ++++- ic10emu/src/network.rs | 46 ++- ic10emu/src/vm.rs | 165 ++++++---- ic10emu_wasm/src/lib.rs | 36 ++- ic10emu_wasm/src/types.ts | 30 ++ www/src/ts/app/app.ts | 18 +- www/src/ts/editor/index.ts | 13 +- www/src/ts/index.ts | 65 ---- www/src/ts/main.ts | 81 ++++- www/src/ts/session.ts | 124 ++++++-- www/src/ts/utils.ts | 7 + www/src/ts/virtual_machine/base_device.ts | 20 +- www/src/ts/virtual_machine/controls.ts | 10 +- www/src/ts/virtual_machine/device.ts | 354 +++++++++++++++------- www/src/ts/virtual_machine/index.ts | 101 ++++-- www/src/ts/virtual_machine/registers.ts | 2 +- www/src/ts/virtual_machine/stack.ts | 2 +- www/src/ts/virtual_machine/ui.ts | 2 +- www/src/ts/virtual_machine/utils.ts | 2 +- 22 files changed, 896 insertions(+), 368 deletions(-) delete mode 100644 www/src/ts/index.ts diff --git a/Cargo.lock b/Cargo.lock index 874cafc..79c415b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,7 @@ dependencies = [ "rand", "regex", "serde", + "serde_with", "strum", "strum_macros", "thiserror", diff --git a/ic10emu/Cargo.toml b/ic10emu/Cargo.toml index 4e04a89..60d5dbc 100644 --- a/ic10emu/Cargo.toml +++ b/ic10emu/Cargo.toml @@ -16,6 +16,7 @@ phf = "0.11.2" rand = "0.8.5" regex = "1.10.3" serde = { version = "1.0.197", features = ["derive"] } +serde_with = "3.7.0" strum = { version = "0.26.2", features = ["derive", "phf", "strum_macros"] } strum_macros = "0.26.2" thiserror = "1.0.58" diff --git a/ic10emu/src/device.rs b/ic10emu/src/device.rs index d6baebd..a2fa7a4 100644 --- a/ic10emu/src/device.rs +++ b/ic10emu/src/device.rs @@ -1,7 +1,12 @@ use crate::{ - grammar::{LogicType, ReagentMode, SlotLogicType}, interpreter::{ICError, ICState}, network::{CableConnectionType, Connection}, vm::VM + grammar::{LogicType, ReagentMode, SlotLogicType}, + interpreter::{ICError, ICState}, + network::{CableConnectionType, Connection}, + vm::VM, }; -use std::collections::HashMap; +use std::{collections::HashMap, ops::Deref}; + +use itertools::Itertools; use serde::{Deserialize, Serialize}; use strum_macros::{AsRefStr, EnumIter, EnumString}; @@ -486,7 +491,6 @@ pub enum SlotType { None = 0, } - #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Prefab { pub name: String, @@ -512,18 +516,7 @@ pub struct Device { pub reagents: HashMap>, pub ic: Option, pub connections: Vec, - pub fields: HashMap, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct DeviceTemplate { - pub id: Option, - pub name: Option, - pub prefab_name: Option, - pub slots: Vec, - // pub reagents: HashMap>, - pub connections: Vec, - pub fields: HashMap, + fields: HashMap, } impl Device { @@ -925,3 +918,94 @@ impl Device { }) } } + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct DeviceTemplate { + pub id: Option, + pub name: Option, + pub prefab_name: Option, + pub slots: Vec, + // pub reagents: HashMap>, + pub connections: Vec, + pub fields: HashMap, +} + +impl Device { + /// create a devive from a template and return the device, does not create it's own IC + pub fn from_template(template: DeviceTemplate, mut id_fn: F) -> Self + where + F: FnMut() -> u32, + { + // id_fn *must* be captured not moved + #[allow(clippy::redundant_closure)] + let device_id = template.id.unwrap_or_else(|| id_fn()); + let name_hash = template + .name + .as_ref() + .map(|name| const_crc32::crc32(name.as_bytes()) as i32); + + #[allow(clippy::redundant_closure)] + let slots = template + .slots + .into_iter() + .map(|slot| Slot { + typ: slot.typ, + occupant: slot + .occupant + .map(|occupant| SlotOccupant::from_template(occupant, || id_fn())), + }) + .collect_vec(); + + let ic = slots + .iter() + .find_map(|slot| { + if slot.typ == SlotType::ProgrammableChip && slot.occupant.is_some() { + Some(slot.occupant.clone()).flatten() + } else { + None + } + }) + .map(|occupant| occupant.id); + + let fields = template.fields; + + Device { + id: device_id, + name: template.name, + name_hash, + prefab: template.prefab_name.map(|name| Prefab::new(&name)), + slots, + // reagents: template.reagents, + reagents: HashMap::new(), + ic, + connections: template.connections, + fields, + } + } +} + +impl From for DeviceTemplate +where + T: Deref, +{ + fn from(device: T) -> Self { + DeviceTemplate { + id: Some(device.id), + name: device.name.clone(), + prefab_name: device.prefab.as_ref().map(|prefab| prefab.name.clone()), + slots: device + .slots + .iter() + .map(|slot| SlotTemplate { + typ: slot.typ, + occupant: slot.occupant.as_ref().map(|occupant| SlotOccupantTemplate { + id: Some(occupant.id), + fields: occupant.get_fields(), + }), + }) + .collect_vec(), + connections: device.connections.clone(), + fields: device.fields.clone(), + } + } +} diff --git a/ic10emu/src/interpreter.rs b/ic10emu/src/interpreter.rs index 469386c..933f256 100644 --- a/ic10emu/src/interpreter.rs +++ b/ic10emu/src/interpreter.rs @@ -1,6 +1,6 @@ use core::f64; use serde::{Deserialize, Serialize}; -use std::string::ToString; +use std::{ops::Deref, string::ToString}; use std::{ collections::{HashMap, HashSet}, error::Error, @@ -12,7 +12,12 @@ use itertools::Itertools; use time::format_description; -use crate::{grammar::{self, ParseError}, vm::VM}; +use crate::{ + grammar::{self, ParseError}, + vm::VM, +}; + +use serde_with::serde_as; use thiserror::Error; @@ -113,7 +118,7 @@ pub enum ICError { #[error("channel index out of range '{0}'")] ChannelIndexOutOfRange(usize), #[error("slot has no occupant")] - SlotNotOccupied + SlotNotOccupied, } impl ICError { @@ -189,6 +194,65 @@ pub struct IC { pub state: ICState, } + +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrozenIC { + pub device: u32, + pub id: u32, + pub registers: [f64; 18], + /// Instruction Pointer + pub ip: u32, + /// Instruction Count since last yield + pub ic: u16, + #[serde_as(as = "[_; 512]")] + pub stack: [f64; 512], + pub aliases: HashMap, + pub defines: HashMap, + pub pins: [Option; 6], + pub state: ICState, + pub code: String, +} + +impl From for FrozenIC + where T: Deref +{ + fn from(ic: T) -> Self { + FrozenIC { + device: ic.device, + id: ic.id, + registers: ic.registers, + ip: ic.ip, + ic: ic.ic, + stack: ic.stack, + aliases: ic.aliases.clone(), + defines: ic.defines.clone(), + pins: ic.pins, + state: ic.state.clone(), + code: ic.code.clone(), + } + } +} + +impl From for IC { + fn from(value: FrozenIC) -> Self { + IC { + device: value.device, + id: value.id, + registers: value.registers, + ip: value.ip, + ic: value.ic, + stack: value.stack, + aliases: value.aliases, + defines: value.defines, + pins: value.pins, + state: value.state, + code: value.code.clone(), + program: Program::from_code_with_invalid(&value.code), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Program { pub instructions: Vec, diff --git a/ic10emu/src/network.rs b/ic10emu/src/network.rs index 9e36a4e..7823675 100644 --- a/ic10emu/src/network.rs +++ b/ic10emu/src/network.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{collections::HashSet, ops::Deref}; use serde::{Deserialize, Serialize}; use strum_macros::{AsRefStr, EnumIter}; @@ -85,17 +85,41 @@ impl Connection { #[derive(Debug, Serialize, Deserialize)] pub struct Network { + pub id: u32, pub devices: HashSet, pub power_only: HashSet, pub channels: [f64; 8], } -impl Default for Network { - fn default() -> Self { +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrozenNetwork { + pub id: u32, + pub devices: Vec, + pub power_only: Vec, + pub channels: [f64; 8], +} + +impl From for FrozenNetwork +where + T: Deref, +{ + fn from(value: T) -> Self { + FrozenNetwork { + id: value.id, + devices: value.devices.iter().copied().collect_vec(), + power_only: value.power_only.iter().copied().collect_vec(), + channels: value.channels, + } + } +} + +impl From for Network { + fn from(value: FrozenNetwork) -> Self { Network { - devices: HashSet::new(), - power_only: HashSet::new(), - channels: [f64::NAN; 8], + id: value.id, + devices: value.devices.into_iter().collect(), + power_only: value.power_only.into_iter().collect(), + channels: value.channels, } } } @@ -107,6 +131,16 @@ pub enum NetworkError { } impl Network { + + pub fn new(id: u32) -> Self { + Network { + id, + devices: HashSet::new(), + power_only: HashSet::new(), + channels: [f64::NAN; 8], + } + } + pub fn contains(&self, id: &u32) -> bool { self.devices.contains(id) || self.power_only.contains(id) } diff --git a/ic10emu/src/vm.rs b/ic10emu/src/vm.rs index 42e8f85..832ee65 100644 --- a/ic10emu/src/vm.rs +++ b/ic10emu/src/vm.rs @@ -1,10 +1,14 @@ use crate::{ + device::{Device, DeviceTemplate}, grammar::{BatchMode, LogicType, SlotLogicType}, - interpreter::{self, ICError, LineError}, - device::{Device, DeviceTemplate, Prefab, Slot, SlotOccupant, SlotType}, - network::{CableConnectionType, Connection, Network}, + interpreter::{self, FrozenIC, ICError, LineError}, + network::{CableConnectionType, Connection, FrozenNetwork, Network}, +}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, }; -use std::{cell::RefCell, collections::{HashMap, HashSet}, rc::Rc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -41,7 +45,7 @@ pub struct VM { pub networks: HashMap>>, pub default_network: u32, id_space: IdSpace, - network_id_gen: IdSpace, + network_id_space: IdSpace, random: Rc>, /// list of device id's touched on the last operation @@ -58,9 +62,9 @@ impl VM { pub fn new() -> Self { let id_gen = IdSpace::default(); let mut network_id_space = IdSpace::default(); - let default_network = Rc::new(RefCell::new(Network::default())); - let mut networks = HashMap::new(); let default_network_key = network_id_space.next(); + let default_network = Rc::new(RefCell::new(Network::new(default_network_key))); + let mut networks = HashMap::new(); networks.insert(default_network_key, default_network); let mut vm = VM { @@ -69,7 +73,7 @@ impl VM { networks, default_network: default_network_key, id_space: id_gen, - network_id_gen: network_id_space, + network_id_space, random: Rc::new(RefCell::new(crate::rand_mscorlib::Random::new())), operation_modified: RefCell::new(Vec::new()), }; @@ -208,70 +212,24 @@ impl VM { } // collect the id's this template wants to use - let mut to_use_ids = template + let to_use_ids = template .slots .iter() .filter_map(|slot| slot.occupant.as_ref().and_then(|occupant| occupant.id)) .collect_vec(); - let device_id = { - // attempt to use all the idea at once to error without needing to clean up. - if let Some(id) = &template.id { - to_use_ids.push(*id); - self.id_space.use_ids(&to_use_ids)?; - *id - } else { - self.id_space.use_ids(&to_use_ids)?; - self.id_space.next() - } - }; - let name_hash = template - .name - .as_ref() - .map(|name| const_crc32::crc32(name.as_bytes()) as i32); + // use those ids or fail + self.id_space.use_ids(&to_use_ids)?; - let slots = template - .slots - .into_iter() - .map(|slot| Slot { - typ: slot.typ, - occupant: slot - .occupant - .map(|occupant| SlotOccupant::from_template(occupant, || self.id_space.next())), - }) - .collect_vec(); + let device = Device::from_template(template, || self.id_space.next()); + let device_id: u32 = device.id; - let ic = slots - .iter() - .find_map(|slot| { - if slot.typ == SlotType::ProgrammableChip && slot.occupant.is_some() { - Some(slot.occupant.clone()).flatten() - } else { - None - } - }) - .map(|occupant| occupant.id); - - if let Some(ic_id) = &ic { + // if this device says it has an IC make it so. + if let Some(ic_id) = &device.ic { let chip = interpreter::IC::new(*ic_id, device_id); self.ics.insert(*ic_id, Rc::new(RefCell::new(chip))); } - let fields = template.fields; - - let device = Device { - id: device_id, - name: template.name, - name_hash, - prefab: template.prefab_name.map(|name| Prefab::new(&name)), - slots, - // reagents: template.reagents, - reagents: HashMap::new(), - ic, - connections: template.connections, - fields, - }; - device.connections.iter().for_each(|conn| { if let Connection::CableNetwork { net: Some(net), @@ -298,9 +256,9 @@ impl VM { } pub fn add_network(&mut self) -> u32 { - let next_id = self.network_id_gen.next(); + let next_id = self.network_id_space.next(); self.networks - .insert(next_id, Rc::new(RefCell::new(Network::default()))); + .insert(next_id, Rc::new(RefCell::new(Network::new(next_id)))); next_id } @@ -470,7 +428,7 @@ impl VM { .filter(move |(id, device)| { device .borrow() - .fields + .get_fields(self) .get(&LogicType::PrefabHash) .is_some_and(|f| f.value == prefab_hash) && (name.is_none() @@ -790,6 +748,81 @@ impl VM { self.id_space.free_id(id); Ok(()) } + + pub fn save_vm_state(&self) -> FrozenVM { + FrozenVM { + ics: self.ics.values().map(|ic| ic.borrow().into()).collect(), + devices: self + .devices + .values() + .map(|device| device.borrow().into()) + .collect(), + networks: self + .networks + .values() + .map(|network| network.borrow().into()) + .collect(), + default_network: self.default_network, + } + } + + pub fn restore_vm_state(&mut self, state: FrozenVM) -> Result<(), VMError> { + self.ics.clear(); + self.devices.clear(); + self.networks.clear(); + self.id_space.reset(); + self.network_id_space.reset(); + + // ic ids sould be in slot occupants, don't duplicate + let to_use_ids = state + .devices + .iter() + .map(|template| { + let mut ids = template + .slots + .iter() + .filter_map(|slot| slot.occupant.as_ref().and_then(|occupant| occupant.id)) + .collect_vec(); + if let Some(id) = template.id { + ids.push(id); + } + ids + }) + .concat(); + self.id_space.use_ids(&to_use_ids)?; + + self.network_id_space + .use_ids(&state.networks.iter().map(|net| net.id).collect_vec())?; + + self.ics = state + .ics + .into_iter() + .map(|ic| (ic.id, Rc::new(RefCell::new(ic.into())))) + .collect(); + self.devices = state + .devices + .into_iter() + .map(|template| { + let device = Device::from_template(template, || self.id_space.next()); + (device.id, Rc::new(RefCell::new(device))) + }) + .collect(); + self.networks = state + .networks + .into_iter() + .map(|network| (network.id, Rc::new(RefCell::new(network.into())))) + .collect(); + self.default_network = state.default_network; + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrozenVM { + pub ics: Vec, + pub devices: Vec, + pub networks: Vec, + pub default_network: u32, } impl BatchMode { @@ -877,4 +910,8 @@ impl IdSpace { pub fn free_id(&mut self, id: u32) { self.in_use.remove(&id); } + + pub fn reset(&mut self) { + self.in_use.clear(); + } } diff --git a/ic10emu_wasm/src/lib.rs b/ic10emu_wasm/src/lib.rs index 65c35b5..703501d 100644 --- a/ic10emu_wasm/src/lib.rs +++ b/ic10emu_wasm/src/lib.rs @@ -3,9 +3,9 @@ mod utils; mod types; use ic10emu::{ - grammar::{LogicType, SlotLogicType}, device::{Device, DeviceTemplate}, - vm::{VMError, VM}, + grammar::{LogicType, SlotLogicType}, + vm::{FrozenVM, VMError, VM}, }; use serde::{Deserialize, Serialize}; use types::{Registers, Stack}; @@ -217,6 +217,17 @@ impl DeviceRef { .unwrap() } + #[wasm_bindgen(getter, js_name = "code")] + pub fn get_code(&self) -> Option { + self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm + .borrow() + .ics + .get(ic) + .map(|ic| ic.borrow().code.clone()) + }) + } + #[wasm_bindgen(js_name = "step")] pub fn step_ic(&self, advance_ip_on_err: bool) -> Result { let id = self.device.borrow().id; @@ -297,7 +308,13 @@ impl DeviceRef { } #[wasm_bindgen(js_name = "setSlotField", skip_typescript)] - pub fn set_slot_field(&self, slot: f64, field: &str, value: f64, force: bool) -> Result<(), JsError> { + pub fn set_slot_field( + &self, + slot: f64, + field: &str, + value: f64, + force: bool, + ) -> Result<(), JsError> { let logic_typ = SlotLogicType::from_str(field)?; let mut device_ref = self.device.borrow_mut(); device_ref.set_slot_field(slot, logic_typ, value, &self.vm.borrow(), force)?; @@ -471,6 +488,19 @@ impl VMRef { pub fn remove_device(&self, id: u32) -> Result<(), JsError> { Ok(self.vm.borrow_mut().remove_device(id)?) } + + #[wasm_bindgen(js_name = "saveVMState", skip_typescript)] + pub fn save_vm_state(&self) -> JsValue { + let state = self.vm.borrow().save_vm_state(); + serde_wasm_bindgen::to_value(&state).unwrap() + } + + #[wasm_bindgen(js_name = "restoreVMState", skip_typescript)] + pub fn restore_vm_state(&self, state: JsValue) -> Result<(), JsError> { + let state: FrozenVM = serde_wasm_bindgen::from_value(state)?; + self.vm.borrow_mut().restore_vm_state(state)?; + Ok(()) + } } impl Default for VMRef { diff --git a/ic10emu_wasm/src/types.ts b/ic10emu_wasm/src/types.ts index 01120ac..0b1ec95 100644 --- a/ic10emu_wasm/src/types.ts +++ b/ic10emu_wasm/src/types.ts @@ -147,6 +147,36 @@ export interface DeviceTemplate { fields: { [key in LogicType]?: LogicField }; } +export interface FrozenIC { + device: number; + id: number; + registers: number[]; + ip: number; + ic: number[]; + stack: number[]; + aliases: Aliases; + defines: Defines; + pins: Pins; + state: string; + code: string; +} + +export interface FrozenNetwork { + id: number; + devices: number[]; + power_only: number[]; + channels: number[]; +} + +export interface FrozenVM { + ics: FrozenIC[]; + devices: DeviceTemplate[]; + networks: FrozenNetwork[]; + default_network: number; +} + export interface VMRef { addDeviceFromTemplate(template: DeviceTemplate): number; + saveVMState(): FrozenVM; + restoreVMState(state: FrozenVM): void; } diff --git a/www/src/ts/app/app.ts b/www/src/ts/app/app.ts index 033911e..94c610d 100644 --- a/www/src/ts/app/app.ts +++ b/www/src/ts/app/app.ts @@ -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" > - +
@@ -107,8 +107,4 @@ export class App extends BaseElement { } } -declare global { - interface Window { - App?: App; - } -} + diff --git a/www/src/ts/editor/index.ts b/www/src/ts/editor/index.ts index 7160ec0..1354bb0 100644 --- a/www/src/ts/editor/index.ts +++ b/www/src/ts/editor/index.ts @@ -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)); }); } } diff --git a/www/src/ts/index.ts b/www/src/ts/index.ts deleted file mode 100644 index 1c2dcee..0000000 --- a/www/src/ts/index.ts +++ /dev/null @@ -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 }, -// ); -// }); diff --git a/www/src/ts/main.ts b/www/src/ts/main.ts index 2d82601..c68a0d3 100644 --- a/www/src/ts/main.ts +++ b/www/src/ts/main.ts @@ -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 { + 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 { + 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"); diff --git a/www/src/ts/session.ts b/www/src/ts/session.ts index ebb7556..778cd09 100644 --- a/www/src/ts/session.ts +++ b/www/src/ts/session.ts @@ -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; - _errors: Map; - _activeIC: number; - _activeLines: Map; - _activeLine: number; - _save_timeout?: ReturnType; - constructor() { + private _programs: Map; + private _errors: Map; + private _activeIC: number; + private _activeLines: Map; + private _save_timeout?: ReturnType; + 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 { 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); diff --git a/www/src/ts/utils.ts b/www/src/ts/utils.ts index 033f4b8..35ecf55 100644 --- a/www/src/ts/utils.ts +++ b/www/src/ts/utils.ts @@ -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; diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts index 3705128..86a13d9 100644 --- a/www/src/ts/virtual_machine/base_device.ts +++ b/www/src/ts/virtual_machine/base_device.ts @@ -79,14 +79,14 @@ export const VMDeviceMixin = >( 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 = >( } 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 = >( 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 = >( 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(); } diff --git a/www/src/ts/virtual_machine/controls.ts b/www/src/ts/virtual_machine/controls.ts index cef22a2..89b9839 100644 --- a/www/src/ts/virtual_machine/controls.ts +++ b/www/src/ts/virtual_machine/controls.ts @@ -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`
@@ -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; } } diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts index b450490..15e1666 100644 --- a/www/src/ts/virtual_machine/device.ts +++ b/www/src/ts/virtual_machine/device.ts @@ -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` - +
- + Id - + Name - + - + Hash - + ${badges.map((badge) => badge)}
- - + +
`; @@ -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` - ${name} - - ${field.field_type} - `; + return html` + ${name} + + ${field.field_type} + `; })} `; } @@ -268,21 +314,20 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); return html` - + ${slotIndex} : ${slot.typ} ${ - typeof slot.occupant !== "undefined" - ? html` - - Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} - - - Quantity: ${slot.occupant.quantity}/ - ${slot.occupant.max_quantity} - - ` - : "" + typeof slot.occupant !== "undefined" + ? html` + + Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} + + + Quantity: ${slot.occupant.quantity}/ + ${slot.occupant.max_quantity} + + ` + : "" }
${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) { Networks Pins - ${this.renderFields()} + ${this.renderFields()} ${this.renderSlots()} ${this.renderReagents()} ${this.renderNetworks()} ${this.renderPins()} - +
- +

Are you sure you want to remove this device?

Id ${this.deviceID} : ${this.name ?? this.prefabName}
- Close - Remove + Close + Remove
`; @@ -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` `, + const deviceCards = repeat( + this.filteredDeviceIds, + (id) => id, + (id) => + html` + `, ); const result = html`
@@ -538,7 +614,12 @@ export class VMDeviceList extends BaseElement { Devices: ${this.devices.length} - + " @@ -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` - + `, ); @@ -747,20 +834,33 @@ export class VMAddDeviceButton extends BaseElement { render() { return html` - + Add Device - + Search Structures "
${this.renderSearchResults()}
- { - this.drawer.hide(); + { + this.drawer.hide(); }} - > + > Close
@@ -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` - - ${name} - ${field.field_type} - - `; + return html` + + ${name} + ${field.field_type} + + `; })} `; } @@ -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`
${connections.map((connection, index, _conns) => { - const conn = - typeof connection === "object" ? connection.CableNetwork : null; - return html` - - Connection:${index} - ${vmNetworks.map( - (net) => - html`Network ${net}`, - )} - ${conn?.typ} - - `; + const conn = + typeof connection === "object" ? connection.CableNetwork : null; + return html` + + Connection:${index} + ${vmNetworks.map( + (net) => + html`Network ${net}`, + )} + ${conn?.typ} + + `; })}
`; @@ -990,16 +1107,23 @@ export class VmDeviceTemplate extends BaseElement {
- +
${device.title} ${device?.name} ${device?.hash}
- Add + Add
@@ -1013,7 +1137,9 @@ export class VmDeviceTemplate extends BaseElement { ${this.renderFields()} ${this.renderSlots()} - ${this.renderNetworks()} + ${this.renderNetworks()}
@@ -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(); diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts index 66edb59..6d2973a 100644 --- a/www/src/ts/virtual_machine/index.ts +++ b/www/src/ts/virtual_machine/index.ts @@ -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 }; diff --git a/www/src/ts/virtual_machine/registers.ts b/www/src/ts/virtual_machine/registers.ts index 29c4586..c14772a 100644 --- a/www/src/ts/virtual_machine/registers.ts +++ b/www/src/ts/virtual_machine/registers.ts @@ -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); } } diff --git a/www/src/ts/virtual_machine/stack.ts b/www/src/ts/virtual_machine/stack.ts index ed795c4..c9715cb 100644 --- a/www/src/ts/virtual_machine/stack.ts +++ b/www/src/ts/virtual_machine/stack.ts @@ -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)); } } diff --git a/www/src/ts/virtual_machine/ui.ts b/www/src/ts/virtual_machine/ui.ts index b0bd1c6..9b1f3a9 100644 --- a/www/src/ts/virtual_machine/ui.ts +++ b/www/src/ts/virtual_machine/ui.ts @@ -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) { diff --git a/www/src/ts/virtual_machine/utils.ts b/www/src/ts/virtual_machine/utils.ts index 59d977f..db81c9d 100644 --- a/www/src/ts/virtual_machine/utils.ts +++ b/www/src/ts/virtual_machine/utils.ts @@ -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 } };