Files
ic10emu/www/src/ts/virtualMachine/baseDevice.ts

658 lines
21 KiB
TypeScript

import { property, state } from "lit/decorators.js";
import type {
Slot,
Connection,
ICError,
LogicType,
LogicField,
Operand,
ObjectID,
TemplateDatabase,
FrozenObjectFull,
Class,
LogicSlotType,
SlotOccupantInfo,
ICState,
} 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;
logicFields: Map<LogicType, LogicField> | null;
slots: VmObjectSlotInfo[] | null;
slotsCount: number | null;
reagents: Map<number, number> | null;
connections: Connection[] | null;
icIP: number | null;
icOpCount: number | null;
icState: string | null;
errors: ICError[] | null;
registers: number[] | null;
memory: number[] | null;
aliases: Map<string, Operand> | null;
defines: Map<string, number> | null;
numPins: number | null;
pins: Map<number, ObjectID> | null;
visibleDevices: 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"
| "memory"
| "ic"
| "active-ic"
| { field: LogicType }
| { slot: number }
| "visible-devices";
export interface VmObjectSlotInfo {
parent: ObjectID;
index: number;
name: string;
typ: Class;
logicFields: Map<LogicSlotType, LogicField>;
quantity: number;
occupant: FrozenObjectFull | undefined;
}
export const VMObjectMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMObjectMixinClass extends superClass {
private _objectID: number;
get objectID() {
return this._objectID;
}
@property({ type: Number })
set objectID(val: number) {
this._objectID = 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 = null;
@state() prefabHash: number | null = null;
@state() logicFields: Map<LogicType, LogicField> | null = null;
@state() slots: VmObjectSlotInfo[] | null = null;
@state() slotsCount: number | null = null;
@state() reagents: Map<number, number> | null = null;
@state() connections: Connection[] | null = null;
@state() icIP: number | null = null;
@state() icOpCount: number | null = null;
@state() icState: ICState | null = null;
@state() errors: ICError[] | null = null;
@state() registers: number[] | null = null;
@state() memory: number[] | null = null;
@state() aliases: Map<string, Operand> | null = null;
@state() defines: Map<string, number> | null = null;
@state() numPins: number | null = null;
@state() pins: Map<number, ObjectID> | null = null;
@state() visibleDevices: ObjectID[] | null = 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.updateDevice();
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 === 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.objectID,
);
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.objectID)) {
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.objectID,
);
if (ids.some((id) => visibleDevices.includes(id))) {
this.updateDevice();
this.requestUpdate();
}
}
}
async _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) {
if (this.objectID === e.detail.old) {
this.objectID = e.detail.new;
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID,
);
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.objectID)!;
if (typeof this.obj === "undefined") {
return;
}
let newFields: Map<LogicType, LogicField> | null = null;
if (
this.objectSubscriptions.some(
(sub) =>
sub === "fields" || (typeof sub === "object" && "field" in sub),
)
) {
const logicValues =
this.obj.obj_info.logic_values != null
? (new Map(Object.entries(this.obj.obj_info.logic_values)) as Map<
LogicType,
number
>)
: null;
const logicTemplate =
"logic" in this.obj.template ? this.obj.template.logic : null;
newFields = 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];
},
),
);
}
const visibleDevices = this.obj.obj_info.visible_devices ?? [];
if (!structuralEqual(this.visibleDevices, visibleDevices)) {
this.visibleDevices = visibleDevices;
}
let newSlots: VmObjectSlotInfo[] | null = null;
if (
this.objectSubscriptions.some(
(sub) =>
sub === "slots" || (typeof sub === "object" && "slot" in sub),
)
) {
const slotsOccupantInfo =
this.obj.obj_info.slots != null
? new Map(
Object.entries(this.obj.obj_info.slots).map(([key, val]) => [
parseInt(key),
val,
]),
)
: null;
const slotsLogicValues =
this.obj.obj_info.slot_logic_values != null
? new Map<number, Map<LogicSlotType, number>>(
Object.entries(this.obj.obj_info.slot_logic_values).map(
([index, values]) => [
parseInt(index),
new Map(Object.entries(values)) as Map<
LogicSlotType,
number
>,
],
),
)
: null;
const logicTemplate =
"logic" in this.obj.template ? this.obj.template.logic : null;
const slotsTemplate =
"slots" in this.obj.template ? this.obj.template.slots : [];
newSlots = slotsTemplate.map((template, index) => {
const fieldEntryInfos = Array.from(
Object.entries(logicTemplate?.logic_slot_types[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"
? window.VM.vm.objects.get(occupantInfo.id)
: null;
let slot: VmObjectSlotInfo = {
parent: this.obj.obj_info.id,
index: index,
name: template.name,
typ: template.typ,
logicFields: logicFields,
occupant: occupant,
quantity: occupantInfo?.quantity ?? 0,
};
return slot;
});
}
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") {
if (!structuralEqual(this.logicFields, newFields)) {
this.logicFields = newFields;
}
} else if (sub === "slots") {
if (!structuralEqual(this.slots, newSlots)) {
this.slots = newSlots;
this.slotsCount = newSlots.length;
}
} else if (sub === "slots-count") {
const slotsTemplate =
"slots" in this.obj.template ? this.obj.template.slots : [];
const slotsCount = slotsTemplate.length;
if (this.slotsCount !== slotsCount) {
this.slotsCount = slotsCount;
}
} else if (sub === "reagents") {
const reagents =
this.obj.obj_info.reagents != null
? new Map(
Object.entries(this.obj.obj_info.reagents).map(
([key, val]) => [parseInt(key), val],
),
)
: null;
if (!structuralEqual(this.reagents, reagents)) {
this.reagents = reagents;
}
} else if (sub === "connections") {
const connectionsMap =
this.obj.obj_info.connections != null
? new Map(
Object.entries(this.obj.obj_info.connections).map(
([key, val]) => [parseInt(key), val],
),
)
: null;
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 === "memory") {
const stack = this.obj.obj_info.memory ?? null;
if (!structuralEqual(this.memory, stack)) {
this.memory = stack;
}
} else if (sub === "ic") {
if (
typeof this.obj.obj_info.circuit !== "undefined" ||
typeof 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) {
if (this.logicFields.get(sub.field) !== newFields.get(sub.field)) {
this.logicFields = newFields;
}
} else if ("slot" in sub) {
if (
typeof this.slots === "undefined" ||
this.slots.length < sub.slot
) {
this.slots = newSlots;
} else if (
!structuralEqual(this.slots[sub.slot], newSlots[sub.slot])
) {
this.slots = newSlots;
}
}
}
}
}
updateIC() {
const ip = this.obj.obj_info.circuit?.instruction_pointer ?? null;
if (this.icIP !== ip) {
this.icIP = ip;
}
const opCount =
this.obj.obj_info.circuit?.yield_instruction_count ?? null;
if (this.icOpCount !== opCount) {
this.icOpCount = opCount;
}
const state = this.obj.obj_info.circuit?.state ?? null;
if (this.icState !== state) {
this.icState = state;
}
const errors = this.obj.obj_info.compile_errors ?? null;
if (!structuralEqual(this.errors, errors)) {
this.errors = errors;
}
const registers = this.obj.obj_info.circuit?.registers ?? null;
if (!structuralEqual(this.registers, registers)) {
this.registers = registers;
}
const aliases =
this.obj.obj_info.circuit?.aliases != null
? new Map(Object.entries(this.obj.obj_info.circuit.aliases))
: null;
if (!structuralEqual(this.aliases, aliases)) {
this.aliases = aliases;
}
const defines =
this.obj.obj_info.circuit?.defines != null
? new Map(
Object.entries(this.obj.obj_info.circuit.defines),
// .map(([key, val]) => [])
)
: null;
if (!structuralEqual(this.defines, defines)) {
this.defines = new Map(defines);
}
const pins =
this.obj.obj_info.device_pins != null
? new Map(
Object.entries(this.obj.obj_info.device_pins).map(
([key, val]) => [parseInt(key), val],
),
)
: null;
if (!structuralEqual(this.pins, pins)) {
this.pins = pins;
this.numPins =
"device" in this.obj.template
? this.obj.template.device.device_pins_length
: Math.max(...Array.from(this.pins?.keys() ?? [0]));
}
}
}
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 VMTemplateDBMixinInterface {
templateDB: TemplateDatabase;
_handleDeviceDBLoad(e: CustomEvent): void;
postDBSetUpdate(): void;
}
export const VMTemplateDBMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMTemplateDBMixinClass 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 VMTemplateDBMixinClass as Constructor<VMTemplateDBMixinInterface> & T;
};