diff --git a/ic10emu/src/grammar.rs b/ic10emu/src/grammar.rs index 9131e3d..24ec601 100644 --- a/ic10emu/src/grammar.rs +++ b/ic10emu/src/grammar.rs @@ -339,7 +339,7 @@ pub struct RegisterSpec { #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct DeviceSpec { pub device: Device, - pub connection: Option, + pub connection: Option, } #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] @@ -424,7 +424,7 @@ impl Operand { ic: &interpreter::IC, inst: InstructionOp, index: u32, - ) -> Result<(Option, Option), interpreter::ICError> { + ) -> Result<(Option, Option), interpreter::ICError> { match self.translate_alias(ic) { Operand::DeviceSpec(DeviceSpec { device, connection }) => match device { Device::Db => Ok((Some(ic.device), connection)), @@ -615,7 +615,7 @@ impl FromStr for Operand { let connection_str = rest_iter .take_while_ref(|c| c.is_ascii_digit()) .collect::(); - let connection = connection_str.parse::().unwrap(); + let connection = connection_str.parse::().unwrap(); if rest_iter.next().is_none() { Ok(Some(connection)) } else { @@ -669,7 +669,7 @@ impl FromStr for Operand { let connection_str = rest_iter .take_while_ref(|c| c.is_ascii_digit()) .collect::(); - let connection = connection_str.parse::().unwrap(); + let connection = connection_str.parse::().unwrap(); if rest_iter.next().is_none() { Ok(Some(connection)) } else { diff --git a/ic10emu/src/interpreter.rs b/ic10emu/src/interpreter.rs index e73ee1c..79041dd 100644 --- a/ic10emu/src/interpreter.rs +++ b/ic10emu/src/interpreter.rs @@ -12,9 +12,7 @@ use itertools::Itertools; use time::format_description; -use crate::{ - grammar::{self, ParseError}, -}; +use crate::grammar::{self, ParseError}; use thiserror::Error; @@ -112,6 +110,8 @@ pub enum ICError { NetworkNotConnected(usize), #[error("bad network Id '{0}'")] BadNetworkId(u32), + #[error("channel index out of range '{0}'")] + ChannelIndexOutOfRange(usize) } impl ICError { @@ -2147,10 +2147,10 @@ impl IC { }; let network_id = vm .get_device_same_network(this.device, device_id) - .map(|device| device.borrow().get_network_id(connection as usize)) + .map(|device| device.borrow().get_network_id(connection)) .unwrap_or(Err(UnknownDeviceID(device_id as f64)))?; let val = val.as_value(this, inst, 3)?; - vm.set_network_channel(network_id as usize, channel, val)?; + vm.set_network_channel(network_id, channel, val)?; return Ok(()); } let device = vm.get_device_same_network(this.device, device_id); @@ -2256,9 +2256,9 @@ impl IC { }; let network_id = vm .get_device_same_network(this.device, device_id) - .map(|device| device.borrow().get_network_id(connection as usize)) + .map(|device| device.borrow().get_network_id(connection)) .unwrap_or(Err(UnknownDeviceID(device_id as f64)))?; - let val = vm.get_network_channel(network_id as usize, channel)?; + let val = vm.get_network_channel(network_id, channel)?; this.set_register(indirection, target, val)?; return Ok(()); } diff --git a/ic10emu/src/lib.rs b/ic10emu/src/lib.rs index 9346dd0..d1f369d 100644 --- a/ic10emu/src/lib.rs +++ b/ic10emu/src/lib.rs @@ -34,6 +34,8 @@ pub enum VMError { InvalidNetwork(u32), #[error("device {0} not visible to device {1} (not on the same networks)")] DeviceNotVisible(u32, u32), + #[error("a device with id {0} already exists")] + IdInUse(u32), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -65,7 +67,7 @@ impl SlotOccupant { id, prefab_hash, quantity: 1, - max_quantity: 1, // FIXME: need a good way to set a better default + max_quantity: 1, damage: 0.0, fields: HashMap::new(), } @@ -976,6 +978,35 @@ impl VM { } } + pub fn change_device_id(&mut self, old_id: u32, new_id: u32) -> Result<(), VMError> { + if self.devices.contains_key(&new_id) | self.ics.contains_key(&new_id) { + return Err(VMError::IdInUse(new_id)); + } + let device = self + .devices + .remove(&old_id) + .ok_or(VMError::UnknownId(old_id))?; + device.borrow_mut().id = new_id; + self.devices.insert(new_id, device); + self.ics.iter().for_each(|(_id, ic)| { + if let Ok(mut ic_ref) = ic.try_borrow_mut() { + ic_ref.pins.iter_mut().for_each(|pin| { + if pin.is_some_and(|d| d == old_id) { + pin.replace(new_id); + } + }) + } + }); + self.networks.iter().for_each(|(_net_id, net)| { + if let Ok(mut net_ref) = net.try_borrow_mut() { + if net_ref.devices.remove(&old_id) { + net_ref.devices.insert(new_id); + } + } + }); + Ok(()) + } + /// Set program code if it's valid pub fn set_code(&self, id: u32, code: &str) -> Result { let device = self @@ -1113,21 +1144,23 @@ impl VM { } } - pub fn get_network_channel(&self, id: usize, channel: usize) -> Result { - let network = self - .networks - .get(&(id as u32)) - .ok_or(ICError::BadNetworkId(id as u32))?; - Ok(network.borrow().channels[channel]) + pub fn get_network_channel(&self, id: u32, channel: usize) -> Result { + let network = self.networks.get(&id).ok_or(ICError::BadNetworkId(id))?; + if !(0..8).contains(&channel) { + Err(ICError::ChannelIndexOutOfRange(channel)) + } else { + Ok(network.borrow().channels[channel]) + } } - pub fn set_network_channel(&self, id: usize, channel: usize, val: f64) -> Result<(), ICError> { - let network = self - .networks - .get(&(id as u32)) - .ok_or(ICError::BadNetworkId(id as u32))?; - network.borrow_mut().channels[channel] = val; - Ok(()) + pub fn set_network_channel(&self, id: u32, channel: usize, val: f64) -> Result<(), ICError> { + let network = self.networks.get(&(id)).ok_or(ICError::BadNetworkId(id))?; + if !(0..8).contains(&channel) { + Err(ICError::ChannelIndexOutOfRange(channel)) + } else { + network.borrow_mut().channels[channel] = val; + Ok(()) + } } pub fn devices_on_same_network(&self, ids: &[u32]) -> bool { @@ -1200,7 +1233,7 @@ impl VM { { // scope this borrow let connections = &device.borrow().connections; - let Connection::CableNetwork { net, .. } = & connections[connection] else { + let Connection::CableNetwork { net, .. } = &connections[connection] else { return Err(ICError::NotACableConnection(connection).into()); }; // remove from current network diff --git a/ic10emu_wasm/src/lib.rs b/ic10emu_wasm/src/lib.rs index 569a226..2cd1d7b 100644 --- a/ic10emu_wasm/src/lib.rs +++ b/ic10emu_wasm/src/lib.rs @@ -439,6 +439,11 @@ impl VM { pub fn set_pin(&self, id: u32, pin: usize, val: Option) -> Result { Ok(self.vm.borrow().set_pin(id, pin, val)?) } + + #[wasm_bindgen(js_name = "changeDeviceId")] + pub fn change_device_id(&self, old_id: u32, new_id: u32) -> Result<(), JsError> { + Ok(self.vm.borrow_mut().change_device_id(old_id, new_id)?) + } } impl Default for VM { diff --git a/www/src/index.html b/www/src/index.html index ddf8ff9..0c7f33c 100644 --- a/www/src/index.html +++ b/www/src/index.html @@ -1,5 +1,5 @@ - + diff --git a/www/src/scss/styles.scss b/www/src/scss/styles.scss index 3efd6b8..6c127a2 100644 --- a/www/src/scss/styles.scss +++ b/www/src/scss/styles.scss @@ -65,6 +65,9 @@ $accordion-button-padding-y: 0.5rem; // Utilities @import "bootstrap/scss/utilities/api"; +// Sholace theme +@import "@shoelace-style/shoelace/dist/themes/dark.css"; + // // Custom styles diff --git a/www/src/ts/components/details.ts b/www/src/ts/components/details.ts index 6f34838..bcb1df3 100644 --- a/www/src/ts/components/details.ts +++ b/www/src/ts/components/details.ts @@ -1,4 +1,10 @@ -import { html, css, HTMLTemplateResult, PropertyValueMap } from "lit"; +import { + html, + css, + HTMLTemplateResult, + PropertyValueMap, + CSSResultGroup, +} from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.js"; @@ -7,6 +13,18 @@ import SlDetails from "@shoelace-style/shoelace/dist/components/details/details. export class IC10Details extends SlDetails { @query(".details__summary-icon") accessor summaryIcon: HTMLSpanElement; + static styles = [ + SlDetails.styles, + css` + .details__header { + cursor: auto; + } + .details__summary-icon { + cursor: pointer; + } + `, + ]; + constructor() { super(); } @@ -48,34 +66,16 @@ export class IC10Details extends SlDetails { render() { return html` -
- - ${this.summary} + e.preventDefault()} > + ${this.summary} - + diff --git a/www/src/ts/editor/index.ts b/www/src/ts/editor/index.ts index 8c170b6..d3ceda7 100644 --- a/www/src/ts/editor/index.ts +++ b/www/src/ts/editor/index.ts @@ -28,8 +28,7 @@ declare global { import { BaseElement, defaultCss } from "../components"; import { html } from "lit"; -import { Ref, createRef, ref } from "lit/directives/ref.js"; -import { customElement, property, query } from "lit/decorators.js"; +import { customElement, state, query } from "lit/decorators.js"; import { editorStyles } from "./styles"; import "./shortcuts_ui"; import { AceKeyboardShortcuts } from "./shortcuts_ui"; @@ -45,8 +44,7 @@ export class IC10Editor extends BaseElement { }; sessions: Map; - @property({ type: Number }) - accessor active_session: number = 0; + @state() active_session: number = 0; active_line_markers: Map = new Map(); languageProvider?: LanguageProvider; @@ -291,7 +289,7 @@ export class IC10Editor extends BaseElement { window.App!.session.onActiveLine(((e: CustomEvent) => { const session = window.App?.session!; - const id = e.detail; + const id: number = e.detail; const active_line = session.getActiveLine(id); if (typeof active_line !== "undefined") { const marker = that.active_line_markers.get(id); @@ -485,14 +483,14 @@ export class IC10Editor extends BaseElement { } createOrSetSession(session_id: number, content: any) { - if (!this.sessions.hasOwnProperty(session_id)) { + if (!this.sessions.has(session_id)) { this.newSession(session_id); } this.sessions.get(session_id)?.setValue(content); } newSession(session_id: number) { - if (this.sessions.hasOwnProperty(session_id)) { + if (this.sessions.has(session_id)) { return false; } const session = ace.createEditSession("", this.mode as any); @@ -564,7 +562,7 @@ export class IC10Editor extends BaseElement { } destroySession(session_id: number) { - if (!this.sessions.hasOwnProperty(session_id)) { + if (!this.sessions.has(session_id)) { return false; } if (!(Object.keys(this.sessions).length > 1)) { diff --git a/www/src/ts/virtual_machine/controls.ts b/www/src/ts/virtual_machine/controls.ts index 1efd169..cef22a2 100644 --- a/www/src/ts/virtual_machine/controls.ts +++ b/www/src/ts/virtual_machine/controls.ts @@ -52,6 +52,7 @@ export class VMICControls extends VMActiveICMixin(BaseElement) { sl-button[variant="success"] { /* Changes the success theme color to purple using primitives */ --sl-color-success-600: var(--sl-color-purple-700); + --sl-color-success-500: var(--sl-color-purple-600); } sl-button[variant="primary"] { /* Changes the success theme color to purple using primitives */ diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts index 23e6f8e..7d77a69 100644 --- a/www/src/ts/virtual_machine/device.ts +++ b/www/src/ts/virtual_machine/device.ts @@ -62,7 +62,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { width: 10rem; } .device-id::part(input) { - width: 2rem; + width: 7rem; } .device-name-hash::part(input) { width: 7rem; @@ -129,7 +129,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { size="small" pill value=${this.deviceID} - disabled + @sl-change=${this._handleChangeID} > Id @@ -314,6 +314,16 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { `; } + _handleChangeID(e: CustomEvent) { + const input = e.target as SlInput; + const val = input.valueAsNumber; + if (!isNaN(val)) { + window.VM.changeDeviceId(this.deviceID, val); + } else { + input.value = this.deviceID.toString(); + } + } + _handleChangeName(e: CustomEvent) { const input = e.target as SlInput; window.VM?.setDeviceName(this.deviceID, input.value); @@ -404,7 +414,14 @@ export class VMDeviceList extends BaseElement { } protected render(): HTMLTemplateResult { - return html` + const deviceCards: HTMLTemplateResult[] = this.devices.map( + (id, _index, _ids) => + html``, + ); + const result = html`
Devices: @@ -413,15 +430,11 @@ export class VMDeviceList extends BaseElement {
- ${this.devices.map( - (id, _index, _ids) => - html``, - )} + ${deviceCards}
`; + + return result; } } diff --git a/www/src/ts/virtual_machine/device_db.ts b/www/src/ts/virtual_machine/device_db.ts index 43a8f00..789e400 100644 --- a/www/src/ts/virtual_machine/device_db.ts +++ b/www/src/ts/virtual_machine/device_db.ts @@ -386,7 +386,7 @@ export type DeviceDBEntry = { slotlogic?: { [key in SlotLogicType]: number[] }; slots?: { name: string; typ: SlotClass }[]; modes?: { [key: string]: string }; - conn?: { [key in SlotLogicType]: [NetworkType, ConnectionRole] }; + conn?: { [key: number]: [NetworkType, ConnectionRole] }; slotclass?: SlotClass; sorting?: SortingClass; pins?: number; @@ -403,3 +403,30 @@ export type DeviceDB = { }; names_by_hash: { [key: number]: string }; }; + + +export type PreCastDeviceDBEntry = { + name: string; + hash: number; + desc: string; + logic?: { [key in LogicType]?: string }; + slotlogic?: { [key in SlotLogicType]?: number[] }; + slots?: { name: string; typ: string }[]; + modes?: { [key: string]: string }; + conn?: { [key: number]: string[] }; + slotclass?: string; + sorting?: string; + pins?: number; +}; + +export type PreCastDeviceDB = { + logic_enabled: string[]; + slot_logic_enabled: string[]; + devices: string[]; + items: string[]; + structures: string[]; + db: { + [key: string]: PreCastDeviceDBEntry; + }; + names_by_hash: { [key: number]: string }; +}; diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts index e98e807..183dee0 100644 --- a/www/src/ts/virtual_machine/index.ts +++ b/www/src/ts/virtual_machine/index.ts @@ -1,5 +1,5 @@ import { DeviceRef, VM, init } from "ic10emu_wasm"; -import { DeviceDB } from "./device_db"; +import { DeviceDB, PreCastDeviceDB } from "./device_db"; import "./base_device"; declare global { @@ -8,13 +8,21 @@ declare global { } } +export interface ToastMessage { + variant: "warning" | "danger" | "success" | "primary" | "neutral"; + icon: string; + title: string; + msg: string; + id: string; +} + class VirtualMachine extends EventTarget { ic10vm: VM; _devices: Map; _ics: Map; accessor db: DeviceDB; - dbPromise: Promise<{ default: DeviceDB }>; + dbPromise: Promise<{ default: PreCastDeviceDB }>; constructor() { super(); @@ -28,7 +36,9 @@ class VirtualMachine extends EventTarget { this._ics = new Map(); this.dbPromise = import("../../../data/database.json"); - this.dbPromise.then((module) => this.setupDeviceDatabase(module.default)); + this.dbPromise.then((module) => + this.setupDeviceDatabase(module.default as DeviceDB), + ); this.updateDevices(); this.updateCode(); @@ -78,7 +88,6 @@ class VirtualMachine extends EventTarget { } for (const id of this._devices.keys()) { if (!device_ids.includes(id)) { - this._devices.get(id)!.free(); this._devices.delete(id); update_flag = true; } @@ -102,7 +111,9 @@ class VirtualMachine extends EventTarget { if (update_flag) { this.dispatchEvent( - new CustomEvent("vm-devices-update", { detail: device_ids }), + new CustomEvent("vm-devices-update", { + detail: Array.from(device_ids), + }), ); } } @@ -122,8 +133,8 @@ class VirtualMachine extends EventTarget { this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: id }), ); - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } console.timeEnd(`CompileProgram_${id}_${attempt}`); } @@ -136,8 +147,8 @@ class VirtualMachine extends EventTarget { if (ic) { try { ic.step(false); - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } this.update(); this.dispatchEvent( @@ -151,8 +162,8 @@ class VirtualMachine extends EventTarget { if (ic) { try { ic.run(false); - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } this.update(); this.dispatchEvent( @@ -178,7 +189,7 @@ class VirtualMachine extends EventTarget { ); } }, this); - this.updateDevice(this.activeIC) + this.updateDevice(this.activeIC); } updateDevice(device: DeviceRef) { @@ -190,13 +201,37 @@ class VirtualMachine extends EventTarget { } } + handleVmError(err: Error) { + console.log("Error in Virtual Machine", err); + const message: ToastMessage = { + variant: "danger", + icon: "bug", + title: `Error in Virtual Machine ${err.name}`, + msg: err.message, + id: Date.now().toString(16), + }; + this.dispatchEvent(new CustomEvent("vm-message", { detail: message })); + } + + changeDeviceId(old_id: number, new_id: number) { + try { + this.ic10vm.changeDeviceId(old_id, new_id); + this.updateDevices(); + if (window.App.session.activeIC === old_id) { + window.App.session.activeIC = new_id; + } + } catch (err) { + this.handleVmError(err); + } + } + setRegister(index: number, val: number) { const ic = this.activeIC!; try { ic.setRegister(index, val); this.updateDevice(ic); - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } } @@ -205,8 +240,8 @@ class VirtualMachine extends EventTarget { try { ic!.setStack(addr, val); this.updateDevice(ic); - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } } @@ -227,8 +262,8 @@ class VirtualMachine extends EventTarget { device.setField(field, val); this.updateDevice(device); return true; - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } } return false; @@ -241,8 +276,8 @@ class VirtualMachine extends EventTarget { device.setSlotField(slot, field, val); this.updateDevice(device); return true; - } catch (e) { - console.log(e); + } catch (err) { + this.handleVmError(err); } } return false; diff --git a/www/src/ts/virtual_machine/ui.ts b/www/src/ts/virtual_machine/ui.ts index be82639..0a9b0fc 100644 --- a/www/src/ts/virtual_machine/ui.ts +++ b/www/src/ts/virtual_machine/ui.ts @@ -1,15 +1,17 @@ import { HTMLTemplateResult, html, css } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; +import { customElement, property, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "../components"; import "@shoelace-style/shoelace/dist/components/details/details.js"; import "@shoelace-style/shoelace/dist/components/tab/tab.js"; import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; +import "@shoelace-style/shoelace/dist/components/alert/alert.js"; import "./controls"; import "./registers"; import "./stack"; import "./device"; +import { ToastMessage } from "."; @customElement("vm-ui") export class VMUI extends BaseElement { @@ -34,16 +36,39 @@ export class VMUI extends BaseElement { margin-top: 0.5rem; } .side-container { - height: 100% + height: 100%; overflow-y: auto; } `, ]; + constructor() { super(); } + connectedCallback(): void { + super.connectedCallback(); + window.VM.addEventListener("vm-message", this._handleVMMessage.bind(this) ) + } + + _handleVMMessage(e: CustomEvent) { + const msg: ToastMessage = e.detail; + const alert = Object.assign(document.createElement('sl-alert'), { + variant: msg.variant, + closable: true, + // duration: 5000, + innerHTML: ` + + ${msg.title}
+ ${msg.msg} + ` + }); + + document.body.append(alert); + alert.toast(); + } + protected render() { return html`