Files
ic10emu/www/src/ts/virtual_machine/base_device.ts

545 lines
17 KiB
TypeScript

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<T = {}> = 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<LogicType, LogicField>;
slots: Slot[];
reagents: Map<number, number>;
connections: Connection[];
icIP: number;
icOpCount: number;
icState: string;
errors: ICError[];
registers: number[] | null;
memory: number[] | null;
aliases: Map<string, Operand> | null;
defines: Map<string, string> | null;
numPins: number | null;
pins: Map<number, ObjectID> | 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 = <T extends Constructor<LitElement>>(
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<LogicType, LogicField>;
@state() slots: Slot[];
@state() reagents: Map<number, number>;
@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<string, Operand> | null;
@state() defines: Map<string, string> | null;
@state() numPins: number | null;
@state() pins: Map<number, ObjectID> | 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<number[]>) {
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<number[]>) {
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<LogicType, LogicField> | 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<VMObjectMixinInterface> & T;
};
export const VMActiveICMixin = <T extends Constructor<LitElement>>(
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<VMObjectMixinInterface> & T;
};
export declare class VMDeviceDBMixinInterface {
templateDB: TemplateDatabase;
_handleDeviceDBLoad(e: CustomEvent): void;
postDBSetUpdate(): void;
}
export const VMDeviceDBMixin = <T extends Constructor<LitElement>>(
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<VMDeviceDBMixinInterface> & T;
};