import { property, state } from "lit/decorators.js"; import type { Slot, Connection, ICError, LogicType, LogicField, Operand, ObjectID, TemplateDatabase, FrozenObjectFull, } from "ic10emu_wasm"; import { crc32, structuralEqual } from "utils"; import { LitElement, PropertyValueMap } from "lit"; type Constructor = new (...args: any[]) => T; export declare class VMObjectMixinInterface { objectID: ObjectID; activeICId: ObjectID; obj: FrozenObjectFull; name: string | null; nameHash: number | null; prefabName: string | null; prefabHash: number | null; fields: Map; slots: Slot[]; reagents: Map; connections: Connection[]; icIP: number; icOpCount: number; icState: string; errors: ICError[]; registers: number[] | null; memory: number[] | null; aliases: Map | null; defines: Map | null; numPins: number | null; pins: Map | null; _handleDeviceModified(e: CustomEvent): void; updateDevice(): void; updateIC(): void; subscribe(...sub: VMObjectMixinSubscription[]): void; unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean): void; } export type VMObjectMixinSubscription = | "name" | "nameHash" | "prefabName" | "fields" | "slots" | "slots-count" | "reagents" | "connections" | "ic" | "active-ic" | { field: LogicType } | { slot: number } | "visible-devices"; export const VMObjectMixin = >( superClass: T, ) => { class VMObjectMixinClass extends superClass { private _deviceID: number; get deviceID() { return this._deviceID; } @property({ type: Number }) set deviceID(val: number) { this._deviceID = val; this.updateDevice(); } @state() private objectSubscriptions: VMObjectMixinSubscription[] = []; subscribe(...sub: VMObjectMixinSubscription[]) { this.objectSubscriptions = this.objectSubscriptions.concat(sub); } // remove subscripotions matching the filter unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean) { this.objectSubscriptions = this.objectSubscriptions.filter( (sub) => !filter(sub), ); } obj: FrozenObjectFull; @state() activeICId: number; @state() name: string | null = null; @state() nameHash: number | null = null; @state() prefabName: string | null; @state() prefabHash: number | null; @state() fields: Map; @state() slots: Slot[]; @state() reagents: Map; @state() connections: Connection[]; @state() icIP: number; @state() icOpCount: number; @state() icState: string; @state() errors: ICError[]; @state() registers: number[] | null; @state() memory: number[] | null; @state() aliases: Map | null; @state() defines: Map | null; @state() numPins: number | null; @state() pins: Map | null; connectedCallback(): void { const root = super.connectedCallback(); window.VM.get().then((vm) => { vm.addEventListener( "vm-device-modified", this._handleDeviceModified.bind(this), ); vm.addEventListener( "vm-devices-update", this._handleDevicesModified.bind(this), ); vm.addEventListener( "vm-device-id-change", this._handleDeviceIdChange.bind(this), ); vm.addEventListener( "vm-devices-removed", this._handleDevicesRemoved.bind(this), ); }); this.updateDevice(); return root; } disconnectedCallback(): void { window.VM.get().then((vm) => { vm.removeEventListener( "vm-device-modified", this._handleDeviceModified.bind(this), ); vm.removeEventListener( "vm-devices-update", this._handleDevicesModified.bind(this), ); vm.removeEventListener( "vm-device-id-change", this._handleDeviceIdChange.bind(this), ); vm.removeEventListener( "vm-devices-removed", this._handleDevicesRemoved.bind(this), ); }); } async _handleDeviceModified(e: CustomEvent) { const id = e.detail; const activeIcId = window.App.app.session.activeIC; if (this.deviceID === id) { this.updateDevice(); } else if ( id === activeIcId && this.objectSubscriptions.includes("active-ic") ) { this.updateDevice(); this.requestUpdate(); } else if (this.objectSubscriptions.includes("visible-devices")) { const visibleDevices = await window.VM.vm.visibleDeviceIds( this.deviceID, ); if (visibleDevices.includes(id)) { this.updateDevice(); this.requestUpdate(); } } } async _handleDevicesModified(e: CustomEvent) { const activeIcId = window.App.app.session.activeIC; const ids = e.detail; if (ids.includes(this.deviceID)) { this.updateDevice(); if (this.objectSubscriptions.includes("visible-devices")) { this.requestUpdate(); } } else if ( ids.includes(activeIcId) && this.objectSubscriptions.includes("active-ic") ) { this.updateDevice(); this.requestUpdate(); } else if (this.objectSubscriptions.includes("visible-devices")) { const visibleDevices = await window.VM.vm.visibleDeviceIds( this.deviceID, ); if (ids.some((id) => visibleDevices.includes(id))) { this.updateDevice(); this.requestUpdate(); } } } async _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) { if (this.deviceID === e.detail.old) { this.deviceID = e.detail.new; } else if (this.objectSubscriptions.includes("visible-devices")) { const visibleDevices = await window.VM.vm.visibleDeviceIds( this.deviceID, ); 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(); } } updateDevice() { this.obj = window.VM.vm.objects.get(this.deviceID)!; if (typeof this.obj === "undefined") { return; } if ( this.objectSubscriptions.includes("slots") || this.objectSubscriptions.includes("slots-count") ) { const slotsOccupantInfo = this.obj.obj_info.slots; const logicTemplate = "logic" in this.obj.template ? this.obj.template.logic : null; const slotsTemplate = "slots" in this.obj.template ? this.obj.template.slots : []; let slots: Slot[] | null = null; if (slotsOccupantInfo.size !== 0) { slots = slotsTemplate.map((template, index) => { let slot = { parent: this.obj.obj_info.id, index: index, name: template.name, typ: template.typ, readable_logic: Array.from( logicTemplate?.logic_slot_types.get(index)?.entries() ?? [], ) .filter(([_, val]) => val === "Read" || val === "ReadWrite") .map(([key, _]) => key), writeable_logic: Array.from( logicTemplate?.logic_slot_types.get(index)?.entries() ?? [], ) .filter(([_, val]) => val === "Write" || val === "ReadWrite") .map(([key, _]) => key), occupant: slotsOccupantInfo.get(index), }; return slot; }); } if (!structuralEqual(this.slots, slots)) { this.slots = slots; } } for (const sub of this.objectSubscriptions) { if (typeof sub === "string") { if (sub == "name") { const name = this.obj.obj_info.name ?? null; if (this.name !== name) { this.name = name; } } else if (sub === "nameHash") { const nameHash = typeof this.obj.obj_info.name !== "undefined" ? crc32(this.obj.obj_info.name) : null; if (this.nameHash !== nameHash) { this.nameHash = nameHash; } } else if (sub === "prefabName") { const prefabName = this.obj.obj_info.prefab ?? null; if (this.prefabName !== prefabName) { this.prefabName = prefabName; this.prefabHash = crc32(prefabName); } } else if (sub === "fields") { const fields = this.obj.obj_info.logic_values ?? null; const logicTemplate = "logic" in this.obj.template ? this.obj.template.logic : null; let logic_fields: Map | null = null; if (fields !== null) { logic_fields = new Map(); for (const [lt, val] of fields) { const access = logicTemplate?.logic_types.get(lt) ?? "Read"; logic_fields.set(lt, { value: val, field_type: access, }); } } if (!structuralEqual(this.fields, logic_fields)) { this.fields = logic_fields; } } else if (sub === "reagents") { const reagents = this.obj.obj_info.reagents; if (!structuralEqual(this.reagents, reagents)) { this.reagents = reagents; } } else if (sub === "connections") { const connectionsMap = this.obj.obj_info.connections ?? new Map(); const connectionList = "device" in this.obj.template ? this.obj.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"; }); } if (!structuralEqual(this.connections, connections)) { this.connections = connections; } } else if (sub === "ic") { if ( typeof this.obj.obj_info.circuit !== "undefined" || this.obj.obj_info.socketed_ic !== "undefined" ) { this.updateIC(); } } else if (sub === "active-ic") { const activeIc = window.VM.vm?.activeIC; if (this.activeICId !== activeIc.obj_info.id) { this.activeICId = activeIc.obj_info.id; } } } else { if ("field" in sub) { const fields = this.obj.obj_info.logic_values; if (this.fields.get(sub.field) !== fields.get(sub.field)) { this.fields = fields; } } else if ("slot" in sub) { const slots = this.obj.slots; if ( typeof this.slots === "undefined" || this.slots.length < sub.slot ) { this.slots = slots; } else if ( !structuralEqual(this.slots[sub.slot], slots[sub.slot]) ) { this.slots = slots; } } } } } updateIC() { const ip = this.obj.ip!; if (this.icIP !== ip) { this.icIP = ip; } const opCount = this.obj.instructionCount!; if (this.icOpCount !== opCount) { this.icOpCount = opCount; } const state = this.obj.state!; if (this.icState !== state) { this.icState = state; } const errors = this.obj.program?.errors ?? null; if (!structuralEqual(this.errors, errors)) { this.errors = errors; } const registers = this.obj.registers ?? null; if (!structuralEqual(this.registers, registers)) { this.registers = registers; } const stack = this.obj.stack ?? null; if (!structuralEqual(this.memory, stack)) { this.memory = stack; } const aliases = this.obj.aliases ?? null; if (!structuralEqual(this.aliases, aliases)) { this.aliases = aliases; } const defines = this.obj.defines ?? null; if (!structuralEqual(this.defines, defines)) { this.defines = defines; } const pins = this.obj.pins ?? null; if (!structuralEqual(this.pins, pins)) { this.pins = pins; } } } return VMObjectMixinClass as Constructor & T; }; export const VMActiveICMixin = >( superClass: T, ) => { class VMActiveICMixinClass extends VMObjectMixin(superClass) { constructor() { super(); this.objectID = 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 !== id) { this.objectID = id; this.obj = window.VM.vm.objects.get(this.objectID)!; } this.updateDevice(); } } return VMActiveICMixinClass as Constructor & T; }; export declare class VMDeviceDBMixinInterface { templateDB: TemplateDatabase; _handleDeviceDBLoad(e: CustomEvent): void; postDBSetUpdate(): void; } export const VMDeviceDBMixin = >( superClass: T, ) => { class VMDeviceDBMixinClass extends superClass { connectedCallback(): void { const root = super.connectedCallback(); window.VM.vm.addEventListener( "vm-device-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-device-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 VMDeviceDBMixinClass as Constructor & T; };