Allow the Id of a device to be changed, toast errors

This commit is contained in:
Rachel Powers
2024-04-14 17:21:57 -07:00
parent 9283d9d939
commit eed4f1f429
13 changed files with 235 additions and 95 deletions

View File

@@ -339,7 +339,7 @@ pub struct RegisterSpec {
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct DeviceSpec {
pub device: Device,
pub connection: Option<u32>,
pub connection: Option<usize>,
}
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
@@ -424,7 +424,7 @@ impl Operand {
ic: &interpreter::IC,
inst: InstructionOp,
index: u32,
) -> Result<(Option<u32>, Option<u32>), interpreter::ICError> {
) -> Result<(Option<u32>, Option<usize>), 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::<String>();
let connection = connection_str.parse::<u32>().unwrap();
let connection = connection_str.parse::<usize>().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::<String>();
let connection = connection_str.parse::<u32>().unwrap();
let connection = connection_str.parse::<usize>().unwrap();
if rest_iter.next().is_none() {
Ok(Some(connection))
} else {

View File

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

View File

@@ -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<bool, VMError> {
let device = self
@@ -1113,21 +1144,23 @@ impl VM {
}
}
pub fn get_network_channel(&self, id: usize, channel: usize) -> Result<f64, ICError> {
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<f64, ICError> {
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

View File

@@ -439,6 +439,11 @@ impl VM {
pub fn set_pin(&self, id: u32, pin: usize, val: Option<u32>) -> Result<bool, JsError> {
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 {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<html data-bs-theme="dark" class="sl-theme-dark">
<head>
<meta charset="utf-8">

View File

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

View File

@@ -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`
<details
part="base"
class=${classMap({
details: true,
"details--open": this.open,
"details--disabled": this.disabled,
})}
>
<summary
part="header"
id="header"
class="details__header"
role="button"
aria-expanded=${this.open ? "true" : "false"}
aria-controls="content"
aria-disabled=${this.disabled ? "true" : "false"}
tabindex=${this.disabled ? "-1" : "0"}
>
<slot name="summary" part="summary" class="details__summary"
>${this.summary}</slot
<details part="base" class=${classMap({ details: true, "details--open" : this.open, "details--disabled" : this.disabled,
})}>
<summary part="header" id="header" class="details__header" role="button" aria-expanded=${this.open ? "true" : "false"
} aria-controls="content" aria-disabled=${this.disabled ? "true" : "false" } tabindex=${this.disabled ? "-1" : "0" }
@click=${(e: Event)=> e.preventDefault()}
>
<slot name="summary" part="summary" class="details__summary">${this.summary}</slot>
<span
part="summary-icon"
class="details__summary-icon"
@click=${this.handleSummaryIconClick}
@keydown=${this.handleSummaryIconKeyDown}
>
<span part="summary-icon" class="details__summary-icon" @click=${this.handleSummaryIconClick}
@keydown=${this.handleSummaryIconKeyDown}>
<slot name="expand-icon">
<sl-icon library="system" name="chevron-right"></sl-icon>
</slot>

View File

@@ -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<number, Ace.EditSession>;
@property({ type: Number })
accessor active_session: number = 0;
@state() active_session: number = 0;
active_line_markers: Map<number, number | null> = 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)) {

View File

@@ -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 */

View File

@@ -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}
>
<span slot="prefix">Id</span>
<sl-copy-button slot="suffix" value=${this.deviceID}></sl-copy-button>
@@ -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`<vm-device-card
.deviceID=${id}
class="device-list-card"
></vm-device-card>`,
);
const result = html`
<div class="header">
<span>
Devices:
@@ -413,15 +430,11 @@ export class VMDeviceList extends BaseElement {
<vm-add-device-button class="ms-auto"></vm-add-device-button>
</div>
<div class="device-list">
${this.devices.map(
(id, _index, _ids) =>
html`<vm-device-card
.deviceID=${id}
class="device-list-card"
></vm-device-card>`,
)}
${deviceCards}
</div>
`;
return result;
}
}

View File

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

View File

@@ -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<number, DeviceRef>;
_ics: Map<number, DeviceRef>;
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;

View File

@@ -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: `
<sl-icon slot="icon" name="${msg.icon}"></sl-icon>
<strong>${msg.title}</strong><br />
${msg.msg}
`
});
document.body.append(alert);
alert.toast();
}
protected render() {
return html`
<div class="side-container">