diff --git a/ic10emu/src/vm.rs b/ic10emu/src/vm.rs index 2a55f2d..073a05f 100644 --- a/ic10emu/src/vm.rs +++ b/ic10emu/src/vm.rs @@ -155,7 +155,7 @@ impl VM { obj_ids.push(obj_id) } - transaction.finialize()?; + transaction.finalize()?; let transaction_ids = transaction.id_space.in_use_ids(); self.id_space.borrow_mut().use_new_ids(&transaction_ids); @@ -200,15 +200,12 @@ impl VM { /// current database. /// Errors if the object can not be built do to a template error /// Returns the built object's ID - pub fn add_object_frozen( - self: &Rc, - frozen: FrozenObject, - ) -> Result { + pub fn add_object_frozen(self: &Rc, frozen: FrozenObject) -> Result { let mut transaction = VMTransaction::new(self); let obj_id = transaction.add_object_from_frozen(frozen)?; - transaction.finialize()?; + transaction.finalize()?; let transaction_ids = transaction.id_space.in_use_ids(); self.id_space.borrow_mut().use_new_ids(&transaction_ids); @@ -1351,17 +1348,7 @@ impl VM { .objects .borrow() .iter() - .filter_map(|(_obj_id, obj)| { - if obj - .borrow() - .as_item() - .is_some_and(|item| item.get_parent_slot().is_some()) - { - None - } else { - Some(FrozenObject::freeze_object_sparse(obj, self)) - } - }) + .map(|(_obj_id, obj)| FrozenObject::freeze_object_sparse(obj, self)) .collect::, _>>()?, networks: self .networks @@ -1406,7 +1393,7 @@ impl VM { for frozen in state.objects { let _ = transaction.add_object_from_frozen(frozen)?; } - transaction.finialize()?; + transaction.finalize()?; self.circuit_holders.borrow_mut().clear(); self.program_holders.borrow_mut().clear(); @@ -1423,6 +1410,7 @@ impl VM { let transaction_ids = transaction.id_space.in_use_ids(); self.id_space.borrow_mut().use_ids(&transaction_ids)?; + self.objects.borrow_mut().extend(transaction.objects); self.circuit_holders .borrow_mut() .extend(transaction.circuit_holders); @@ -1557,7 +1545,7 @@ impl VMTransaction { Ok(obj_id) } - pub fn finialize(&mut self) -> Result<(), VMError> { + pub fn finalize(&mut self) -> Result<(), VMError> { for (child, (slot, parent)) in &self.object_parents { let child_obj = self .objects diff --git a/stationeers_data/src/templates.rs b/stationeers_data/src/templates.rs index e498615..4f7f19d 100644 --- a/stationeers_data/src/templates.rs +++ b/stationeers_data/src/templates.rs @@ -203,9 +203,11 @@ pub struct SlotInfo { #[cfg_attr(feature = "tsify", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct LogicInfo { #[serde_as( as = "BTreeMap")] + #[cfg_attr(feature = "tsify", tsify(type = "Map>"))] pub logic_slot_types: BTreeMap>, pub logic_types: BTreeMap, #[serde_as( as = "Option>")] + #[cfg_attr(feature = "tsify", tsify(type = "Map | undefined"))] pub modes: Option>, pub transmission_receiver: bool, pub wireless_logic: bool, diff --git a/www/cspell.json b/www/cspell.json index 380c6ea..522f907 100644 --- a/www/cspell.json +++ b/www/cspell.json @@ -132,6 +132,7 @@ "trunc", "ufuzzy", "VMIC", + "VMUI", "vstack", "whos" ], diff --git a/www/src/ts/editor/index.ts b/www/src/ts/editor/index.ts index f9b6660..41fdafa 100644 --- a/www/src/ts/editor/index.ts +++ b/www/src/ts/editor/index.ts @@ -243,7 +243,7 @@ export class IC10Editor extends BaseElement { app.session.onLoad((_e) => { const session = app.session; const updated_ids: number[] = []; - for (const [id, code] of session.programs) { + for (const [id, code] of session.programs.value) { updated_ids.push(id); that.createOrSetSession(id, code); } @@ -271,7 +271,7 @@ export class IC10Editor extends BaseElement { that.activeLineMarkers.set( id, session.addMarker( - new Range(active_line, 0, active_line, 1), + new Range(active_line.value, 0, active_line.value, 1), "vm_ic_active_line", "fullLine", true, diff --git a/www/src/ts/index.ts b/www/src/ts/index.ts index 61798f8..a879a4e 100644 --- a/www/src/ts/index.ts +++ b/www/src/ts/index.ts @@ -39,7 +39,7 @@ import "@shoelace-style/shoelace/dist/components/relative-time/relative-time.js" import "ace-builds"; import "ace-builds/esm-resolver"; -class DeferedApp { +class DeferredApp { app: App; private resolvers: ((value: App) => void)[]; @@ -69,7 +69,7 @@ class DeferedApp { } -class DeferedVM { +class DeferredVM { vm: VirtualMachine; private resolvers: ((value: VirtualMachine) => void)[]; @@ -102,13 +102,13 @@ class DeferedVM { declare global { interface Window { - App: DeferedApp; - VM: DeferedVM; + App: DeferredApp; + VM: DeferredVM; } } -window.App = new DeferedApp(); -window.VM = new DeferedVM(); +window.App = new DeferredApp(); +window.VM = new DeferredVM(); import type { App } from "./app"; import type { VirtualMachine } from "./virtualMachine"; diff --git a/www/src/ts/session.ts b/www/src/ts/session.ts index 2fdef21..b224e0e 100644 --- a/www/src/ts/session.ts +++ b/www/src/ts/session.ts @@ -25,6 +25,7 @@ import { } from "./utils"; import * as presets from "./presets"; +import { batch, computed, effect, signal, Signal } from "@lit-labs/preact-signals"; const { demoVMState } = presets; export interface SessionEventMap { @@ -37,63 +38,64 @@ export interface SessionEventMap { } export class Session extends TypedEventTarget() { - private _programs: Map; - private _errors: Map; - private _activeIC: number; - private _activeLines: Map; + private _programs: Signal>; + private _errors: Signal>; + private _activeIC: Signal; + private _activeLines: Signal>; private _save_timeout?: ReturnType; - private _vm_state: FrozenVM; private app: App; constructor(app: App) { super(); this.app = app; - this._programs = new Map(); - this._errors = new Map(); + this._programs = signal(new Map()); + this._errors = signal(new Map()); this._save_timeout = undefined; - this._activeIC = 1; - this._activeLines = new Map(); - this._vm_state = undefined; + this._activeIC = signal(null); + this._activeLines = signal(new Map()); this.loadFromFragment(); const that = this; window.addEventListener("hashchange", (_event) => { that.loadFromFragment(); }); + + this._programs.subscribe((_) => {this._fireOnLoad()}); } - get programs(): Map { + get programs(): Signal> { return this._programs; } set programs(programs: Iterable<[number, string]>) { - this._programs = new Map([...programs]); - this._fireOnLoad(); + this._programs.value = new Map(programs); } - get activeIC() { + get activeIC(): Signal { return this._activeIC; } - set activeIC(val: number) { - this._activeIC = val; - this.dispatchCustomEvent("session-active-ic", this.activeIC); + set activeIC(val: ObjectID) { + this._activeIC.value = val; + this.dispatchCustomEvent("session-active-ic", this.activeIC.peek()); } - changeID(oldID: number, newID: number) { - if (this.programs.has(oldID)) { - this.programs.set(newID, this.programs.get(oldID)); - this.programs.delete(oldID); + changeID(oldID: ObjectID, newID: ObjectID) { + if (this.programs.peek().has(oldID)) { + const newVal = new Map(this.programs.value); + newVal.set(newID, newVal.get(oldID)); + newVal.delete(oldID); + this.programs.value = newVal; } this.dispatchCustomEvent("session-id-change", { old: oldID, new: newID }); } - onIDChange(callback: (e: CustomEvent<{ old: number; new: number }>) => any) { + onIDChange(callback: (e: CustomEvent<{ old: ObjectID; new: ObjectID}>) => any) { this.addEventListener("session-id-change", callback); } - onActiveIc(callback: (e: CustomEvent) => any) { + onActiveIc(callback: (e: CustomEvent) => any) { this.addEventListener("session-active-ic", callback); } @@ -101,36 +103,36 @@ export class Session extends TypedEventTarget() { return this._errors; } - getActiveLine(id: number) { - return this._activeLines.get(id); + getActiveLine(id: ObjectID) { + return computed(() => this._activeLines.value.get(id)); } - setActiveLine(id: number, line: number) { - const last = this._activeLines.get(id); + setActiveLine(id: ObjectID, line: number) { + const last = this._activeLines.peek().get(id); if (last !== line) { - this._activeLines.set(id, line); + this._activeLines.value = new Map([ ... this._activeLines.value.entries(), [id, line]]); this._fireOnActiveLine(id); } } - setProgramCode(id: number, code: string) { - this._programs.set(id, code); + setProgramCode(id: ObjectID, code: string) { + this._programs.value = new Map([ ...this._programs.value.entries(), [id, code]]); if (this.app.vm) { this.app.vm.updateCode(); } this.save(); } - setProgramErrors(id: number, errors: ICError[]) { - this._errors.set(id, errors); + setProgramErrors(id: ObjectID, errors: ICError[]) { + this._errors.value = new Map([ ...this._errors.value.entries(), [id, errors]]); this._fireOnErrors([id]); } - _fireOnErrors(ids: number[]) { + _fireOnErrors(ids: ObjectID[]) { this.dispatchCustomEvent("session-errors", ids); } - onErrors(callback: (e: CustomEvent) => any) { + onErrors(callback: (e: CustomEvent) => any) { this.addEventListener("session-errors", callback); } @@ -142,7 +144,7 @@ export class Session extends TypedEventTarget() { this.dispatchCustomEvent("session-load", this); } - onActiveLine(callback: (e: CustomEvent) => any) { + onActiveLine(callback: (e: CustomEvent) => any) { this.addEventListener("active-line", callback); } @@ -159,7 +161,8 @@ export class Session extends TypedEventTarget() { } async saveToFragment() { - const toSave = { vm: this.app.vm.saveVMState(), activeIC: this.activeIC }; + const vm = await window.VM.get() + const toSave = { vm: vm.state.vm.value, activeIC: this.activeIC }; const bytes = new TextEncoder().encode(toJson(toSave)); try { const c_bytes = await compress(bytes, defaultCompression); @@ -172,21 +175,21 @@ export class Session extends TypedEventTarget() { } async load(data: SessionDB.CurrentDBVmState | OldPrograms | string) { + const vm = await window.VM.get() if (typeof data === "string") { - this._activeIC = 1; - this.app.vm.restoreVMState(demoVMState.vm); - this._programs = new Map([[1, data]]); + this.activeIC = 1; + await vm.restoreVMState(demoVMState.vm); + this.programs = [[1, data]]; } else if ("programs" in data) { - this._activeIC = 1; - this.app.vm.restoreVMState(demoVMState.vm); - this._programs = new Map(data.programs); + this.activeIC = 1; + await vm.restoreVMState(demoVMState.vm); + this.programs = data.programs; } else if ("vm" in data) { - this._programs = new Map(); + this.programs = []; const state = data.vm; // assign first so it's present when the // vm fires events - this._activeIC = data.activeIC; - const vm = await window.VM.get() + this._activeIC.value = data.activeIC; await vm.restoreVMState(state); this.programs = vm.getPrograms(); // assign again to fire event @@ -259,7 +262,7 @@ export class Session extends TypedEventTarget() { async saveLocal(name: string) { const state: SessionDB.CurrentDBVmState = { vm: await (await window.VM.get()).ic10vm.saveVMState(), - activeIC: this.activeIC, + activeIC: this.activeIC.peek(), }; const db = await this.openIndexDB(); const transaction = db.transaction( diff --git a/www/src/ts/utils.ts b/www/src/ts/utils.ts index 2f7b4ed..f937d5e 100644 --- a/www/src/ts/utils.ts +++ b/www/src/ts/utils.ts @@ -1,6 +1,18 @@ import { Ace } from "ace-builds"; import { TransferHandler } from "comlink"; +export function isSome(object: T | null | undefined): object is T { + return typeof object !== "undefined" && object !== null; +} + +export function range(size: number, start: number = 0): number[] { + const base = [...Array(size ?? 0).keys()] + if (start != 0) { + return base.map(i => i + start); + } + return base +} + export function docReady(fn: () => void) { // see if DOM is already available if ( diff --git a/www/src/ts/virtualMachine/baseDevice.ts b/www/src/ts/virtualMachine/baseDevice.ts index d7c4fef..642ede9 100644 --- a/www/src/ts/virtualMachine/baseDevice.ts +++ b/www/src/ts/virtualMachine/baseDevice.ts @@ -1,334 +1,22 @@ -import { property, state } from "lit/decorators.js"; - import type { - Slot, - Connection, - ICError, - LogicType, - LogicField, - Operand, ObjectID, TemplateDatabase, - FrozenObjectFull, - Class, - LogicSlotType, - SlotOccupantInfo, - ICState, - ObjectTemplate, } from "ic10emu_wasm"; -import { crc32, structuralEqual } from "utils"; -import { LitElement, PropertyValueMap } from "lit"; +import { LitElement } from "lit"; import { - computed, signal, } from '@lit-labs/preact-signals'; import type { Signal } from '@lit-labs/preact-signals'; - -export interface VmObjectSlotInfo { - parent: ObjectID; - index: number; - name: string; - typ: Class; - logicFields: Map; - quantity: number; - occupant: ComputedObjectSignals | null; -} - -export class ComputedObjectSignals { - obj: Signal; - id: Signal; - template: Signal; - - name: Signal; - nameHash: Signal; - prefabName: Signal; - prefabHash: Signal; - displayName: Signal; - logicFields: Signal | null>; - slots: Signal; - slotsCount: Signal; - reagents: Signal | null>; - - connections: Signal; - visibleDevices: Signal; - - memory: Signal; - icIP: Signal; - icOpCount: Signal; - icState: Signal; - errors: Signal; - registers: Signal; - aliases: Signal | null>; - defines: Signal | null>; - - numPins: Signal; - pins: Signal | null>; - - - constructor(obj: Signal) { - this.obj = obj - this.id = computed(() => { return this.obj.value.obj_info.id; }); - - this.template = computed(() => { return this.obj.value.template; }); - - this.name = computed(() => { return this.obj.value.obj_info.name; }); - this.nameHash = computed(() => { return this.name.value !== "undefined" ? crc32(this.name.value) : null; }); - this.prefabName = computed(() => { return this.obj.value.obj_info.prefab; }); - this.prefabHash = computed(() => { return this.obj.value.obj_info.prefab_hash; }); - this.displayName = computed(() => { return this.obj.value.obj_info.name ?? this.obj.value.obj_info.prefab; }); - - this.logicFields = computed(() => { - const obj_info = this.obj.value.obj_info; - const template = this.obj.value.template; - - const logicValues = - obj_info.logic_values != null - ? (new Map(Object.entries(obj_info.logic_values)) as Map< - LogicType, - number - >) - : null; - const logicTemplate = - "logic" in template ? template.logic : null; - - return new Map( - Array.from(Object.entries(logicTemplate?.logic_types) ?? []).map( - ([lt, access]) => { - let field: LogicField = { - field_type: access, - value: logicValues.get(lt as LogicType) ?? 0, - }; - return [lt as LogicType, field]; - }, - ), - ) - }); - - this.slots = computed(() => { - const obj_info = this.obj.value.obj_info; - const template = this.obj.value.template; - - const slotsOccupantInfo = - obj_info.slots != null - ? new Map( - Object.entries(obj_info.slots).map(([key, val]) => [ - parseInt(key), - val, - ]), - ) - : null; - const slotsLogicValues = - obj_info.slot_logic_values != null - ? new Map>( - Object.entries(obj_info.slot_logic_values).map( - ([index, values]) => [ - parseInt(index), - new Map(Object.entries(values)) as Map< - LogicSlotType, - number - >, - ], - ), - ) - : null; - const logicTemplate = - "logic" in template ? template.logic : null; - const slotsTemplate = - "slots" in template ? template.slots : []; - - return slotsTemplate.map((template, index) => { - const fieldEntryInfos = Array.from( - Object.entries(logicTemplate?.logic_slot_types.get(index)) ?? [], - ); - const logicFields = new Map( - fieldEntryInfos.map(([slt, access]) => { - let field: LogicField = { - field_type: access, - value: - slotsLogicValues.get(index)?.get(slt as LogicSlotType) ?? 0, - }; - return [slt as LogicSlotType, field]; - }), - ); - let occupantInfo = slotsOccupantInfo.get(index); - let occupant = - typeof occupantInfo !== "undefined" - ? globalObjectSignalMap.get(occupantInfo.id) ?? null - : null; - let slot: VmObjectSlotInfo = { - parent: obj_info.id, - index: index, - name: template.name, - typ: template.typ, - logicFields: logicFields, - occupant: occupant, - quantity: occupantInfo?.quantity ?? 0, - }; - return slot; - }); - }); - - this.slotsCount = computed(() => { - const slotsTemplate = - "slots" in this.obj.value.template ? this.obj.value.template.slots : []; - return slotsTemplate.length; - }); - - this.reagents = computed(() => { - const reagents = - this.obj.value.obj_info.reagents != null - ? new Map( - Object.entries(this.obj.value.obj_info.reagents).map( - ([key, val]) => [parseInt(key), val], - ), - ) - : null; - return reagents; - }); - - this.connections = computed(() => { - const obj_info = this.obj.value.obj_info; - const template = this.obj.value.template; - - const connectionsMap = - obj_info.connections != null - ? new Map( - Object.entries(obj_info.connections).map( - ([key, val]) => [parseInt(key), val], - ), - ) - : null; - const connectionList = - "device" in template - ? template.device.connection_list - : []; - let connections: Connection[] | null = null; - if (connectionList.length !== 0) { - connections = connectionList.map((conn, index) => { - if (conn.typ === "Data") { - return { - CableNetwork: { - typ: "Data", - role: conn.role, - net: connectionsMap.get(index), - }, - }; - } else if (conn.typ === "Power") { - return { - CableNetwork: { - typ: "Power", - role: conn.role, - net: connectionsMap.get(index), - }, - }; - } else if (conn.typ === "PowerAndData") { - return { - CableNetwork: { - typ: "Data", - role: conn.role, - net: connectionsMap.get(index), - }, - }; - } else if (conn.typ === "Pipe") { - return { Pipe: { role: conn.role } }; - } else if (conn.typ === "Chute") { - return { Chute: { role: conn.role } }; - } else if (conn.typ === "Elevator") { - return { Elevator: { role: conn.role } }; - } else if (conn.typ === "LaunchPad") { - return { LaunchPad: { role: conn.role } }; - } else if (conn.typ === "LandingPad") { - return { LandingPad: { role: conn.role } }; - } else if (conn.typ === "PipeLiquid") { - return { PipeLiquid: { role: conn.role } }; - } - return "None"; - }); - } - return connections; - }); - - this.visibleDevices = computed(() => { - return this.obj.value.obj_info.visible_devices.map((id) => globalObjectSignalMap.get(id)) - }); - - this.memory = computed(() => { - return this.obj.value.obj_info.memory ?? null; - }); - - this.icIP = computed(() => { - return this.obj.value.obj_info.circuit?.instruction_pointer ?? null; - }); - - this.icOpCount = computed(() => { - return this.obj.value.obj_info.circuit?.yield_instruction_count ?? null; - }); - - this.icState = computed(() => { - return this.obj.value.obj_info.circuit?.state ?? null; - }); - - this.errors = computed(() => { - return this.obj.value.obj_info.compile_errors ?? null; - }); - - this.registers = computed(() => { - return this.obj.value.obj_info.circuit?.registers ?? null; - }); - - this.aliases = computed(() => { - const aliases = this.obj.value.obj_info.circuit?.aliases ?? null; - return aliases != null ? new Map(Object.entries(aliases)) : null; - }); - - this.defines = computed(() => { - const defines = this.obj.value.obj_info.circuit?.defines ?? null; - return defines != null ? new Map(Object.entries(defines)) : null; - }); - - this.pins = computed(() => { - const pins = this.obj.value.obj_info.device_pins; - return pins != null ? new Map(Object.entries(pins).map(([key, val]) => [parseInt(key), val])) : null; - }); - - this.numPins = computed(() => { - return "device" in this.obj.value.template - ? this.obj.value.template.device.device_pins_length - : Math.max(...Array.from(this.pins.value?.keys() ?? [0])); - }); - - } -} - -class ObjectComputedSignalMap extends Map { - get(id: ObjectID): ComputedObjectSignals { - if (!this.has(id)) { - const obj = window.VM.vm.objects.get(id) - if (typeof obj !== "undefined") { - this.set(id, new ComputedObjectSignals(obj)); - } - } - return super.get(id); - } - set(id: ObjectID, value: ComputedObjectSignals): this { - super.set(id, value); - return this - } -} - -export const globalObjectSignalMap = new ObjectComputedSignalMap(); +import { VirtualMachine } from "virtualMachine"; +import { property } from "lit/decorators.js"; type Constructor = new (...args: any[]) => T; export declare class VMObjectMixinInterface { - objectID: Signal; - activeICId: Signal; - objectSignals: ComputedObjectSignals | null; - _handleDeviceModified(e: CustomEvent): void; - updateObject(): void; - subscribe(...sub: VMObjectMixinSubscription[]): void; - unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean): void; + objectIDSignal: Signal; + objectID: ObjectID; + vm: Signal; } export type VMObjectMixinSubscription = @@ -339,259 +27,33 @@ export const VMObjectMixin = >( superClass: T, ) => { class VMObjectMixinClass extends superClass { - objectID: Signal; + objectIDSignal: Signal = signal(null); + vm: Signal = signal(null); + + @property({type: Number}) + get objectID(): number { + return this.objectIDSignal.peek(); + } + + set objectID(value: number) { + this.objectIDSignal.value = value; + } constructor (...args: any[]) { super(...args); - this.objectID = signal(null); - this.objectID.subscribe((_) => {this.updateObject()}) + this.setupVM(); } - @state() private objectSubscriptions: VMObjectMixinSubscription[] = []; - - subscribe(...sub: VMObjectMixinSubscription[]) { - this.objectSubscriptions = this.objectSubscriptions.concat(sub); - } - - // remove subscriptions matching the filter - unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean) { - this.objectSubscriptions = this.objectSubscriptions.filter( - (sub) => !filter(sub), - ); - } - - @state() objectSignals: ComputedObjectSignals | null = null; - - activeICId: Signal = signal(null); - - connectedCallback(): void { - const root = super.connectedCallback(); - window.VM.get().then((vm) => { - vm.addEventListener( - "vm-object-modified", - this._handleDeviceModified.bind(this), - ); - vm.addEventListener( - "vm-objects-update", - this._handleDevicesModified.bind(this), - ); - vm.addEventListener( - "vm-object-id-change", - this._handleDeviceIdChange.bind(this), - ); - vm.addEventListener( - "vm-objects-removed", - this._handleDevicesRemoved.bind(this), - ); - }); - this.updateObject(); - return root; - } - - disconnectedCallback(): void { - window.VM.get().then((vm) => { - vm.removeEventListener( - "vm-object-modified", - this._handleDeviceModified.bind(this), - ); - vm.removeEventListener( - "vm-objects-update", - this._handleDevicesModified.bind(this), - ); - vm.removeEventListener( - "vm-object-id-change", - this._handleDeviceIdChange.bind(this), - ); - vm.removeEventListener( - "vm-objects-removed", - this._handleDevicesRemoved.bind(this), - ); - }); - } - - async _handleDeviceModified(e: CustomEvent) { - const id = e.detail; - const activeIcId = window.App.app.session.activeIC; - if (this.objectID.peek() === id) { - this.updateObject(); - } else if ( - id === activeIcId && - this.objectSubscriptions.includes("active-ic") - ) { - this.updateObject(); - this.requestUpdate(); - } else if (this.objectSubscriptions.includes("visible-devices")) { - const visibleDevices = await window.VM.vm.visibleDeviceIds( - this.objectID.peek(), - ); - if (visibleDevices.includes(id)) { - this.updateObject(); - this.requestUpdate(); - } - } - } - - async _handleDevicesModified(e: CustomEvent) { - const activeIcId = window.App.app.session.activeIC; - const ids = e.detail; - if (ids.includes(this.objectID.peek())) { - this.updateObject(); - if (this.objectSubscriptions.includes("visible-devices")) { - this.requestUpdate(); - } - } else if ( - ids.includes(activeIcId) && - this.objectSubscriptions.includes("active-ic") - ) { - this.updateObject(); - this.requestUpdate(); - } else if (this.objectSubscriptions.includes("visible-devices")) { - const visibleDevices = await window.VM.vm.visibleDeviceIds( - this.objectID.peek(), - ); - if (ids.some((id) => visibleDevices.includes(id))) { - this.updateObject(); - this.requestUpdate(); - } - } - } - - async _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) { - if (this.objectID.peek() === e.detail.old) { - this.objectID.value = e.detail.new; - } else if (this.objectSubscriptions.includes("visible-devices")) { - const visibleDevices = await window.VM.vm.visibleDeviceIds( - this.objectID.peek(), - ); - if ( - visibleDevices.some( - (id) => id === e.detail.old || id === e.detail.new, - ) - ) { - this.requestUpdate(); - } - } - } - - _handleDevicesRemoved(e: CustomEvent) { - const _ids = e.detail; - if (this.objectSubscriptions.includes("visible-devices")) { - this.requestUpdate(); - } - } - - updateObject() { - this.activeICId.value = window.App.app.session.activeIC; - const newObjSignals = globalObjectSignalMap.get(this.objectID.peek()); - if (newObjSignals !== this.objectSignals) { - this.objectSignals = newObjSignals - } - - if (typeof this.objectSignals === "undefined") { - return; - } - - // other updates needed - + private async setupVM() { + this.vm.value = await window.VM.get(); } } return VMObjectMixinClass as Constructor & T; }; -export const VMActiveICMixin = >( - superClass: T, -) => { - class VMActiveICMixinClass extends VMObjectMixin(superClass) { - constructor(...args: any[]) { - super(...args); - this.objectID.value = window.App.app.session.activeIC; - } - - connectedCallback(): void { - const root = super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener("vm-run-ic", this._handleDeviceModified.bind(this)), - ); - window.App.app.session.addEventListener( - "session-active-ic", - this._handleActiveIC.bind(this), - ); - return root; - } - - disconnectedCallback(): void { - window.VM.get().then((vm) => - vm.removeEventListener( - "vm-run-ic", - this._handleDeviceModified.bind(this), - ), - ); - window.App.app.session.removeEventListener( - "session-active-ic", - this._handleActiveIC.bind(this), - ); - } - - _handleActiveIC(e: CustomEvent) { - const id = e.detail; - if (this.objectID.value !== id) { - this.objectID.value = id; - } - this.updateObject(); - } - } - - return VMActiveICMixinClass as Constructor & T; -}; - export declare class VMTemplateDBMixinInterface { - templateDB: TemplateDatabase; + templateDB: Signal; _handleDeviceDBLoad(e: CustomEvent): void; postDBSetUpdate(): void; } - -export const VMTemplateDBMixin = >( - superClass: T, -) => { - class VMTemplateDBMixinClass extends superClass { - connectedCallback(): void { - const root = super.connectedCallback(); - window.VM.vm.addEventListener( - "vm-template-db-loaded", - this._handleDeviceDBLoad.bind(this), - ); - if (typeof window.VM.vm.templateDB !== "undefined") { - this.templateDB = window.VM.vm.templateDB!; - } - return root; - } - - disconnectedCallback(): void { - window.VM.vm.removeEventListener( - "vm-template-db-loaded", - this._handleDeviceDBLoad.bind(this), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.templateDB = e.detail; - } - - private _templateDB: TemplateDatabase; - - get templateDB(): TemplateDatabase { - return this._templateDB; - } - - postDBSetUpdate(): void { } - - @state() - set templateDB(val: TemplateDatabase) { - this._templateDB = val; - this.postDBSetUpdate(); - } - } - - return VMTemplateDBMixinClass as Constructor & T; -}; diff --git a/www/src/ts/virtualMachine/controls.ts b/www/src/ts/virtualMachine/controls.ts index 1f100d8..29645e2 100644 --- a/www/src/ts/virtualMachine/controls.ts +++ b/www/src/ts/virtualMachine/controls.ts @@ -1,29 +1,15 @@ import { html, css, nothing } from "lit"; import { customElement, query } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { ComputedObjectSignals, globalObjectSignalMap, VMActiveICMixin } from "virtualMachine/baseDevice"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; -import { computed, Signal, watch } from "@lit-labs/preact-signals"; +import { computed, Signal, SignalWatcher, watch } from "@lit-labs/preact-signals"; import { FrozenObjectFull } from "ic10emu_wasm"; +import { VMObjectMixin } from "./baseDevice"; +import { createRef, Ref, ref } from "lit/directives/ref.js"; @customElement("vm-ic-controls") -export class VMICControls extends VMActiveICMixin(BaseElement) { - - circuitHolders: Signal; - - constructor() { - super(); - this.subscribe("active-ic") - this.circuitHolders = computed(() => { - const ids = window.VM.vm.circuitHolderIds.value; - const circuitHolders = []; - for (const id of ids) { - circuitHolders.push(globalObjectSignalMap.get(id)); - } - return circuitHolders; - }); - } +export class VMICControls extends VMObjectMixin(SignalWatcher(BaseElement)) { static styles = [ ...defaultCss, @@ -74,37 +60,89 @@ export class VMICControls extends VMActiveICMixin(BaseElement) { `, ]; - @query(".active-ic-select") activeICSelect: SlSelect; - - forceSelectUpdate() { - if (this.activeICSelect != null) { - this.activeICSelect.handleValueChange(); - } + constructor() { + super(); + this.activeIC.subscribe(() => this.forceSelectUpdate()); + this.icOptions.subscribe(() => this.forceSelectUpdate()); } - protected render() { - const icsOptions = computed(() => { - return this.circuitHolders.value.map((circuitHolder) => { + activeICSelect: Ref = createRef(); - circuitHolder.prefabName.subscribe((_) => {this.forceSelectUpdate()}); - circuitHolder.id.subscribe((_) => {this.forceSelectUpdate()}); - circuitHolder.displayName.subscribe((_) => {this.forceSelectUpdate()}); + selectUpdateTimeout: ReturnType = null; - const span = circuitHolder.name ? html`${watch(circuitHolder.prefabName)}` : nothing ; - return html` - - ${span} - Device:${watch(circuitHolder.id)} ${watch(circuitHolder.displayName)} - ` + forceSelectUpdate() { + if (this.selectUpdateTimeout) { + clearTimeout(this.selectUpdateTimeout); + } + this.selectUpdateTimeout = setTimeout(() => { + if (this.activeICSelect.value != null) { + this.activeICSelect.value.value = this.activeIC.value.toString(); + this.activeICSelect.value.handleValueChange(); + } + }, 100); + } + + activeIC = computed(() => { + return this.vm.value?.activeIC.value + }) + + circuitHolderIds = computed(() => { + return this.vm.value?.state.circuitHolderIds.value ?? []; + }); + + errors = computed(() => { + const obj = this.vm.value?.state.getObject(this.activeIC.value).value; + return obj?.obj_info.compile_errors ?? []; + }); + + icIP = computed(() => { + const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value; + return circuit?.instruction_pointer ?? null; + }); + + icOpCount = computed(() => { + const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value; + return circuit?.yield_instruction_count ?? 0; + }); + + icState = computed(() => { + const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value; + return circuit?.state ?? null; + }); + + + icOptions = computed(() => { + return this.circuitHolderIds.value.map(id => { + const circuitHolder = computed(() => { + return this.vm.value?.state.getObject(id).value; }); + + const prefabName = computed(() => { + return circuitHolder.value?.obj_info.prefab ?? ""; + }); + const displayName = computed(() => { + return circuitHolder.value?.obj_info.name ?? circuitHolder.value?.obj_info.prefab ?? ""; + }); + + prefabName.subscribe(() => this.forceSelectUpdate()); + displayName.subscribe(() => this.forceSelectUpdate()); + + const span = html`${watch(displayName)}`; + return html` + + ${span} + Device:${id} ${watch(displayName)} + ` }); - icsOptions.subscribe((_) => {this.forceSelectUpdate()}); + }); + + render() { const icErrors = computed(() => { - return this.objectSignals?.errors.value?.map( + return this.errors.value.map( (err) => typeof err === "object" && "ParseError" in err @@ -169,28 +207,29 @@ export class VMICControls extends VMActiveICMixin(BaseElement) { hoist size="small" placement="bottom" - value="${watch(this.objectID)}" + value="${this.activeIC.value}" @sl-change=${this._handleChangeActiveIC} class="active-ic-select" + ${ref(this.activeICSelect)} > - ${watch(icsOptions)} + ${watch(this.icOptions)}
Instruction Pointer - ${this.objectSignals ? watch(this.objectSignals.icIP) : nothing} + ${watch(this.icIP)}
Last Run Operations Count - ${this.objectSignals ? watch(this.objectSignals.icOpCount) : nothing} + ${watch(this.icOpCount)}
Last State - ${this.objectSignals ? watch(this.objectSignals.icState) : nothing} + ${watch(this.icState)}
diff --git a/www/src/ts/virtualMachine/device/addDevice.ts b/www/src/ts/virtualMachine/device/addDevice.ts index 4c034ef..a916dfe 100644 --- a/www/src/ts/virtualMachine/device/addDevice.ts +++ b/www/src/ts/virtualMachine/device/addDevice.ts @@ -1,4 +1,4 @@ -import { html, css } from "lit"; +import { html, css, nothing } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; @@ -10,16 +10,24 @@ import { cache } from "lit/directives/cache.js"; import { default as uFuzzy } from "@leeoniya/ufuzzy"; import { when } from "lit/directives/when.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { VMTemplateDBMixin } from "virtualMachine/baseDevice"; import { LogicInfo, ObjectTemplate, StructureInfo } from "ic10emu_wasm"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; +import { computed, ReadonlySignal, signal, Signal, watch } from "@lit-labs/preact-signals"; +import { isSome, range, structuralEqual } from "utils"; type LogicableStructureTemplate = Extract< ObjectTemplate, { structure: StructureInfo; logic: LogicInfo } >; +type SearchResult = { + entry: LogicableStructureTemplate; + haystackEntry: string; + ranges: number[]; +}; + @customElement("vm-add-device-button") -export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) { +export class VMAddDeviceButton extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -38,214 +46,262 @@ export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) { @query("sl-drawer") drawer: SlDrawer; @query(".device-search-input") searchInput: SlInput; - private _structures: Map = new Map(); - private _datapoints: [string, string][] = []; - private _haystack: string[] = []; + templateDB = computed(() => { + return this.vm.value?.state.templateDB.value ?? null; + }); - postDBSetUpdate(): void { - this._structures = new Map( - Array.from(Object.values(this.templateDB)).flatMap((template) => { - if ("structure" in template && "logic" in template) { - return [[template.prefab.prefab_name, template]] as [ - string, - LogicableStructureTemplate, - ][]; - } else { - return [] as [string, LogicableStructureTemplate][]; - } - }), - ); - - const datapoints: [string, string][] = []; - for (const entry of this._structures.values()) { - datapoints.push( - [entry.prefab.name, entry.prefab.prefab_name], - [entry.prefab.prefab_name, entry.prefab.prefab_name], - [entry.prefab.desc, entry.prefab.prefab_name], + structures = (() => { + let last: Map = null + return computed(() => { + const next = new Map( + Array.from(Object.values(this.templateDB.value ?? {})).flatMap((template) => { + if ("structure" in template && "logic" in template) { + return [[template.prefab.prefab_name, template]] as [ + string, + LogicableStructureTemplate, + ][]; + } else { + return [] as [string, LogicableStructureTemplate][]; + } + }), ); - } - const haystack: string[] = datapoints.map((data) => data[0]); - this._datapoints = datapoints; - this._haystack = haystack; - this.performSearch(); - } + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); - private _filter: string = ""; + datapoints = (() => { + let last: [string, string][] = null; + return computed(() => { + const next = [...this.structures.value.values()].flatMap((entry): [string, string][] => { + return [ + [entry.prefab.name, entry.prefab.prefab_name], + [entry.prefab.prefab_name, entry.prefab.prefab_name], + [entry.prefab.desc, entry.prefab.prefab_name], + ] + }); + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); + + haystack = (() => { + let last: string[] = null; + return computed(() => { + const next = this.datapoints.value.map(data => data[0]); + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); + + private _filter: Signal = signal(""); + private page = signal(0); get filter() { - return this._filter; + return this._filter.peek(); } - @state() set filter(val: string) { - this._filter = val; - this.page = 0; - this.performSearch(); + this._filter.value = val; + this.page.value = 0; } - private _searchResults: { - entry: LogicableStructureTemplate; - haystackEntry: string; - ranges: number[]; - }[] = []; + private searchResults: ReadonlySignal = (() => { + let last: SearchResult[] = null; + return computed((): SearchResult[] => { + let next: SearchResult[]; + if (this._filter.value) { + const uf = new uFuzzy({}); + const [_idxs, info, order] = uf.search( + this.haystack.value, + this._filter.value, + 0, + 1e3, + ); - private filterTimeout: number | undefined; + const filtered = order?.map((infoIdx) => ({ + name: this.datapoints.value[info.idx[infoIdx]][1], + haystackEntry: this.haystack.value[info.idx[infoIdx]], + ranges: info.ranges[infoIdx], + })); - performSearch() { - if (this._filter) { - const uf = new uFuzzy({}); - const [_idxs, info, order] = uf.search( - this._haystack, - this._filter, - 0, - 1e3, - ); + const unique = [...new Set(filtered.map((obj) => obj.name))].map( + (result) => { + return filtered.find((obj) => obj.name === result); + }, + ); - const filtered = order?.map((infoIdx) => ({ - name: this._datapoints[info.idx[infoIdx]][1], - haystackEntry: this._haystack[info.idx[infoIdx]], - ranges: info.ranges[infoIdx], - })); + next = unique.map(({ name, haystackEntry, ranges }) => ({ + entry: this.structures.value.get(name)!, + haystackEntry, + ranges, + })); + } else { + // return everything + next = [...this.structures.value.values()].map((st) => ({ + entry: st, + haystackEntry: st.prefab.prefab_name, + ranges: [], + })); + } + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); - const unique = [...new Set(filtered.map((obj) => obj.name))].map( - (result) => { - return filtered.find((obj) => obj.name === result); - }, - ); + numSearchResults = computed(() => { + return this.searchResults.value?.length ?? 0; + }) - this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({ - entry: this._structures.get(name)!, - haystackEntry, - ranges, - })); - } else { - // return everything - this._searchResults = [...this._structures.values()].map((st) => ({ - entry: st, - haystackEntry: st.prefab.prefab_name, - ranges: [], - })); - } - } + private filterTimeout: ReturnType; - connectedCallback(): void { - super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener( - "vm-template-db-loaded", - this._handleDeviceDBLoad.bind(this), - ), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.templateDB = e.detail; - } - - @state() private page = 0; + perPage: Signal = signal(40); + maxResultsRendered: Signal = signal(20); renderSearchResults() { - const perPage = 40; - const totalPages = Math.ceil((this._searchResults?.length ?? 0) / perPage); - let pageKeys = Array.from({ length: totalPages }, (_, index) => index); - const extra: { + const totalPages = computed(() => Math.ceil((this.searchResults.value?.length ?? 0) / this.perPage.value)); + const pageKeys = computed(() => range(totalPages.value)); + const extra = computed((): { entry: { prefab: { name: string; prefab_name: string } }; haystackEntry: string; ranges: number[]; - }[] = []; - if (this.page < totalPages - 1) { - extra.push({ - entry: { prefab: { name: "", prefab_name: this.filter } }, - haystackEntry: "...", - ranges: [], - }); - } - return when( - typeof this._searchResults !== "undefined" && - this._searchResults.length < 20, - () => - repeat( - this._searchResults ?? [], + }[] => { + const next: { + entry: { prefab: { name: string; prefab_name: string } }; + haystackEntry: string; + ranges: number[]; + }[] = []; + + if (this.page.value < totalPages.value - 1) { + next.push({ + entry: { prefab: { name: "", prefab_name: this.filter } }, + haystackEntry: "...", + ranges: [], + }); + } + return next; + }); + const pageKeyButtons = computed(() => { + return pageKeys.value.map( + (key, index) => { + const textColorClass = computed(() => index === this.page.value ? "text-purple-500" : nothing) + return html` + + ${key + 1}${index < totalPages.value - 1 ? "," : nothing} + + ` + } + ) + }) + + const results = computed(() => [ + ...this.searchResults.value.slice( + this.perPage.value * this.page.value, + this.perPage.value * this.page.value + this.perPage.value, + ), + ...extra.value, + ].map((result) => { + let hay = result.haystackEntry.slice(0, 15); + if (result.haystackEntry.length > 15) hay += "..."; + const ranges = result.ranges.filter((pos) => pos < 20); + const key = result.entry.prefab.prefab_name; + return html` +
+ ${result.entry.prefab.name} ( + ${ranges.length + ? unsafeHTML(uFuzzy.highlight(hay, ranges)) + : hay + } ) +
+ `; + })); + + const cards = computed(() => { + if (this.numSearchResults.value <= this.maxResultsRendered.value) { + return repeat( + this.searchResults.value ?? [], (result) => result.entry.prefab.prefab_name, (result) => - cache(html` - - - `), - ), - () => html` + html` + + + `, + ); + } else { + return nothing; + } + }); + const searchResultsHtml = computed(() => { + if (this.numSearchResults.value > 0 && this.numSearchResults.value <= this.maxResultsRendered.value) { + return html`${watch(cards)}` + } else { + const excessResults = this.numSearchResults.value - this.maxResultsRendered.value + const filterText = (() => { + if (this.numSearchResults.value > this.maxResultsRendered.value) { + return html`, filter ${excessResults} more to get cards` + } + return nothing + })(); + return html`

- results, filter more to get cards + results${filterText}

Page: - ${pageKeys.map( - (key, index) => html` - ${key + 1}${index < totalPages - 1 ? "," : ""} - `, - )} + ${watch(pageKeyButtons)}
- ${[ - ...this._searchResults.slice( - perPage * this.page, - perPage * this.page + perPage, - ), - ...extra, - ].map((result) => { - let hay = result.haystackEntry.slice(0, 15); - if (result.haystackEntry.length > 15) hay += "..."; - const ranges = result.ranges.filter((pos) => pos < 20); - const key = result.entry.prefab.prefab_name; - return html` -
- ${result.entry.prefab.name} ( - ${ranges.length - ? unsafeHTML(uFuzzy.highlight(hay, ranges)) - : hay} ) -
- `; - })} + ${watch(results)}
- `, - ); + ` + + } + }); + return html`${watch(searchResultsHtml)}`; } _handlePageChange(e: Event) { const span = e.currentTarget as HTMLSpanElement; const key = parseInt(span.getAttribute("key")); - this.page = key; + this.page.value = key; } _handleHaystackClick(e: Event) { const div = e.currentTarget as HTMLDivElement; const key = div.getAttribute("key"); if (key === this.filter) { - this.page += 1; + this.page.value += 1; } else { this.filter = key; this.searchInput.value = key; @@ -284,8 +340,8 @@ export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) { slot="footer" variant="primary" @click=${() => { - this.drawer.hide(); - }} + this.drawer.hide(); + }} > Close diff --git a/www/src/ts/virtualMachine/device/card.ts b/www/src/ts/virtualMachine/device/card.ts index c238983..0f738c8 100644 --- a/www/src/ts/virtualMachine/device/card.ts +++ b/www/src/ts/virtualMachine/device/card.ts @@ -1,10 +1,10 @@ -import { html, css, HTMLTemplateResult } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { watch, SignalWatcher, computed } from '@lit-labs/preact-signals'; +import { html, css, HTMLTemplateResult, nothing } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { watch, computed } from '@lit-labs/preact-signals'; import { BaseElement, defaultCss } from "components"; -import { VMTemplateDBMixin, VMObjectMixin, globalObjectSignalMap } from "virtualMachine/baseDevice"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; -import { parseIntWithHexOrBinary, parseNumber } from "utils"; +import { crc32, isSome, parseIntWithHexOrBinary, range } from "utils"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; import "./slot"; @@ -12,13 +12,13 @@ import "./fields"; import "./pins"; import { until } from "lit/directives/until.js"; import { repeat } from "lit/directives/repeat.js"; +import { Connection } from "ic10emu_wasm"; +import { createRef, ref, Ref } from "lit/directives/ref.js"; export type CardTab = "fields" | "slots" | "reagents" | "networks" | "pins"; @customElement("vm-device-card") -export class VMDeviceCard extends VMTemplateDBMixin( - VMObjectMixin(SignalWatcher(BaseElement)), -) { +export class VMDeviceCard extends VMObjectMixin(BaseElement) { image_err: boolean; @property({ type: Boolean }) open: boolean; @@ -26,9 +26,6 @@ export class VMDeviceCard extends VMTemplateDBMixin( constructor() { super(); this.open = false; - this.subscribe( - "active-ic", - ); } static styles = [ @@ -120,114 +117,117 @@ export class VMDeviceCard extends VMTemplateDBMixin( `, ]; - _handleDeviceDBLoad(e: CustomEvent): void { - super._handleDeviceDBLoad(e); - this.updateObject(); - } - onImageErr(e: Event) { this.image_err = true; console.log("Image load error", e); } + thisIsActiveIc = computed(() => { + return this.vm.value?.activeIC.value === this.objectIDSignal.value; + }); + + activeIcPins = computed(() => { + return this.vm.value?.state.getDevicePins(this.vm.value?.activeIC.value).value ?? []; + }); + + prefabName = computed(() => { + return this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.prefab ?? "unknown"; + }); + + objectName = computed(() => { + return this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.name ?? ""; + }); + + objectNameHash = computed(() => { + return crc32(this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.name ?? ""); + }); + + renderHeader(): HTMLTemplateResult { - const thisIsActiveIc = computed(() => { - return this.activeICId.value === this.objectID.value; - }); - - const activeIc = computed(() => { - return globalObjectSignalMap.get(this.activeICId.value); - }); - - const numPins = computed(() => { - return activeIc.value.numPins.value; - }); - - const pins = computed(() => { - return new Array(numPins.value) - .fill(true) - .map((_, index) => this.objectSignals.pins.value.get(index)); - }); const badgesHtml = computed(() => { - const badges: HTMLTemplateResult[] = []; - if (thisIsActiveIc.value) { + if (this.thisIsActiveIc.value) { badges.push(html`db`); } - pins.value.forEach((id, index) => { - if (this.objectID.value == id) { + this.activeIcPins.value.forEach(([pin, id]) => { + if (this.objectIDSignal.value == id) { badges.push( - html`d${index}`, + html`d${pin}`, ); } }, this); return badges }); + + const removeText = computed(() => { + return this.thisIsActiveIc.value + ? "Removing the selected Active IC is disabled" + : "Remove Device"; + }); + return html` - +
Id Name Hash ${watch(badgesHtml)}
@@ -238,7 +238,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( renderFields() { return this.delayRenderTab( "fields", - html``, + html``, ); } @@ -249,17 +249,24 @@ export class VMDeviceCard extends VMTemplateDBMixin( static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; + objectSlotCount = computed(() => { + return this.vm.value?.state.getObjectSlotCount(this.objectIDSignal.value).value; + }); + async renderSlots() { + const slotsHtml = computed(() => { + return repeat(range(this.objectSlotCount.value), + (_slot, index) => html` + + + `, + ); + }); return this.delayRenderTab( "slots", html`
- ${repeat(Array(this.objectSignals.slotsCount), - (_slot, index) => html` - - - `, - )} + ${watch(slotsHtml)}
`, ); @@ -269,37 +276,86 @@ export class VMDeviceCard extends VMTemplateDBMixin( return this.delayRenderTab("reagents", html``); } - renderNetworks() { - const vmNetworks = window.VM.vm.networkIds; - const networks = this.objectSignals.connections.value.map((connection, index, _conns) => { - const conn = - typeof connection === "object" && "CableNetwork" in connection - ? connection.CableNetwork - : null; + networkIds = computed(() => { + return this.vm.value?.state.networkIds.value ?? []; + }); + + numConnections = computed(() => { + return this.vm.value?.state.getObjectConnectionCount(this.objectIDSignal.value).value; + }) + + private _connectionsSelectRefMap: Map> = new Map(); + + getConnectionSelectRef(index: number): Ref { + if (!this._connectionsSelectRefMap.has(index)) { + this._connectionsSelectRefMap.set(index, createRef()); + } + return this._connectionsSelectRefMap.get(index); + } + + forceSelectUpdate(...slSelects: Ref[]) { + for (const slSelect of slSelects) { + if (slSelect.value != null && "handleValueChange" in slSelect.value) { + slSelect.value.handleValueChange(); + } + } + } + + renderConnections() { + const connectionsHtml = computed(() => range(this.numConnections.value).map(index => { + const conn = computed(() => { + return this.vm.value?.state.getObjectConnection(this.objectIDSignal.value, index).value; + }); + const connNet = computed(() => { + const connection: Connection = conn.value ?? "None"; + if (typeof connection === "object" && "CableNetwork" in connection) { + return connection.CableNetwork.net; + } + return null; + }); + const selectDisabled = computed(() => !isSome(connNet.value)); + const selectOptions = computed(() => { + return this.networkIds.value.map(id => html` + + Network ${id} + + `); + }); + const connTyp = computed(() => { + const connection: Connection = conn.value ?? "None"; + return typeof connection === "object" ? Object.keys(connection)[0] : connection; + }); + + const connectionSelectRef = this.getConnectionSelectRef(index); + selectOptions.subscribe(() => {this.forceSelectUpdate(connectionSelectRef)}) + + connNet.subscribe((net) => { + if (isSome(connectionSelectRef.value)) { + connectionSelectRef.value.value = net.toString(0) + connectionSelectRef.value.handleValueChange(); + } + }) + return html` Connection:${index} - ${vmNetworks.value.map( - (net) => - html`Network ${net}`, - )} - ${conn?.typ} + ${watch(selectOptions)} + ${watch(connTyp)} `; - }); + })); return this.delayRenderTab( "networks", - html`
${networks}
`, + html`
${watch(connectionsHtml)}
`, ); } @@ -307,7 +363,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( return this.delayRenderTab( "pins", html`
- +
`, ); } @@ -319,12 +375,12 @@ export class VMDeviceCard extends VMTemplateDBMixin( resolver?: (result: HTMLTemplateResult) => void; }; } = { - fields: {}, - slots: {}, - reagents: {}, - networks: {}, - pins: {}, - }; + fields: {}, + slots: {}, + reagents: {}, + networks: {}, + pins: {}, + }; delayRenderTab( name: CardTab, @@ -351,9 +407,22 @@ export class VMDeviceCard extends VMTemplateDBMixin( } } + numPins = computed(() => { + return this.vm.value?.state.getDeviceNumPins(this.objectIDSignal.value) + }); + + displayName = computed(() => { + const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value; + return obj?.obj_info.name ?? obj?.obj_info.prefab ?? null; + }); + + imageName = computed(() => { + const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value; + return obj?.obj_info.prefab ?? "error"; + }); + render(): HTMLTemplateResult { - const disablePins = computed(() => {return !this.objectSignals.numPins.value;}); - const displayName = computed(() => { return this.objectSignals.name.value ?? this.objectSignals.prefabName.value}) + const disablePins = computed(() => { return !this.numPins.value; }); return html`
${this.renderHeader()}
@@ -376,7 +445,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( ${until(this.renderReagents(), html``)} - ${until(this.renderNetworks(), html``)} + ${until(this.renderConnections(), html``)} ${until(this.renderPins(), html``)} @@ -391,12 +460,12 @@ export class VMDeviceCard extends VMTemplateDBMixin(

Are you sure you want to remove this device?

- Id ${this.objectID} : ${watch(displayName)} + Id ${watch(this.objectIDSignal)} : ${watch(this.displayName)}
@@ -435,7 +504,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( const val = parseIntWithHexOrBinary(input.value); if (!isNaN(val)) { window.VM.get().then((vm) => { - if (!vm.changeObjectID(this.objectID.peek(), val)) { + if (!vm.changeObjectID(this.objectID, val)) { input.value = this.objectID.toString(); } }); @@ -448,10 +517,9 @@ export class VMDeviceCard extends VMTemplateDBMixin( const input = e.target as SlInput; const name = input.value.length === 0 ? undefined : input.value; window.VM.get().then((vm) => { - if (!vm.setObjectName(this.objectID.peek(), name)) { - input.value = this.objectSignals.name.value; + if (!vm.setObjectName(this.objectID, name)) { + input.value = this.objectName.peek(); } - this.updateObject(); }); } _handleDeviceRemoveButton(_e: Event) { @@ -460,7 +528,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( _removeDialogRemove() { this.removeDialog.hide(); - window.VM.get().then((vm) => vm.removeDevice(this.objectID.peek())); + window.VM.get().then((vm) => vm.removeDevice(this.objectID)); } _handleChangeConnection(e: CustomEvent) { @@ -468,8 +536,7 @@ export class VMDeviceCard extends VMTemplateDBMixin( const conn = parseInt(select.getAttribute("key")!); const val = select.value ? parseInt(select.value as string) : undefined; window.VM.get().then((vm) => - vm.setDeviceConnection(this.objectID.peek(), conn, val), + vm.setDeviceConnection(this.objectID, conn, val), ); - this.updateObject(); } } diff --git a/www/src/ts/virtualMachine/device/dbutils.ts b/www/src/ts/virtualMachine/device/dbutils.ts index d9b66a1..6348fb8 100644 --- a/www/src/ts/virtualMachine/device/dbutils.ts +++ b/www/src/ts/virtualMachine/device/dbutils.ts @@ -12,7 +12,7 @@ export function connectionFromConnectionInfo(conn: ConnectionInfo): Connection { ) { connection = { CableNetwork: { - net: window.VM.vm.defaultNetwork.peek(), + net: window.VM.vm.state.defaultNetworkId.peek(), typ: conn.typ as CableConnectionType, role: conn.role, }, diff --git a/www/src/ts/virtualMachine/device/deviceList.ts b/www/src/ts/virtualMachine/device/deviceList.ts index 178a13a..bc54390 100644 --- a/www/src/ts/virtualMachine/device/deviceList.ts +++ b/www/src/ts/virtualMachine/device/deviceList.ts @@ -3,23 +3,19 @@ import { customElement, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { structuralEqual } from "utils"; +import { isSome, structuralEqual } from "utils"; import { repeat } from "lit/directives/repeat.js"; import { default as uFuzzy } from "@leeoniya/ufuzzy"; import { VMSlotAddDialog } from "./slotAddDialog"; import "./addDevice" import { SlotModifyEvent } from "./slot"; -import { computed, Signal, signal, SignalWatcher, watch } from "@lit-labs/preact-signals"; -import { globalObjectSignalMap } from "virtualMachine/baseDevice"; +import { computed, ReadonlySignal, Signal, signal, SignalWatcher, watch } from "@lit-labs/preact-signals"; import { ObjectID } from "ic10emu_wasm"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; @customElement("vm-device-list") -export class VMDeviceList extends SignalWatcher(BaseElement) { - devices: Signal; - private _filter: Signal = signal(""); - private _filteredDeviceIds: Signal; - +export class VMDeviceList extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -48,34 +44,42 @@ export class VMDeviceList extends SignalWatcher(BaseElement) { constructor() { super(); - this.devices = computed(() => { - const objIds = window.VM.vm.objectIds.value; - const deviceIds = []; - for (const id of objIds) { - const obj = window.VM.vm.objects.get(id); - const info = obj.value.obj_info; - if (!(info.parent_slot != null || info.root_parent_human != null)) { - deviceIds.push(id) + } + + devices: ReadonlySignal = (() => { + let last: ObjectID[] = null; + return computed(() => { + const vm = this.vm.value; + const next: ObjectID[] = vm?.state.vm.value?.objects.flatMap((obj): ObjectID[] => { + if (!isSome(obj.obj_info.parent_slot) && !isSome(obj.obj_info.root_parent_human)) { + return [obj.obj_info.id] } + return []; + }) + if (structuralEqual(last, next)) { + return last; } - deviceIds.sort(); - return deviceIds; + last = next; + return next; }); - this._filteredDeviceIds = computed(() => { + })(); + + private _filter: Signal = signal(""); + private _filteredDeviceIds: ReadonlySignal = (() => { + let last: ObjectID[] = null; + return computed(() => { + const vm = this.vm.value; + let next = this.devices.value; if (this._filter.value) { const datapoints: [string, number][] = []; for (const device_id of this.devices.value) { - const device = globalObjectSignalMap.get(device_id); - if (device) { - const name = device.name.peek(); - const id = device.id.peek(); - const prefab = device.prefabName.peek(); - if (name != null) { - datapoints.push([name, id]); - } - if (prefab != null) { - datapoints.push([prefab, id]); - } + const name = vm?.state.getObjectName(device_id).value; + const prefab = vm?.state.getObjectPrefabName(device_id).value; + if (name != null) { + datapoints.push([name, device_id]); + } + if (prefab != null) { + datapoints.push([prefab, device_id]); } } const haystack: string[] = datapoints.map((data) => data[0]); @@ -87,12 +91,15 @@ export class VMDeviceList extends SignalWatcher(BaseElement) { filtered ?.map((data) => data[1]) ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? []; - return deviceIds; - } else { - return Array.from(this.devices.value); + next = deviceIds; } + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; }); - } + })(); protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { this.renderRoot.querySelector(".device-list").addEventListener( @@ -102,13 +109,13 @@ export class VMDeviceList extends SignalWatcher(BaseElement) { } protected render(): HTMLTemplateResult { - const deviceCards = repeat( + const deviceCards = computed(() => repeat( this.filteredDeviceIds.value, (id) => id, (id) => - html` + html` `, - ); + )); const numDevices = computed(() => this.devices.value.length); const result = html`
@@ -126,7 +133,7 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
-
${deviceCards}
+
${watch(deviceCards)}
`; @@ -138,7 +145,7 @@ export class VMDeviceList extends SignalWatcher(BaseElement) { _showDeviceSlotDialog( e: CustomEvent, ) { - this.slotDialog.show(e.detail.deviceID, e.detail.slotIndex); + this.slotDialog.show(e.detail.objectID, e.detail.slotIndex); } get filteredDeviceIds() { diff --git a/www/src/ts/virtualMachine/device/fields.ts b/www/src/ts/virtualMachine/device/fields.ts index d71b1ad..dfd6b00 100644 --- a/www/src/ts/virtualMachine/device/fields.ts +++ b/www/src/ts/virtualMachine/device/fields.ts @@ -1,41 +1,36 @@ import { html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { VMTemplateDBMixin, VMObjectMixin } from "virtualMachine/baseDevice"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; import { displayNumber, parseNumber } from "utils"; import type { LogicType } from "ic10emu_wasm"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import { computed, Signal, watch } from "@lit-labs/preact-signals"; @customElement("vm-device-fields") -export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(BaseElement)) { +export class VMDeviceSlot extends VMObjectMixin(BaseElement) { constructor() { super(); - this.setupSignals(); } - setupSignals() { - this.logicFieldNames = computed(() => { - return Array.from(this.objectSignals.logicFields.value.keys()); - }); - } - - logicFieldNames: Signal; + logicFieldNames = computed(() => { + return this.vm.value?.state.getObjectFieldNames(this.objectIDSignal.value).value; + }); render() { const inputIdBase = `vmDeviceCard${this.objectID}Field`; const fieldsHtml = computed(() => { return this.logicFieldNames.value.map((name) => { const field = computed(() => { - return this.objectSignals.logicFields.value.get(name); + return this.vm.value?.state.getObjectField(this.objectIDSignal.value, name).value ?? null; }); const typ = computed(() => { - return field.value.field_type; + return field.value?.field_type ?? null; }); const value = computed(() => { - return displayNumber(field.value.value); + return displayNumber(field.value?.value ?? null); }); - return html` ${name} @@ -53,10 +48,9 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(BaseElement)) const field = input.getAttribute("key")! as LogicType; const val = parseNumber(input.value); window.VM.get().then((vm) => { - if (!vm.setObjectField(this.objectID.peek(), field, val, true)) { - input.value = this.objectSignals.logicFields.value.get(field).value.toString(); + if (!vm.setObjectField(this.objectID, field, val, true)) { + input.value = displayNumber(this.vm.value?.state.getObjectField(this.objectIDSignal.value, field).value?.value ?? null); } - this.updateObject(); }); } } diff --git a/www/src/ts/virtualMachine/device/pins.ts b/www/src/ts/virtualMachine/device/pins.ts index 240a8b3..8a8bcd1 100644 --- a/www/src/ts/virtualMachine/device/pins.ts +++ b/www/src/ts/virtualMachine/device/pins.ts @@ -1,61 +1,86 @@ -import { html, css } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "components"; -import { VMTemplateDBMixin, VMObjectMixin } from "virtualMachine/baseDevice"; +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { BaseElement } from "components"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; -import { ObjectID } from "ic10emu_wasm"; -import { effect, watch } from "@lit-labs/preact-signals"; -import { SlOption } from "@shoelace-style/shoelace"; +import { ObjectID, ObjectTemplate } from "ic10emu_wasm"; +import { computed, watch } from "@lit-labs/preact-signals"; +import { createRef, ref, Ref } from "lit/directives/ref.js"; +import { isSome, range } from "utils"; @customElement("vm-device-pins") -export class VMDevicePins extends VMObjectMixin(VMTemplateDBMixin(BaseElement)) { - constructor() { - super(); - // this.subscribe("visible-devices"); +export class VMDevicePins extends VMObjectMixin(BaseElement) { + + forceSelectUpdate(...slSelects: Ref[]) { + for (const slSelect of slSelects) { + if (slSelect.value != null && "handleValueChange" in slSelect.value) { + slSelect.value.handleValueChange(); + } + } } - render() { - const pins = new Array(this.objectSignals.numPins.value ?? 0) - .fill(true) - .map((_, index) => this.objectSignals.pins.value.get(index)); - const visibleDevices = (this.objectSignals.visibleDevices.value ?? []); - const forceSelectUpdate = () => { - const slSelect = this.renderRoot.querySelector("sl-select") as SlSelect; - if (slSelect != null) { - slSelect.handleValueChange(); - } - }; - const pinsHtml = pins?.map( - (pin, index) => { - return html` - d${index} - ${visibleDevices.map( - (device, _index) => { - device.id.subscribe((id: ObjectID) => { - forceSelectUpdate(); - }); - device.displayName.subscribe((_: string) => { - forceSelectUpdate(); - }); - return html` - - Device ${watch(device.id)} : - ${watch(device.displayName)} - - ` - } + private _pinSelectRefMap: Map> = new Map(); - )} - `; - } - ); + getPinSelectRef(index: number): Ref { + if (!this._pinSelectRefMap.has(index)) { + this._pinSelectRefMap.set(index, createRef()); + } + return this._pinSelectRefMap.get(index); + } + + visibleDeviceIds = computed(() => { + const vm = this.vm.value; + const obj = vm?.state.getObject(this.objectIDSignal.value).value + return obj?.obj_info.visible_devices ?? []; + }); + + numPins = computed(() => { + const vm = this.vm.value; + return vm?.state.getDeviceNumPins(this.objectIDSignal.value).value; + }) + + deviceOptions = computed(() => { + return this.visibleDeviceIds.value.map(id => { + const deviceDisplayName = this.vm.value?.state.getObjectDisplayName(id); + deviceDisplayName.subscribe(() => { + this.forceSelectUpdate(...this._pinSelectRefMap.values()); + }); + return html` + + Device ${id} : + ${watch(deviceDisplayName)} + + ` + }); + }); + + render() { + const pinsHtml = computed(() => { + return range(this.numPins.value).map( + index => { + const selectRef = this.getPinSelectRef(index); + const pin = computed(() => { + const vm = this.vm.value; + return vm?.state.getDevicePin(this.objectIDSignal.value, index).value; + }); + + return html` + + d${index} + ${watch(this.deviceOptions)} + + `; + } + ); + }); return pinsHtml; } @@ -63,7 +88,6 @@ export class VMDevicePins extends VMObjectMixin(VMTemplateDBMixin(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.get().then((vm) => vm.setDevicePin(this.objectID.peek(), pin, val)); - this.updateObject(); + window.VM.get().then((vm) => vm.setDevicePin(this.objectID, pin, val)); } } diff --git a/www/src/ts/virtualMachine/device/slot.ts b/www/src/ts/virtualMachine/device/slot.ts index 6f05e04..cf34878 100644 --- a/www/src/ts/virtualMachine/device/slot.ts +++ b/www/src/ts/virtualMachine/device/slot.ts @@ -1,11 +1,12 @@ import { html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { VMTemplateDBMixin, VMObjectMixin, VmObjectSlotInfo, ComputedObjectSignals } from "virtualMachine/baseDevice"; +import { VMObjectMixin, } from "virtualMachine/baseDevice"; import { clamp, crc32, displayNumber, + isSome, parseNumber, } from "utils"; import { @@ -21,34 +22,22 @@ import { when } from "lit/directives/when.js"; import { computed, signal, Signal, SignalWatcher, watch } from "@lit-labs/preact-signals"; export interface SlotModifyEvent { - deviceID: number; + objectID: number; slotIndex: number; } -@customElement("vm-device-slot") -export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(BaseElement))) { - private _slotIndex: Signal; +@customElement("vm-object-slot") +export class VMDeviceSlot extends VMObjectMixin(BaseElement) { - slotSignal: Signal; - - get slotIndex() { - return this._slotIndex.value; - } + slotIndexSignal: Signal = signal(0); @property({ type: Number }) - set slotIndex(val: number) { - this._slotIndex.value = val; + get slotIndex() { + return this.slotIndexSignal.peek(); } - constructor() { - super(); - this._slotIndex = signal(0); - this.subscribe("active-ic"); - this.slotSignal = computed(() => { - const index = this._slotIndex.value; - return this.objectSignals.slots.value[index]; - }); - this.setupSignals(); + set slotIndex(val: number) { + this.slotIndexSignal.value = val; } static styles = [ @@ -79,93 +68,86 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( `, ]; - setupSignals() { - this.slotOccupant = computed(() => { - const slot = this.slotSignal.value ?? null; - return slot?.occupant ?? null; - }); - this.slotFieldTypes = computed(() => { - return Array.from(this.slotSignal.value?.logicFields.keys() ?? []) ; - }); - this.slotOccupantImg = computed(() => { - const slot = this.slotSignal.value ?? null; - if (slot != null && slot.occupant != null) { - const prefabName = slot.occupant.prefabName; - return `img/stationpedia/${watch(prefabName)}.png`; - } else { - return `img/stationpedia/SlotIcon_${slot.typ}.png`; - } - }); - this.slotOccupantPrefabName = computed(() => { - const slot = this.slotSignal.value ?? null; - if (slot != null && slot.occupant != null) { - const prefabName = slot.occupant.prefabName.value; - return prefabName; - } else { - return null; - } - }); - this.slotOccupantTemplate = computed(() => { - if (this.objectSignals != null && "slots" in this.objectSignals.template.value) { - return this.objectSignals.template.value.slots[this.slotIndex]; - } else { - return null; - } - }); - } - slotOccupant: Signal; - slotFieldTypes: Signal; - slotOccupantImg: Signal; - slotOccupantPrefabName: Signal; - slotOccupantTemplate: Signal; + slotInfo = computed(() => { + return this.vm.value?.state.getObjectSlotInfo(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? null; + }); + + slotOccupantId = computed(() => { + const slot = this.slotInfo.value ?? null; + return slot?.occupant ?? null; + }); + + slotOccupant = computed(() => { + return this.vm.value?.state.getObject(this.slotOccupantId.value).value; + }); + + slotFieldTypes = computed(() => { + return this.vm.value?.state.getObjectSlotFieldNames(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? []; + }); + + slotOccupantImg = computed(() => { + const occupant = this.slotOccupant.value; + if (isSome(occupant)) { + const prefabName = occupant.obj_info.prefab; + return `img/stationpedia/${prefabName}.png`; + } else { + const slot = this.vm.value?.state.getObjectSlotInfo(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? null; + return `img/stationpedia/SlotIcon_${slot?.typ}.png`; + } + }); + + slotOccupantPrefabName = computed(() => { + const occupant = this.slotOccupant.value; + return occupant?.obj_info.prefab ?? null; + }); + + slotQuantity = computed(() => { + const slot = this.slotInfo.value ?? null; + return slot?.quantity; + }); + + slotTyp = computed(() => { + const slot = this.slotInfo.value ?? null; + return slot?.typ; + }); + + slotName = computed(() => { + const slot = this.slotInfo.value ?? null; + return slot?.name ?? slot?.typ; + }); + + slotDisplayName = computed(() => { + return this.slotOccupantPrefabName.value ?? this.slotName ?? ""; + }); + + maxQuantity = computed(() => { + const occupant = this.slotOccupant.value; + const template = occupant?.template ?? null; + if (isSome(template) && "item" in template) { + return template.item.max_quantity; + } + return 1; + }); renderHeader() { - const inputIdBase = `vmDeviceSlot${this.objectID}Slot${this.slotIndex}Head`; - // const slot = this.slotSignal.value; - const slotImg = this.slotOccupantImg; + // const inputIdBase = computed(() => `vmDeviceSlot${this.objectIDSignal.value}Slot${this.slotIndexSignal.value}Head`); const img = html``; - const template = this.slotOccupantTemplate; - const templateName = computed(() => { - return template.value?.name ?? null; - }); - const slotTyp = computed(() => { - return this.slotSignal.value.typ; - }) const enableQuantityInput = false; - const quantity = computed(() => { - const slot = this.slotSignal.value; - return slot.quantity; - }); - - const maxQuantity = computed(() => { - const slotOccupant = this.slotSignal.value.occupant; - const template = slotOccupant?.template.value ?? null; - if (template != null && "item" in template) { - return template.item.max_quantity; - } else { - return 1; - } - }); - - const slotDisplayName = computed(() => { - return this.slotOccupantPrefabName.value ?? this.slotSignal.value.typ; + const removeDisabled = computed(() => { + return this.vm.value?.activeIC.value === this.objectIDSignal.value && this.slotTyp.value === "ProgrammableChip" }); const tooltipContent = computed(() => { - return this.activeICId === this.objectID && slotTyp.value === "ProgrammableChip" + return removeDisabled.value ? "Removing the selected Active IC is disabled" : "Remove Occupant" - }) - - const removeDisabled = computed(() => { - return this.activeICId === this.objectID && slotTyp.value === "ProgrammableChip" }); const quantityContent = computed(() => { @@ -176,7 +158,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1" > - ${watch(quantity)}/${watch(maxQuantity)} + ${watch(this.slotQuantity)}/${watch(this.maxQuantity)}
` } else { @@ -185,34 +167,30 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( }); const slotName = computed(() => { - if(this.slotOccupant.value != null) { - return html` ${watch(this.slotOccupantPrefabName)} ` - } else { - html` ${watch(templateName)} ` - } + return html` ${watch(this.slotDisplayName)} ` }); const inputContent = computed(() => { - if (this.slotOccupant.value != null) { + if (isSome(this.slotOccupant.value)) { return html`
${enableQuantityInput - ? html`
Max Quantity: - ${watch(maxQuantity)} + ${watch(this.maxQuantity)}
` - : ""} + : ""} @@ -245,7 +223,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( > ${this.slotIndex}
- + ${img} ${watch(quantityContent)} @@ -258,7 +236,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
Type:${watch(slotTyp)} + >${watch(this.slotTyp)}
@@ -268,7 +246,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( } _handleSlotOccupantRemove() { - window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex); + window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex); } _handleSlotClick(_e: Event) { @@ -276,64 +254,57 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( new CustomEvent("device-modify-slot", { bubbles: true, composed: true, - detail: { deviceID: this.objectID.peek(), slotIndex: this.slotIndex }, + detail: { objectID: this.objectID, slotIndex: this.slotIndex }, }), ); } _handleSlotQuantityChange(e: Event) { const input = e.currentTarget as SlInput; - const slot = this.slotSignal.value; const val = clamp( input.valueAsNumber, 1, - "item" in slot.occupant.template.value - ? slot.occupant.template.value.item.max_quantity - : 1, + this.maxQuantity.peek() ); if ( !window.VM.vm.setObjectSlotField( - this.objectID.peek(), + this.objectID, this.slotIndex, "Quantity", val, true, ) ) { - input.value = this.slotSignal.value.quantity.toString(); + input.value = this.slotQuantity.value.toString(); } } renderFields() { - const inputIdBase = `vmDeviceSlot${this.objectID}Slot${this.slotIndex}Field`; + const inputIdBase = computed(() => `vmDeviceSlot${this.objectIDSignal.value}Slot${this.slotIndexSignal.value}Field`); const fields = computed(() => { - const slot = this.slotSignal.value; - const _fields = - slot.logicFields?? - new Map(); return this.slotFieldTypes.value.map( - (name, _index, _types) => { + field => { const slotField = computed(() => { - return this.slotSignal.value.logicFields.get(name); + return this.vm.value?.state.getObjectSlotField(this.objectIDSignal.value, this.slotIndexSignal.value, field).value ?? null; }); const fieldValue = computed(() => { - return displayNumber(slotField.value.value); + return displayNumber(slotField.value?.value ?? null); }) const fieldAccessType = computed(() => { - return slotField.value.field_type; + return slotField.value?.field_type ?? null; }) return html` - ${name} + ${field} ${watch(fieldAccessType)} @@ -354,27 +325,19 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher( const field = input.getAttribute("key")! as LogicSlotType; let val = parseNumber(input.value); if (field === "Quantity") { - const slot = this.slotSignal.value; + const slot = this.slotIndexSignal.value; val = clamp( input.valueAsNumber, 1, - "item" in slot.occupant.template.value - ? slot.occupant.template.value.item.max_quantity - : 1, + this.maxQuantity.peek(), ); } window.VM.get().then((vm) => { if ( - !vm.setObjectSlotField(this.objectID.peek(), this.slotIndex, field, val, true) + !vm.setObjectSlotField(this.objectID, this.slotIndex, field, val, true) ) { - input.value = ( - this.slotSignal.value.logicFields ?? - new Map() - ) - .get(field) - .toString(); + input.value = (vm.state.getObjectSlotField(this.objectIDSignal.value, this.slotIndexSignal.value, field).value.value ?? null).toString(); } - this.updateObject(); }); } diff --git a/www/src/ts/virtualMachine/device/slotAddDialog.ts b/www/src/ts/virtualMachine/device/slotAddDialog.ts index 4650aef..10eebcd 100644 --- a/www/src/ts/virtualMachine/device/slotAddDialog.ts +++ b/www/src/ts/virtualMachine/device/slotAddDialog.ts @@ -1,27 +1,25 @@ import { html, css, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; +import { customElement, query } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { ComputedObjectSignals, globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; import { VMDeviceCard } from "./card"; -import { when } from "lit/directives/when.js"; import uFuzzy from "@leeoniya/ufuzzy"; import { FrozenObject, ItemInfo, - LogicField, - LogicSlotType, ObjectInfo, ObjectTemplate, } from "ic10emu_wasm"; import { computed, ReadonlySignal, signal, Signal, watch } from "@lit-labs/preact-signals"; import { repeat } from "lit/directives/repeat.js"; +import { isSome, structuralEqual } from "utils"; type SlotableItemTemplate = Extract; @customElement("vm-slot-add-dialog") -export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { +export class VMSlotAddDialog extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -40,11 +38,6 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { `, ]; - private _items: Signal> = signal({}); - private _filteredItems: ReadonlySignal; - private _datapoints: ReadonlySignal<[string, string][]>; - private _haystack: ReadonlySignal; - private _filter: Signal = signal(""); get filter() { return this._filter.peek(); @@ -54,75 +47,104 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { this._filter.value = val; } - private _searchResults: ReadonlySignal<{ - entry: SlotableItemTemplate; - haystackEntry: string; - ranges: number[]; - }[]>; + templateDB = computed(() => { + return this.vm.value?.state.templateDB.value ?? null; + }); - constructor() { - super(); - this.setupSearch(); - } + items = (() => { + let last: { [k: string]: SlotableItemTemplate } = null; + return computed(() => { + const next = Object.fromEntries( + Array.from(Object.values(this.templateDB.value ?? {})).flatMap((template) => { + if ("item" in template) { + return [[template.prefab.prefab_name, template]] as [ + string, + SlotableItemTemplate, + ][]; + } else { + return [] as [string, SlotableItemTemplate][]; + } + }), + ); + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); - postDBSetUpdate(): void { - this._items.value = Object.fromEntries( - Array.from(Object.values(this.templateDB)).flatMap((template) => { - if ("item" in template) { - return [[template.prefab.prefab_name, template]] as [ - string, - SlotableItemTemplate, - ][]; - } else { - return [] as [string, SlotableItemTemplate][]; - } - }), - ); - } - - setupSearch() { - const filteredItems = computed(() => { - let filtered = Array.from(Object.values(this._items.value)); - const obj = globalObjectSignalMap.get(this.objectID.value ?? null); - if (obj != null) { + filteredItems = (() => { + let last: SlotableItemTemplate[] = null; + return computed(() => { + let filtered = Array.from(Object.values(this.items.value)); + const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value; + if (isSome(obj)) { const template = obj.template; - const slot = "slots" in template.value ? template.value.slots[this.slotIndex.value] : null; + const slot = "slots" in template ? template.slots[this.slotIndex.value] : null; const typ = slot.typ; if (typeof typ === "string" && typ !== "None") { - filtered = Array.from(Object.values(this._items.value)).filter( + filtered = Array.from(Object.values(this.items.value)).filter( (item) => item.item.slot_class === typ, ); } } + if (structuralEqual(last, filtered)) { + return last; + } + last = filtered; return filtered; }); - this._filteredItems = filteredItems; + })(); - const datapoints = computed(() => { + datapoints = (() => { + let last: [string, string][] = null; + return computed(() => { const datapoints: [string, string][] = []; - for (const entry of this._filteredItems.value) { + for (const entry of this.filteredItems.value) { datapoints.push( [entry.prefab.name, entry.prefab.prefab_name], [entry.prefab.prefab_name, entry.prefab.prefab_name], [entry.prefab.desc, entry.prefab.prefab_name], ); } + if (structuralEqual(last, datapoints)) { + return last; + } + last = datapoints; return datapoints; }); - this._datapoints = datapoints; + })(); - const haystack: Signal = computed(() => { - return datapoints.value.map((data) => data[0]); + haystack = (() => { + let last: string[] = null; + return computed(() => { + const hay = this.datapoints.value.map(data => data[0]) + if (structuralEqual(last, hay)) { + return last; + } + last = hay; + return hay }); - this._haystack = haystack; + })(); - const searchResults = computed(() => { + searchResults: ReadonlySignal<{ + entry: SlotableItemTemplate + haystackEntry: string, + ranges: number[] + }[]> = (() => { + let last: { + entry: SlotableItemTemplate + haystackEntry: string, + ranges: number[] + }[] = null; + return computed(() => { let results; if (this._filter.value) { const uf = new uFuzzy({}); const [_idxs, info, order] = uf.search( - this._haystack.value, + this.haystack.value, this._filter.value, 0, 1e3, @@ -130,8 +152,8 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { const filtered = order?.map((infoIdx) => ({ - name: this._datapoints.value[info.idx[infoIdx]][1], - haystackEntry: this._haystack.value[info.idx[infoIdx]], + name: this.datapoints.value[info.idx[infoIdx]][1], + haystackEntry: this.haystack.value[info.idx[infoIdx]], ranges: info.ranges[infoIdx], })) ?? []; @@ -141,22 +163,25 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { }); results = unique.map(({ name, haystackEntry, ranges }) => ({ - entry: this._items.value[name]!, + entry: this.items.value[name]!, haystackEntry, ranges, })); } else { // return everything - results = [...this._filteredItems.value].map((st) => ({ + results = [...this.filteredItems.value].map((st) => ({ entry: st, haystackEntry: st.prefab.prefab_name, ranges: [], })); } + if (structuralEqual(last, results)) { + return last; + } + last = results return results; }); - this._searchResults = searchResults; - } + })(); renderSearchResults() { const enableNone = false; @@ -170,7 +195,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { `; const resultsHtml = computed(() => { return repeat( - this._searchResults.value, + this.searchResults.value, (result) => { return result.entry.prefab.prefab_hash; }, @@ -205,15 +230,15 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { } _handleClickNone() { - window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex.peek()); + window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex.peek()); this.hide(); } _handleClickItem(e: Event) { const div = e.currentTarget as HTMLDivElement; const key = parseInt(div.getAttribute("key")); - const entry = this.templateDB.get(key) as SlotableItemTemplate; - const obj = window.VM.vm.objects.get(this.objectID.peek()); + const entry = this.templateDB.value.get(key) as SlotableItemTemplate; + const obj = window.VM.vm.state.getObject(this.objectID); const dbTemplate = obj.peek().template; console.log("using entry", dbTemplate); @@ -224,7 +249,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { database_template: true, template: undefined, }; - window.VM.vm.setSlotOccupant(this.objectID.peek(), this.slotIndex.peek(), template, 1); + window.VM.vm.setSlotOccupant(this.objectID, this.slotIndex.peek(), template, 1); this.hide(); } @@ -232,14 +257,10 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { @query(".device-search-input") searchInput: SlInput; render() { - const device = computed(() => { - return globalObjectSignalMap.get(this.objectID.value) ?? null; + const name = computed(() => { + return this.vm.value?.state.getObjectDisplayName(this.objectIDSignal.value) ?? ""; }); - const name = computed(() => { - return device.value?.displayName.value ?? nothing; - - }); - const id = computed(() => this.objectID.value ?? 0); + const id = computed(() => this.objectIDSignal.value ?? 0); const resultsHtml = html`
${this.renderSearchResults()} @@ -284,11 +305,10 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) { this.slotIndex = undefined; } - private objectID: Signal = signal(null); private slotIndex: Signal = signal(0); show(objectID: number, slotIndex: number) { - this.objectID.value = objectID; + this.objectIDSignal.value = objectID; this.slotIndex.value = slotIndex; this.dialog.show(); this.searchInput.select(); diff --git a/www/src/ts/virtualMachine/device/template.ts b/www/src/ts/virtualMachine/device/template.ts index 897af10..67840bc 100644 --- a/www/src/ts/virtualMachine/device/template.ts +++ b/www/src/ts/virtualMachine/device/template.ts @@ -20,12 +20,12 @@ import { customElement, property, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; import { connectionFromConnectionInfo } from "./dbutils"; -import { crc32, displayNumber, parseNumber } from "utils"; +import { crc32, displayNumber, parseNumber, structuralEqual } from "utils"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; import { VMDeviceCard } from "./card"; -import { globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice"; -import { computed, Signal, watch } from "@lit-labs/preact-signals"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; +import { computed, effect, signal, Signal, watch } from "@lit-labs/preact-signals"; import { createRef, ref, Ref } from "lit/directives/ref.js"; export interface SlotTemplate { @@ -43,7 +43,7 @@ export interface ConnectionCableNetwork { } @customElement("vm-device-template") -export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { +export class VmObjectTemplate extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -84,34 +84,40 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { objectName: Signal; connections: Signal; - constructor() { - super(); - this.templateDB = window.VM.vm.templateDB; - } - - private _prefabName: string; - private _prefabHash: number; + private prefabNameSignal = signal(null); + private prefabHashSignal = computed(() => crc32(this.prefabNameSignal.value)); get prefabName(): string { - return this._prefabName; + return this.prefabNameSignal.peek(); } get prefabHash(): number { - return this._prefabHash; + return this.prefabHashSignal.peek(); } @property({ type: String }) set prefabName(val: string) { - this._prefabName = val; - this._prefabHash = crc32(this._prefabName); - this.setupState(); + this.prefabNameSignal.value = val; } - get dbTemplate(): ObjectTemplate { - return this.templateDB.get(this._prefabHash); + dbTemplate = (() => { + let last: ObjectTemplate = null; + return computed(() => { + const next = this.vm.value?.state.templateDB.value.get(this.prefabHashSignal.value) ?? null; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + })(); + + constructor() { + super(); + this.dbTemplate.subscribe(() => this.setupState()) } setupState() { - const dbTemplate = this.dbTemplate; + const dbTemplate = this.dbTemplate.value; this.fields.value = Object.fromEntries( ( @@ -122,7 +128,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { ) as [LogicType, MemoryAccess][] ).map(([lt, access]) => { const value = - lt === "PrefabHash" ? this.dbTemplate.prefab.prefab_hash : 0.0; + lt === "PrefabHash" ? dbTemplate.prefab.prefab_hash : 0.0; return [lt, value]; }), ) as Record; @@ -187,7 +193,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { const val = parseNumber(input.value); this.fields.value = { ...this.fields.value, [field]: val}; if (field === "ReferenceId" && val !== 0) { - this.objectId.value = val; + this.objectIDSignal.value = val; } } @@ -213,11 +219,16 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { return html``; } + networkOptions = computed(() => { + const vm = this.vm.value; + return vm?.state.networkIds.value.map(net => html`Network ${net}`); + }); + renderNetworks() { const vm = window.VM.vm; - const vmNetworks = computed(() => { - return vm.networkIds.value.map((net) => html`Network ${net}`); - }); + this.networkOptions.subscribe((_) => { + this.forceSelectUpdate(this.networksSelectRef); + }) const connections = computed(() => { this.connections.value.map((connection, index, _conns) => { const conn = @@ -236,13 +247,12 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { ${ref(this.networksSelectRef)} > Connection:${index} - ${watch(vmNetworks)} + ${watch(this.networkOptions)} ${conn?.typ} `; }); }); - vmNetworks.subscribe((_) => { this.forceSelectUpdate(this.networksSelectRef)}) return html`
${watch(connections)} @@ -272,42 +282,50 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { this.forceSelectUpdate(...this._pinsSelectRefMap.values()); } - renderPins(): HTMLTemplateResult { - const networks = computed(() => { - return this.connections.value.flatMap((connection, index) => { - return typeof connection === "object" && "CableNetwork" in connection - ? [connection.CableNetwork.net] - : []; - }); + networks = computed(() => { + return this.connections.value.flatMap(connection => { + return typeof connection === "object" && "CableNetwork" in connection + ? [connection.CableNetwork.net] + : []; }); - const visibleDeviceIds = computed(() => { - return [ - ...new Set( - networks.value.flatMap((net) => window.VM.vm.networkDataDevicesSignal(net).value), - ), - ]; + }); + visibleDeviceIds = (() => { + let last: ObjectID[] = null; + return computed(() => { + const vm = this.vm.value; + const next = [ + ...new Set( + this.networks.value.flatMap((net) => vm?.state.getNetwork(net).value.devices ?? []), + ), + ]; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; }); - const visibleDevices = computed(() => { - return visibleDeviceIds.value.map((id) => - globalObjectSignalMap.get(id), - ); - }); - const visibleDevicesHtml = computed(() => { - return visibleDevices.value.map( - (device, _index) => { - device.id.subscribe((_) => { this.forcePinSelectUpdate(); }); - device.displayName.subscribe((_) => { this.forcePinSelectUpdate(); }); - return html` - - Device ${watch(device.id)} : - ${watch(device.displayName)} - - ` - } - ) - }); - visibleDeviceIds.subscribe((_) => { this.forcePinSelectUpdate(); }); + })(); + + visibleDeviceOptions = computed(() => { + return this.visibleDeviceIds.value.map( + id => { + const displayName = computed(() => { + this.vm.value?.state.getObjectDisplayName(id).value; + }); + displayName.subscribe((_) => { this.forcePinSelectUpdate(); }); + return html` + + Device ${id} : + ${watch(displayName)} + + ` + } + ) + }); + + renderPins(): HTMLTemplateResult { + this.visibleDeviceOptions.subscribe((_) => { this.forcePinSelectUpdate(); }); const pinsHtml = computed(() => { this.pins.value.map( (pin, index) => { @@ -322,7 +340,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { ${ref(pinRef)} > d${index} - ${watch(visibleDevicesHtml)} + ${watch(this.visibleDeviceOptions)} ` } ); @@ -341,20 +359,22 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) { render() { const device = this.dbTemplate; + const prefabName = computed(() => device.value.prefab.prefab_name); + const name = computed(() => device.value.prefab.name); return html`
- +
- ${device.prefab.name} - ${device?.prefab.prefab_name} - ${device?.prefab.prefab_hash} + ${watch(name)} + ${watch(prefabName)} + ${watch(prefabName)}
; @@ -50,17 +52,8 @@ const jsonErrorRegex = /((invalid type: .*)|(missing field .*)) at line (?() { ic10vm: Comlink.Remote; templateDBPromise: Promise; - templateDB: TemplateDatabase; - private _vmState: Signal = signal(null); - - private _objects: Map>; - private _objectIds: Signal; - private _circuitHolders: Map>; - private _circuitHolderIds: Signal; - private _networks: Map>; - private _networkIds: Signal; - private _default_network: Signal; + state: VMState = new VMState(); private vm_worker: Worker; @@ -69,15 +62,6 @@ class VirtualMachine extends TypedEventTarget() { constructor(app: App) { super(); this.app = app; - - this._objects = new Map(); - this._objectIds = signal([]); - this._circuitHolders = new Map(); - this._circuitHolderIds = signal([]); - this._networks = new Map(); - this._networkIds = signal([]); - this._networkDevicesSignals = new Map(); - this.setupVM(); } @@ -90,207 +74,30 @@ class VirtualMachine extends TypedEventTarget() { console.info("VM Worker loaded"); const vm = Comlink.wrap(this.vm_worker); this.ic10vm = vm; - this._vmState.value = await this.ic10vm.saveVMState(); - window.VM.set(this); - + this.state.vm.value = await this.ic10vm.saveVMState(); this.templateDBPromise = this.ic10vm.getTemplateDatabase(); this.templateDBPromise.then((db) => this.setupTemplateDatabase(db)); - - effect(() => { - this.updateObjects(this._vmState.value); - this.updateNetworks(this._vmState.value); - }); - this.updateCode(); - } - get state() { - return this._vmState; - } - - get objects() { - return this._objects; - } - - get objectIds(): Signal { - return this._objectIds; - } - - get circuitHolders() { - return this._circuitHolders; - } - - get circuitHolderIds(): Signal { - return this._circuitHolderIds; - } - - get networks() { - return this._networks; - } - - get networkIds(): Signal { - return this._networkIds; - } - - get defaultNetwork() { - return this._default_network; + window.VM.set(this); } get activeIC() { - return this._circuitHolders.get(this.app.session.activeIC); + return computed(() => this.app.session.activeIC.value); } - async visibleDevices(source: number): Promise[]> { - try { - const visDevices = await this.ic10vm.visibleDevices(source); - const ids = Array.from(visDevices); - ids.sort(); - return ids.map((id, _index) => this._objects.get(id)!); - } catch (err) { - this.handleVmError(err); - } - } - - async visibleDeviceIds(source: number): Promise { + async visibleDeviceIds(source: ObjectID): Promise { const visDevices = await this.ic10vm.visibleDevices(source); const ids = Array.from(visDevices); ids.sort(); return ids; } - async updateNetworks(state: FrozenVM) { - let updateFlag = false; - const removedNetworks = []; - const networkIds: ObjectID[] = []; - const frozenNetworks: FrozenCableNetwork[] = state.networks; - const updatedNetworks: ObjectID[] = []; - - for (const [index, net] of frozenNetworks.entries()) { - const id = net.id; - networkIds.push(id); - if (!this._networks.has(id)) { - this._networks.set(id, signal(net)); - updateFlag = true; - updatedNetworks.push(id); - } else { - const mappedNet = this._networks.get(id); - if (!structuralEqual(mappedNet.peek(), net)) { - mappedNet.value = net; - updatedNetworks.push(id); - updateFlag = true; - } - } - } - - for (const id of this._networks.keys()) { - if (!networkIds.includes(id)) { - this._networks.delete(id); - updateFlag = true; - removedNetworks.push(id); - } - } - - if (updateFlag) { - const ids = Array.from(updatedNetworks); - ids.sort(); - this.dispatchCustomEvent("vm-networks-update", ids); - if (removedNetworks.length > 0) { - this.dispatchCustomEvent("vm-networks-removed", removedNetworks); - } - this.app.session.save(); - } - - networkIds.sort(); - this._networkIds.value = networkIds; - } - - async updateObjects(state: FrozenVM) { - const removedObjects = []; - const frozenObjects = state.objects; - const objectIds: ObjectID[] = []; - const updatedObjects: ObjectID[] = []; - let updateFlag = false; - - for (const [index, obj] of frozenObjects.entries()) { - const id = obj.obj_info.id; - objectIds.push(id); - if (!this._objects.has(id)) { - this._objects.set(id, signal(obj)); - updateFlag = true; - updatedObjects.push(id); - } else { - const mappedObject = this._objects.get(id); - if (!structuralEqual(obj, mappedObject.peek())) { - mappedObject.value = obj; - updatedObjects.push(id); - updateFlag = true; - } - } - } - - for (const id of this._objects.keys()) { - if (!objectIds.includes(id)) { - this._objects.delete(id); - updateFlag = true; - removedObjects.push(id); - } - } - - for (const [id, obj] of this._objects) { - if (typeof obj.peek().obj_info.socketed_ic !== "undefined") { - if (!this._circuitHolders.has(id)) { - this._circuitHolders.set(id, obj); - updateFlag = true; - if (!updatedObjects.includes(id)) { - updatedObjects.push(id); - } - } - } else { - if (this._circuitHolders.has(id)) { - updateFlag = true; - if (!updatedObjects.includes(id)) { - updatedObjects.push(id); - } - this._circuitHolders.delete(id); - } - } - } - - for (const id of this._circuitHolders.keys()) { - if (!this._objects.has(id)) { - this._circuitHolders.delete(id); - updateFlag = true; - if (!removedObjects.includes(id)) { - removedObjects.push(id); - } - } - } - - if (updateFlag) { - const ids = Array.from(updatedObjects); - ids.sort(); - this.dispatchCustomEvent("vm-objects-update", ids); - if (removedObjects.length > 0) { - this.dispatchCustomEvent("vm-objects-removed", removedObjects); - } - this.app.session.save(); - } - - objectIds.sort(); - const circuitHolderIds = Array.from(this._circuitHolders.keys()); - circuitHolderIds.sort(); - - batch(() => { - this._objectIds.value = objectIds; - this._circuitHolderIds.value = circuitHolderIds; - }); - } - async updateCode() { - const progs = this.app.session.programs; + const progs = this.app.session.programs.peek(); for (const id of progs.keys()) { const attempt = Date.now().toString(16); - const circuitHolder = this._circuitHolders.get(id); + const circuitHolder = this.state.getObject(id); const prog = progs.get(id); if ( circuitHolder && @@ -314,42 +121,43 @@ class VirtualMachine extends TypedEventTarget() { } async step() { - const ic = this.activeIC; + const ic = this.activeIC.peek(); if (ic) { try { - await this.ic10vm.stepProgrammable(ic.peek().obj_info.id, false); + await this.ic10vm.stepProgrammable(ic, false); } catch (err) { this.handleVmError(err); } this.update(); - this.dispatchCustomEvent("vm-run-ic", this.activeIC!.peek().obj_info.id); + this.dispatchCustomEvent("vm-run-ic", ic); } } async run() { - const ic = this.activeIC; + const ic = this.activeIC.peek(); if (ic) { try { - await this.ic10vm.runProgrammable(ic.peek().obj_info.id, false); + await this.ic10vm.runProgrammable(ic, false); } catch (err) { this.handleVmError(err); } this.update(); - this.dispatchCustomEvent("vm-run-ic", this.activeIC!.peek().obj_info.id); + this.dispatchCustomEvent("vm-run-ic", this.activeIC.peek()); } } async reset() { - const ic = this.activeIC; + const ic = this.activeIC.peek(); if (ic) { - await this.ic10vm.resetProgrammable(ic.peek().obj_info.id); + await this.ic10vm.resetProgrammable(ic); await this.update(); } } async update(save: boolean = true) { try { - this._vmState.value = await this.ic10vm.saveVMState(); + const newState = await this.ic10vm.saveVMState(); + this.state.vm.value = newState; if (save) this.app.session.save(); } catch (err) { this.handleVmError(err); @@ -386,46 +194,30 @@ class VirtualMachine extends TypedEventTarget() { this.dispatchCustomEvent("vm-message", toastMessage); } - // return the data connected oject ids for a network - networkDataDevices(network: ObjectID): ObjectID[] { - return this._networks.get(network)?.peek().devices ?? []; - } - - private _networkDevicesSignals: Map>; - - networkDataDevicesSignal(network: ObjectID): Signal { - if (!this._networkDevicesSignals.has(network) && this._networks.get(network) != null) { - this._networkDevicesSignals.set(network, computed( - () => this._networks.get(network).value.devices ?? [] - )); - } - return this._networkDevicesSignals.get(network); - } - async changeObjectID(oldID: number, newID: number): Promise { try { await this.ic10vm.changeDeviceId(oldID, newID); - if (this.app.session.activeIC === oldID) { - this.app.session.activeIC = newID; - } - await this.update(); - this.dispatchCustomEvent("vm-object-id-change", { - old: oldID, - new: newID, - }); - this.app.session.changeID(oldID, newID); - return true; } catch (err) { this.handleVmError(err); return false; } + if (this.app.session.activeIC.peek() === oldID) { + this.app.session.activeIC = newID; + } + await this.update(); + this.dispatchCustomEvent("vm-object-id-change", { + old: oldID, + new: newID, + }); + this.app.session.changeID(oldID, newID); + return true; } async setRegister(index: number, val: number): Promise { - const ic = this.activeIC!; + const ic = this.activeIC.peek(); if (ic) { try { - await this.ic10vm.setRegister(ic.peek().obj_info.id, index, val); + await this.ic10vm.setRegister(ic, index, val); } catch (err) { this.handleVmError(err); return false; @@ -436,10 +228,10 @@ class VirtualMachine extends TypedEventTarget() { } async setStack(addr: number, val: number): Promise { - const ic = this.activeIC!; + const ic = this.activeIC.peek(); if (ic) { try { - await this.ic10vm.setMemory(ic.peek().obj_info.id, addr, val); + await this.ic10vm.setMemory(ic, addr, val); } catch (err) { this.handleVmError(err); return false; @@ -450,18 +242,14 @@ class VirtualMachine extends TypedEventTarget() { } async setObjectName(id: number, name: string): Promise { - const obj = this._objects.get(id); - if (obj) { - try { - await this.ic10vm.setObjectName(obj.peek().obj_info.id, name); - } catch (e) { - this.handleVmError(e); - return false; - } - await this.update(); - return true; + try { + await this.ic10vm.setObjectName(id, name); + } catch (e) { + this.handleVmError(e); + return false; } - return false; + await this.update(); + return true; } async setObjectField( @@ -471,18 +259,14 @@ class VirtualMachine extends TypedEventTarget() { force?: boolean, ): Promise { force = force ?? false; - const obj = this._objects.get(id); - if (obj) { - try { - await this.ic10vm.setLogicField(obj.peek().obj_info.id, field, val, force); - } catch (err) { - this.handleVmError(err); - return false; - } - await this.update(); - return true; + try { + await this.ic10vm.setLogicField(id, field, val, force); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } async setObjectSlotField( @@ -493,24 +277,20 @@ class VirtualMachine extends TypedEventTarget() { force?: boolean, ): Promise { force = force ?? false; - const obj = this._objects.get(id); - if (obj) { - try { - await this.ic10vm.setSlotLogicField( - obj.peek().obj_info.id, - field, - slot, - val, - force, - ); - } catch (err) { - this.handleVmError(err); - return false; - } - await this.update(); - return true; + try { + await this.ic10vm.setSlotLogicField( + id, + field, + slot, + val, + force, + ); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } async setDeviceConnection( @@ -518,18 +298,14 @@ class VirtualMachine extends TypedEventTarget() { conn: number, val: number | undefined, ): Promise { - const device = this._objects.get(id); - if (typeof device !== "undefined") { - try { - await this.ic10vm.setDeviceConnection(id, conn, val); - } catch (err) { - this.handleVmError(err); - return false; - } - await this.update(); - return true; + try { + await this.ic10vm.setDeviceConnection(id, conn, val); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } async setDevicePin( @@ -537,50 +313,48 @@ class VirtualMachine extends TypedEventTarget() { pin: number, val: number | undefined, ): Promise { - const device = this._objects.get(id); - if (typeof device !== "undefined") { - try { - await this.ic10vm.setPin(id, pin, val); - } catch (err) { - this.handleVmError(err); - return false; - } - await this.update(); - return true; + try { + await this.ic10vm.setPin(id, pin, val); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } setupTemplateDatabase(db: TemplateDatabase) { - this.templateDB = db; - console.log("Loaded Template Database", this.templateDB); - this.dispatchCustomEvent("vm-template-db-loaded", this.templateDB); + this.state.templateDB.value = db; + console.log("Loaded Template Database", this.state.templateDB.value); + this.dispatchCustomEvent("vm-template-db-loaded", this.state.templateDB.value); } async addObjectFrozen(frozen: FrozenObject): Promise { + let id = undefined; try { console.log("adding device", frozen); - const id = await this.ic10vm.addObjectFrozen(frozen); - await this.update(); - return id; + id = await this.ic10vm.addObjectFrozen(frozen); } catch (err) { this.handleVmError(err); return undefined; } + await this.update(); + return id; } async addObjectsFrozen( frozenObjects: FrozenObject[], ): Promise { + let ids = undefined; try { console.log("adding devices", frozenObjects); - const ids = await this.ic10vm.addObjectsFrozen(frozenObjects); - await this.update(); - return Array.from(ids); + ids = await this.ic10vm.addObjectsFrozen(frozenObjects); } catch (err) { this.handleVmError(err); return undefined; } + await this.update(); + return Array.from(ids ?? []); } async removeDevice(id: number): Promise { @@ -600,32 +374,26 @@ class VirtualMachine extends TypedEventTarget() { frozen: FrozenObject, quantity: number, ): Promise { - const device = this._objects.get(id); - if (typeof device !== "undefined") { - try { - console.log("setting slot occupant", frozen); - await this.ic10vm.setSlotOccupant(id, index, frozen, quantity); - await this.update(); - return true; - } catch (err) { - this.handleVmError(err); - } + try { + console.log("setting slot occupant", frozen); + await this.ic10vm.setSlotOccupant(id, index, frozen, quantity); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } async removeSlotOccupant(id: number, index: number): Promise { - const device = this._objects.get(id); - if (typeof device !== "undefined") { - try { - await this.ic10vm.removeSlotOccupant(id, index); - await this.update(); - return true; - } catch (err) { - this.handleVmError(err); - } + try { + await this.ic10vm.removeSlotOccupant(id, index); + } catch (err) { + this.handleVmError(err); + return false; } - return false; + await this.update(); + return true; } async saveVMState(): Promise { @@ -636,18 +404,16 @@ class VirtualMachine extends TypedEventTarget() { try { console.info("Restoring VM State from", state); await this.ic10vm.restoreVMState(state); - this._objects = new Map(); - this._circuitHolders = new Map(); - await this.update(); } catch (e) { - this.handleVmError(e, {jsonContext: JSON.stringify(state)}); + this.handleVmError(e, { jsonContext: JSON.stringify(state) }); + return; } + // TODO: Cleanup old state + await this.update(); } getPrograms(): [number, string][] { - const programs: [number, string][] = Array.from( - this._circuitHolders.entries(), - ).map(([id, ic]) => [id, ic.peek().obj_info.source_code]); + const programs: [number, string][] = this.state.circuitHolderIds.value.map((id) => [id, this.state.getObjectProgramSource(id).value]); return programs; } } diff --git a/www/src/ts/virtualMachine/registers.ts b/www/src/ts/virtualMachine/registers.ts index e664547..54acd23 100644 --- a/www/src/ts/virtualMachine/registers.ts +++ b/www/src/ts/virtualMachine/registers.ts @@ -1,15 +1,14 @@ import { html, css, nothing } from "lit"; import { customElement } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { VMActiveICMixin } from "virtualMachine/baseDevice"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; -import { RegisterSpec } from "ic10emu_wasm"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { displayNumber, parseNumber } from "utils"; -import { computed, Signal, watch } from "@lit-labs/preact-signals"; +import { displayNumber, parseNumber, range, structuralEqual } from "utils"; +import { computed, ReadonlySignal, Signal, watch } from "@lit-labs/preact-signals"; @customElement("vm-ic-registers") -export class VMICRegisters extends VMActiveICMixin(BaseElement) { +export class VMICRegisters extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -39,38 +38,56 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) { ["ra", 17], ]; - constructor() { - super(); - this.subscribe("active-ic") + + circuit = computed(() => { + return this.vm.value?.state.getCircuitInfo(this.vm.value?.activeIC.value).value; + }); + + registerCount = computed(() => { + return this.vm.value?.state.getCircuitRegistersCount(this.vm.value.activeIC.value).value; + }) + + registerAliases = (() => { + let last: [string, number][] = null; + return computed(() => { + const aliases = this.vm.value?.state.getCircuitAliases(this.vm.value?.activeIC.value).value + const forRegisters = [...(Object.entries(aliases ?? {}) ?? [])].flatMap(([alias, target]): [string, number][] => { + if ("RegisterSpec" in target && target.RegisterSpec.indirection === 0) { + return [[alias, target.RegisterSpec.target]]; + } + return []; + }).concat(VMICRegisters.defaultAliases); + if (structuralEqual(last, forRegisters)) { + return last; + } + last = forRegisters; + return forRegisters; + }); + })(); + + aliasesFor(index: number): ReadonlySignal { + return computed(() => { + return this.registerAliases.value?.flatMap(([alias, target]): string[] => target === index ? [alias] : []) + }); + } + + registerAt(index: number): ReadonlySignal { + return computed(() => { + return this.vm.value?.state.getCircuitRegistersAt(this.vm.value?.activeIC.value, index).value + }) } protected render() { - const registerAliases: Signal<[string, number][]> = computed(() => { - return [...(Array.from(this.objectSignals.aliases.value?.entries() ?? []))].flatMap( - ([alias, target]) => { - if ("RegisterSpec" in target && target.RegisterSpec.indirection === 0) { - return [[alias, target.RegisterSpec.target]] as [string, number][]; - } else { - return [] as [string, number][]; - } - } - ).concat(VMICRegisters.defaultAliases); - }); - const registerHtml = this.objectSignals?.registers.peek().map((val, index) => { - const aliases = computed(() => { - return registerAliases.value - .filter(([_alias, target]) => index === target) - .map(([alias, _target]) => alias); - }); + const registerHtml = computed(() => range(this.registerCount.value).map(index => { const aliasesList = computed(() => { - return aliases.value.join(", "); - }); + return this.aliasesFor(index).value?.join(", ") ?? nothing; + }) const aliasesText = computed(() => { - return aliasesList.value || "None"; + return this.aliasesFor(index).value?.join(", ") ?? "None"; }); const valDisplay = computed(() => { - const val = this.objectSignals.registers.value[index]; + const val = this.registerAt(index).value; return displayNumber(val); }); return html` @@ -92,12 +109,12 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
`; - }) ?? nothing; + }) ?? nothing); return html`
- ${registerHtml} + ${watch(registerHtml)}
`; diff --git a/www/src/ts/virtualMachine/stack.ts b/www/src/ts/virtualMachine/stack.ts index 6dd6e15..ade1418 100644 --- a/www/src/ts/virtualMachine/stack.ts +++ b/www/src/ts/virtualMachine/stack.ts @@ -1,14 +1,14 @@ import { html, css, nothing } from "lit"; import { customElement } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { VMActiveICMixin } from "virtualMachine/baseDevice"; +import { VMObjectMixin } from "virtualMachine/baseDevice"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { displayNumber, parseNumber } from "utils"; +import { displayNumber, parseNumber, range } from "utils"; import { computed, watch } from "@lit-labs/preact-signals"; @customElement("vm-ic-stack") -export class VMICStack extends VMActiveICMixin(BaseElement) { +export class VMICStack extends VMObjectMixin(BaseElement) { static styles = [ ...defaultCss, css` @@ -36,25 +36,38 @@ export class VMICStack extends VMActiveICMixin(BaseElement) { `, ]; - constructor() { - super(); - this.subscribe("active-ic") + circuit = computed(() => { + return this.vm.value?.state.getCircuitInfo(this.vm.value?.activeIC.value).value; + }); + + sp = computed(() => { + return this.circuit.value?.registers[16] ?? 0; + }); + + socketedIc = computed(() => { + return this.vm.value?.state.getObject(this.vm.value?.activeIC.value).value?.obj_info.socketed_ic ?? null; + }) + + memorySize = computed(() => { + return this.vm.value?.state.getObjectMemorySize(this.socketedIc.value).value; + }); + + memoryAt(index: number) { + return computed(() => { + return this.vm.value?.state.getObjectMemoryAt(this.socketedIc.value, index).value + }); } protected render() { - const sp = computed(() => { - return this.objectSignals.registers.value != null ? this.objectSignals.registers.value[16] : 0; - }); - - const memoryHtml = this.objectSignals?.memory.peek()?.map((val, index) => { + const memoryHtml = computed(() => range(this.memorySize.value).map(index => { const content = computed(() => { - return sp.value === index ? html`Stack Pointer` : nothing; + return this.sp.value === index ? html`Stack Pointer` : nothing; }); const pointerClass = computed(() => { - return sp.value === index ? "stack-pointer" : nothing; + return this.sp.value === index ? "stack-pointer" : nothing; }); const displayVal = computed(() => { - return displayNumber(this.objectSignals.memory.value[index]); + return displayNumber(this.memoryAt(index).value); }); return html` @@ -75,12 +88,12 @@ export class VMICStack extends VMActiveICMixin(BaseElement) { `; - }) ?? nothing; + })); return html`
- ${memoryHtml} + ${watch(memoryHtml)}
`; diff --git a/www/src/ts/virtualMachine/state.ts b/www/src/ts/virtualMachine/state.ts new file mode 100644 index 0000000..e60fe2f --- /dev/null +++ b/www/src/ts/virtualMachine/state.ts @@ -0,0 +1,600 @@ +import { computed, ReadonlySignal, signal, Signal } from "@lit-labs/preact-signals"; +import { Obj } from "@popperjs/core"; +import { Class, Connection, FrozenCableNetwork, FrozenNetworks, FrozenObject, FrozenObjectFull, FrozenVM, ICInfo, LogicField, LogicSlotType, LogicType, ObjectID, Operand, Slot, TemplateDatabase } from "ic10emu_wasm"; +import { fromJson, isSome, structuralEqual } from "utils"; + + +export interface ObjectSlotInfo { + parent: ObjectID; + index: number; + name: string; + typ: Class; + quantity: number; + occupant: ObjectID; +} + +export class VMState { + + vm: Signal = signal(null); + templateDB: Signal = signal(null); + + objectIds: ReadonlySignal = computed(() => this.vm.value?.objects.map((obj) => obj.obj_info.id) ?? []); + circuitHolderIds: ReadonlySignal = computed(() => this.vm.value?.circuit_holders ?? []); + programHolderIds: ReadonlySignal = computed(() => this.vm.value?.program_holders ?? []); + networkIds: ReadonlySignal = computed(() => this.vm.value?.networks.map((net) => net.id) ?? []); + wirelessTransmitterIds: ReadonlySignal = computed(() => this.vm.value?.wireless_transmitters ?? []); + wirelessReceivers: ReadonlySignal = computed(() => this.vm.value?.wireless_receivers ?? []); + defaultNetworkId: ReadonlySignal = computed(() => this.vm.value?.default_network_key ?? null); + + private _signalCache: Map>> = new Map(); + private _signalRegistry = new FinalizationRegistry((key: string) => { + const s = this._signalCache.get(key); + if (s && !s.deref()) this._signalCache.delete(key); + }); + + signalCacheHas(key: string): boolean { + return this._signalCache.has(key) && typeof this._signalCache.get(key).deref() !== undefined + } + signalCacheGet(key: string): any { + return this._signalCache.get(key).deref(); + } + signalCacheSet(key: string, s: ReadonlySignal) { + this._signalCache.set(key, new WeakRef(s)); + this._signalRegistry.register(s, key); + } + + getObject(id: ObjectID): ReadonlySignal { + const key = `obj:${id}`; + if (!this.signalCacheHas(key)) { + let last: FrozenObject = null; + const s = computed(() => { + const obj = this.vm.value?.objects.find((o) => o.obj_info.id === id) ?? null; + if (obj?.database_template ?? false) { + return { ...obj, template: this.templateDB.value?.get(obj.obj_info.prefab_hash) } + } + if (structuralEqual(last, obj)) { + return last; + } + last = obj; + return obj; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectName(id: ObjectID): ReadonlySignal { + const key = `obj:${id},name`; + if (!this.signalCacheHas(key)) { + const s = computed(() => { + const obj = this.getObject(id); + return obj.value?.obj_info.name ?? ""; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectPrefabName(id: ObjectID): ReadonlySignal { + const key = `obj:${id},prefabName`; + if (!this.signalCacheHas(key)) { + const s = computed(() => { + const obj = this.getObject(id); + return obj.value?.obj_info.prefab ?? ""; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectDisplayName(id: ObjectID): ReadonlySignal { + const key = `obj:${id},DisplayName`; + if (!this.signalCacheHas(key)) { + const s = computed(() => { + const obj = this.getObject(id).value; + return obj?.obj_info.name ?? obj?.obj_info.prefab ?? ""; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getNetwork(id: ObjectID): Signal { + const key = `network:${id}`; + if (!this.signalCacheHas(key)) { + let last: FrozenCableNetwork = null + const s = computed(() => { + const next = this.vm.value?.networks.find((n) => n.id === id) ?? null + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectFieldNames(id: ObjectID): ReadonlySignal { + const key = `obj:${id},fieldNames`; + if (!this.signalCacheHas(key)) { + let last: LogicType[] = null; + const s = computed((): LogicType[] => { + const obj = this.getObject(id).value; + const template = obj?.template; + const logicAccess = isSome(template) && "logic" in template ? template.logic.logic_types : null; + const next = Array.from(logicAccess?.keys() ?? []) + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectField(id: ObjectID, field: LogicType): ReadonlySignal { + const key = `obj:${id},field:${field}`; + if (!this.signalCacheHas(key)) { + const s = computed((): LogicField => { + const obj = this.getObject(id).value; + const template = obj?.template; + const logicAccess = isSome(template) && "logic" in template ? template.logic.logic_types.get(field) : null; + const logicValue = obj?.obj_info.logic_values.get(field) ?? null; + return isSome(logicAccess) || isSome(logicValue) ? { + field_type: logicAccess, + value: logicValue, + } : null; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSlotCount(id: ObjectID): ReadonlySignal { + const key = `obj:${id},slotsCount`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + const obj = this.getObject(id).value; + const template = obj?.template; + return isSome(template) && "slots" in template ? template.slots.length : 0 + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSlotInfo(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},slot${index}`; + if (!this.signalCacheHas(key)) { + let last: ObjectSlotInfo = null; + const s = computed((): ObjectSlotInfo => { + const obj = this.getObject(id).value; + const info = obj?.obj_info.slots.get(index); + const template = obj?.template; + const slotTemplate = isSome(template) && "slots" in template ? template.slots[index] : null; + if (isSome(obj)) { + const next = { + parent: obj?.obj_info.id, + index, + name: slotTemplate?.name, + typ: slotTemplate?.typ, + quantity: info?.quantity, + occupant: info?.id + } + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + } + return null; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSlotFieldNames(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},slot:${index},fieldNames`; + if (!this.signalCacheHas(key)) { + let last: LogicSlotType[] = null; + const s = computed((): LogicSlotType[] => { + const obj = this.getObject(id).value; + const template = obj?.template; + let logicTemplate = null; + if (isSome(template) && ("logic" in template)) { + logicTemplate = template.logic.logic_slot_types.get(index.toString()); + } + const next = Array.from(logicTemplate?.keys() ?? []); + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSlotField(id: ObjectID, index: number, field: LogicSlotType): ReadonlySignal { + const key = `obj:${id},slot:${index},field:${field}`; + if (!this.signalCacheHas(key)) { + let last: LogicField = null + const s = computed((): LogicField => { + const obj = this.getObject(id).value; + const template = obj?.template; + const logicTemplate = isSome(template) && "logic" in template ? template.logic.logic_slot_types.get(index.toString()) : null; + const slotFieldValue = obj?.obj_info.slot_logic_values?.get(index)?.get(field) ?? null; + const slotFieldAccess = logicTemplate?.get(field) + const next = isSome(slotFieldValue) || isSome(slotFieldAccess) ? { + field_type: slotFieldAccess, + value: slotFieldValue + + } : null + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSlotOccupantId(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},slot:${index},occupant` + if (!this.signalCacheHas(key)) { + const s = computed((): ObjectID => { + const obj = this.getObject(id).value; + const info = obj?.obj_info.slots.get(index); + return info?.id ?? null; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectSocketedIcId(id: ObjectID): ReadonlySignal { + const key = `obj:${id},socketedIc` + if (!this.signalCacheHas(key)) { + const s = computed((): ObjectID => { + const obj = this.getObject(id).value; + return obj?.obj_info.socketed_ic ?? null; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectConnectionCount(id: ObjectID): ReadonlySignal { + const key = `obj:${id},connectionCount`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + const obj = this.getObject(id).value; + const template = obj?.template; + const connectionList = + isSome(template) && "device" in template + ? template.device.connection_list + : []; + return connectionList.length; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectConnections(id: ObjectID): ReadonlySignal { + const key = `obj:${id},connections` + if (!this.signalCacheHas(key)) { + let last: Connection[] = null; + const s = computed((): Connection[] => { + const obj = this.getObject(id).value; + const template = obj?.template; + const connectionsMap = obj?.obj_info.connections ?? null; + const connectionList = + isSome(template) && "device" in template + ? template.device.connection_list + : []; + const connections = connectionList.map((conn, index): Connection => { + if (conn.typ === "Data") { + return { + CableNetwork: { + typ: "Data", + role: conn.role, + net: connectionsMap.get(index), + }, + }; + } else if (conn.typ === "Power") { + return { + CableNetwork: { + typ: "Power", + role: conn.role, + net: connectionsMap.get(index), + }, + }; + } else if (conn.typ === "PowerAndData") { + return { + CableNetwork: { + typ: "Data", + role: conn.role, + net: connectionsMap.get(index), + }, + }; + } else if (conn.typ === "Pipe") { + return { Pipe: { role: conn.role } }; + } else if (conn.typ === "Chute") { + return { Chute: { role: conn.role } }; + } else if (conn.typ === "Elevator") { + return { Elevator: { role: conn.role } }; + } else if (conn.typ === "LaunchPad") { + return { LaunchPad: { role: conn.role } }; + } else if (conn.typ === "LandingPad") { + return { LandingPad: { role: conn.role } }; + } else if (conn.typ === "PipeLiquid") { + return { PipeLiquid: { role: conn.role } }; + } else if (conn.typ === "RoboticArmRail") { + return { RoboticArmRail: { role: conn.role } } + } + return "None"; + }); + if (structuralEqual(last, connections)) { + return last; + } + last = connections; + return connections; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectConnection(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},connection:${index}`; + if (!this.signalCacheHas(key)) { + let last: Connection = null; + const s = computed((): Connection => { + const connections = this.getObjectConnections(id).value ?? []; + const next = connections[index] ?? null; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key) + } + + getCircuitInfo(id: ObjectID): ReadonlySignal { + const key = `obj:${id},socketedIc` + if (!this.signalCacheHas(key)) { + let last: ICInfo = null; + const s = computed((): ICInfo => { + const obj = this.getObject(id).value; + if (!isSome(obj)) { + return null; + } + let circuitInfo: ICInfo = obj.obj_info.circuit; + if (!isSome(circuitInfo)) { + const icObj = this.getObject(obj.obj_info.socketed_ic).value; + if (!isSome(icObj)) { + return null; + } + circuitInfo = icObj.obj_info.circuit; + } + if (structuralEqual(last, circuitInfo)) { + return last; + } + last = circuitInfo; + return circuitInfo; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectMemorySize(id: ObjectID): ReadonlySignal { + const key = `obj:${id},memorySize`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + return this.getObject(id).value?.obj_info.memory?.length ?? null; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectMemory(id: ObjectID): ReadonlySignal { + const key = `obj:${id},memory`; + if (!this.signalCacheHas(key)) { + let last: number[] = null; + const s = computed((): number[] => { + const next = this.getObject(id).value?.obj_info.memory ?? null; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getObjectMemoryAt(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},memory:${index}`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + return (this.getObject(id).value?.obj_info.memory ?? [])[index] ?? null; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getCircuitRegistersCount(id: ObjectID): ReadonlySignal { + const key = `obj:${id},registersCount`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + return this.getCircuitInfo(id).value?.registers.length ?? null; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getCircuitRegisters(id: ObjectID): ReadonlySignal { + const key = `obj:${id},registers`; + if (!this.signalCacheHas(key)) { + let last: number[] = null; + const s = computed((): number[] => { + const next = this.getCircuitInfo(id).value?.registers ?? null; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getCircuitRegistersAt(id: ObjectID, index: number): ReadonlySignal { + const key = `obj:${id},register:${index}`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + return (this.getCircuitInfo(id).value?.registers ?? [])[index] ?? null; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getCircuitAliases(id: ObjectID): ReadonlySignal> { + const key = `obj:${id},circuitAliases`; + if (!this.signalCacheHas(key)) { + let last: Record = null; + const s = computed(() => { + const circuit = this.getCircuitInfo(id).value; + const aliases = circuit?.aliases; + const next = Object.fromEntries(aliases?.entries() ?? []) + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key) + } + + getDeviceNumPins(id: ObjectID): ReadonlySignal { + const key = `obj:${id},numPins`; + if (!this.signalCacheHas(key)) { + const s = computed((): number => { + const obj = this.getObject(id).value; + return [...obj?.obj_info.device_pins?.keys() ?? []].length; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getDevicePins(id: ObjectID): ReadonlySignal<[number, ObjectID][]> { + const key = `obj:${id},pins`; + if (!this.signalCacheHas(key)) { + let last: [number, ObjectID][] = null; + const s = computed((): [number, ObjectID][] => { + const obj = this.getObject(id).value; + const next = [...obj?.obj_info.device_pins?.entries() ?? []]; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getDevicePin(id: ObjectID, pin: number): ReadonlySignal { + const key = `obj:${id},pin:${id}`; + if (!this.signalCacheHas(key)) { + const s = computed((): ObjectID => { + const obj = this.getObject(id).value; + return obj?.obj_info.device_pins?.get(pin); + }); + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key); + } + + getNetworkDevices(id: ObjectID): ReadonlySignal { + const key = `network:${id},devices`; + if (!this.signalCacheHas(key)) { + let last: ObjectID[] = null; + const s = computed(() => { + const next = this.vm.value.networks.find((net) => net.id === id)?.devices ?? null; + if (structuralEqual(last, next)) { + return last; + } + last = next; + return next; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key) + } + + getObjectProgramSource(id: ObjectID): ReadonlySignal { + const key = `obj:${id},source`; + if (!this.signalCacheHas(key)) { + const s = computed(() => { + return this.getObject(id).value?.obj_info.source_code ?? null; + }) + this.signalCacheSet(key, s); + return s; + } + return this.signalCacheGet(key) + } + + +}