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

1
Cargo.lock generated
View File

@@ -571,6 +571,7 @@ dependencies = [
"rand",
"regex",
"serde",
"serde_with",
"strum",
"strum_macros",
"thiserror",

View File

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

View File

@@ -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<ReagentMode, HashMap<i32, f64>>,
pub ic: Option<u32>,
pub connections: Vec<Connection>,
pub fields: HashMap<LogicType, LogicField>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DeviceTemplate {
pub id: Option<u32>,
pub name: Option<String>,
pub prefab_name: Option<String>,
pub slots: Vec<SlotTemplate>,
// pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
pub connections: Vec<Connection>,
pub fields: HashMap<LogicType, LogicField>,
fields: HashMap<LogicType, LogicField>,
}
impl Device {
@@ -925,3 +918,94 @@ impl Device {
})
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DeviceTemplate {
pub id: Option<u32>,
pub name: Option<String>,
pub prefab_name: Option<String>,
pub slots: Vec<SlotTemplate>,
// pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
pub connections: Vec<Connection>,
pub fields: HashMap<LogicType, LogicField>,
}
impl Device {
/// create a devive from a template and return the device, does not create it's own IC
pub fn from_template<F>(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<T> From<T> for DeviceTemplate
where
T: Deref<Target = Device>,
{
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(),
}
}
}

View File

@@ -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<String, grammar::Operand>,
pub defines: HashMap<String, f64>,
pub pins: [Option<u32>; 6],
pub state: ICState,
pub code: String,
}
impl<T> From<T> for FrozenIC
where T: Deref<Target = IC>
{
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<FrozenIC> 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<grammar::Instruction>,

View File

@@ -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<u32>,
pub power_only: HashSet<u32>,
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<u32>,
pub power_only: Vec<u32>,
pub channels: [f64; 8],
}
impl<T> From<T> for FrozenNetwork
where
T: Deref<Target = Network>,
{
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<FrozenNetwork> 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)
}

View File

@@ -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<u32, Rc<RefCell<Network>>>,
pub default_network: u32,
id_space: IdSpace,
network_id_gen: IdSpace,
network_id_space: IdSpace,
random: Rc<RefCell<crate::rand_mscorlib::Random>>,
/// 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<FrozenIC>,
pub devices: Vec<DeviceTemplate>,
pub networks: Vec<FrozenNetwork>,
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();
}
}

View File

@@ -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<String> {
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<bool, JsError> {
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 {

View File

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

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