-
+
-
+
Ace
Vim
Emacs
Sublime
VS Code
-
+
Ace
Slim
Smooth
Smooth And Slim
Wide
-
-
+
+
Relative Line Numbers
@@ -237,12 +209,9 @@ export class IC10Editor extends BaseElement {
// characterData: false,
// });
- this.sessions.set(this.active_session, this.editor.getSession());
- this.bindSession(
- this.active_session,
- this.sessions.get(this.active_session),
- );
- this.active_line_markers.set(this.active_session, null);
+ this.sessions.set(this.activeSession, this.editor.getSession());
+ this.bindSession(this.activeSession, this.sessions.get(this.activeSession));
+ this.activeLineMarkers.set(this.activeSession, null);
const worker = await setupLspWorker();
this.setupLsp(worker);
@@ -271,35 +240,35 @@ export class IC10Editor extends BaseElement {
const that = this;
const app = await window.App.get();
- app.session.onLoad(((e: CustomEvent) => {
+ app.session.onLoad((_e) => {
const session = app.session;
const updated_ids: number[] = [];
for (const [id, code] of session.programs) {
updated_ids.push(id);
that.createOrSetSession(id, code);
}
- that.activateSession(that.active_session);
+ that.activateSession(that.activeSession);
for (const [id, _] of that.sessions) {
if (!updated_ids.includes(id)) {
that.destroySession(id);
}
}
- }) as EventListener);
+ });
app.session.loadFromFragment();
- app.session.onActiveLine(((e: CustomEvent) => {
+ app.session.onActiveLine((e) => {
const session = app.session;
const id: number = e.detail;
const active_line = session.getActiveLine(id);
if (typeof active_line !== "undefined") {
- const marker = that.active_line_markers.get(id);
+ const marker = that.activeLineMarkers.get(id);
if (marker) {
that.sessions.get(id)?.removeMarker(marker);
- that.active_line_markers.set(id, null);
+ that.activeLineMarkers.set(id, null);
}
const session = that.sessions.get(id);
if (session) {
- that.active_line_markers.set(
+ that.activeLineMarkers.set(
id,
session.addMarker(
new Range(active_line, 0, active_line, 1),
@@ -308,14 +277,30 @@ export class IC10Editor extends BaseElement {
true,
),
);
- if (that.active_session == id) {
+ if (that.activeSession == id) {
// editor.resize(true);
// TODO: Scroll to line if vm was stepped
//that.editor.scrollToLine(active_line, true, true, ()=>{})
}
}
}
- }) as EventListener);
+ });
+
+ app.session.onIDChange((e) => {
+ const oldID = e.detail.old;
+ const newID = e.detail.new;
+ if (this.sessions.has(oldID)) {
+ this.sessions.set(newID, this.sessions.get(oldID));
+ this.sessions.delete(oldID);
+ }
+ if (this.activeLineMarkers.has(oldID)) {
+ this.activeLineMarkers.set(newID, this.activeLineMarkers.get(oldID));
+ this.activeLineMarkers.delete(oldID);
+ }
+ if (this.activeSession === oldID) {
+ this.activeSession = newID;
+ }
+ });
// change -> possibility to allow saving the value without having to wait for blur
editor.on("change", () => this.editorChangeAction());
@@ -528,7 +513,7 @@ export class IC10Editor extends BaseElement {
const mode = ace.require(this.mode);
const options = mode?.options ?? {};
this.languageProvider?.setSessionOptions(session, options);
- this.active_session = session_id;
+ this.activeSession = session_id;
return true;
}
@@ -576,7 +561,7 @@ export class IC10Editor extends BaseElement {
}
const session = this.sessions.get(session_id);
this.sessions.delete(session_id);
- if ((this.active_session = session_id)) {
+ if ((this.activeSession = session_id)) {
this.activateSession(this.sessions.entries().next().value);
}
session?.destroy();
diff --git a/www/src/ts/app/icons.ts b/www/src/ts/icons.ts
similarity index 100%
rename from www/src/ts/app/icons.ts
rename to www/src/ts/icons.ts
diff --git a/www/src/ts/index.ts b/www/src/ts/index.ts
index a19527f..07daf7e 100644
--- a/www/src/ts/index.ts
+++ b/www/src/ts/index.ts
@@ -1,6 +1,43 @@
import "@popperjs/core";
import "../scss/styles.scss";
-import { Dropdown, Modal } from "bootstrap";
+import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
+setBasePath("shoelace");
+import "./icons";
+
+import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
+import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
+import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
+import "@shoelace-style/shoelace/dist/components/icon/icon.js";
+import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
+import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js";
+import "@shoelace-style/shoelace/dist/components/button-group/button-group.js";
+import "@shoelace-style/shoelace/dist/components/button/button.js";
+import '@shoelace-style/shoelace/dist/components/switch/switch.js';
+import "@shoelace-style/shoelace/dist/components/radio-button/radio-button.js";
+import "@shoelace-style/shoelace/dist/components/radio-group/radio-group.js";
+import "@shoelace-style/shoelace/dist/components/menu/menu.js";
+import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
+import "@shoelace-style/shoelace/dist/components/divider/divider.js";
+import "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js";
+import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
+import "@shoelace-style/shoelace/dist/components/input/input.js";
+import "@shoelace-style/shoelace/dist/components/spinner/spinner.js";
+import "@shoelace-style/shoelace/dist/components/card/card.js";
+import "@shoelace-style/shoelace/dist/components/details/details.js";
+import "@shoelace-style/shoelace/dist/components/tab/tab.js";
+import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
+import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
+import "@shoelace-style/shoelace/dist/components/select/select.js";
+import "@shoelace-style/shoelace/dist/components/badge/badge.js";
+import "@shoelace-style/shoelace/dist/components/option/option.js";
+import "@shoelace-style/shoelace/dist/components/alert/alert.js";
+import "@shoelace-style/shoelace/dist/components/format-number/format-number.js";
+import "@shoelace-style/shoelace/dist/components/format-date/format-date.js";
+import "@shoelace-style/shoelace/dist/components/format-bytes/format-bytes.js";
+import "@shoelace-style/shoelace/dist/components/relative-time/relative-time.js";
+
+import "ace-builds";
+import "ace-builds/esm-resolver";
class DeferedApp {
diff --git a/www/src/ts/presets/demo.ts b/www/src/ts/presets/demo.ts
index 00ffbc3..dd817e4 100644
--- a/www/src/ts/presets/demo.ts
+++ b/www/src/ts/presets/demo.ts
@@ -90,6 +90,10 @@ export const demoVMState: VMState = {
occupant: {
id: 2,
fields: {
+ "PrefabHash": {
+ field_type: "Read",
+ value: -744098481,
+ },
"Quantity":{
field_type: "Read",
value: 1
@@ -101,7 +105,7 @@ export const demoVMState: VMState = {
"SortingClass": {
field_type: "Read",
value: 0,
- }
+ },
},
},
},
@@ -120,7 +124,20 @@ export const demoVMState: VMState = {
},
},
],
- fields: {},
+ fields: {
+ "PrefabHash": {
+ field_type: "Read",
+ value: -128473777,
+ },
+ "Setting": {
+ field_type: "ReadWrite",
+ value: 0,
+ },
+ "RequiredPower": {
+ field_type: "Read",
+ value: 0,
+ }
+ },
},
],
networks: [
diff --git a/www/src/ts/session.ts b/www/src/ts/session.ts
index 0523563..3037e3a 100644
--- a/www/src/ts/session.ts
+++ b/www/src/ts/session.ts
@@ -1,4 +1,3 @@
-
import type { ICError, FrozenVM, SlotType } from "ic10emu_wasm";
import { App } from "./app";
@@ -57,7 +56,25 @@ export class Session extends EventTarget {
);
}
- onActiveIc(callback: EventListenerOrEventListenerObject) {
+ changeID(oldID: number, newID: number) {
+ if (this.programs.has(oldID)) {
+ this.programs.set(newID, this.programs.get(oldID));
+ this.programs.delete(oldID);
+ }
+ this.dispatchEvent(
+ new CustomEvent("session-id-change", {
+ detail: { old: oldID, new: newID },
+ }),
+ );
+ }
+
+ onIDChange(
+ callback: (e: CustomEvent<{ old: number; new: number }>) => any,
+ ) {
+ this.addEventListener("session-id-change", callback);
+ }
+
+ onActiveIc(callback: (e: CustomEvent
) => any,) {
this.addEventListener("session-active-ic", callback);
}
@@ -98,11 +115,11 @@ export class Session extends EventTarget {
);
}
- onErrors(callback: EventListenerOrEventListenerObject) {
+ onErrors(callback: (e: CustomEvent) => any) {
this.addEventListener("session-errors", callback);
}
- onLoad(callback: EventListenerOrEventListenerObject) {
+ onLoad(callback: (e: CustomEvent) => any) {
this.addEventListener("session-load", callback);
}
@@ -114,7 +131,7 @@ export class Session extends EventTarget {
);
}
- onActiveLine(callback: EventListenerOrEventListenerObject) {
+ onActiveLine(callback: (e: CustomEvent) => any) {
this.addEventListener("active-line", callback);
}
diff --git a/www/src/ts/utils.ts b/www/src/ts/utils.ts
index 62c94b6..9dcdbb7 100644
--- a/www/src/ts/utils.ts
+++ b/www/src/ts/utils.ts
@@ -242,3 +242,7 @@ export function parseIntWithHexOrBinary(s: string): number {
}
return parseInt(s);
}
+
+export function clamp (val: number, min: number, max: number) {
+ return Math.min(Math.max(val, min), max);
+}
diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts
index bcc2127..a2626b9 100644
--- a/www/src/ts/virtual_machine/base_device.ts
+++ b/www/src/ts/virtual_machine/base_device.ts
@@ -12,15 +12,17 @@ import type {
Aliases,
Defines,
Pins,
+ LogicType,
} from "ic10emu_wasm";
-import { structuralEqual } from "../utils";
-import { LitElement } from "lit";
-import { BaseElement } from "../components/base";
+import { structuralEqual } from "utils";
+import { LitElement, PropertyValueMap } from "lit";
+import type { DeviceDB } from "./device_db";
type Constructor = new (...args: any[]) => T;
export declare class VMDeviceMixinInterface {
deviceID: number;
+ activeICId: number;
device: DeviceRef;
name: string | null;
nameHash: number | null;
@@ -41,13 +43,29 @@ export declare class VMDeviceMixinInterface {
_handleDeviceModified(e: CustomEvent): void;
updateDevice(): void;
updateIC(): void;
+ subscribe(...sub: VMDeviceMixinSubscription[]): void;
+ unsubscribe(filter: (sub: VMDeviceMixinSubscription) => boolean): void;
}
+export type VMDeviceMixinSubscription =
+ | "name"
+ | "nameHash"
+ | "prefabName"
+ | "fields"
+ | "slots"
+ | "slots-count"
+ | "reagents"
+ | "connections"
+ | "ic"
+ | "active-ic"
+ | { field: LogicType }
+ | { slot: number };
+
export const VMDeviceMixin = >(
superClass: T,
) => {
class VMDeviceMixinClass extends superClass {
- _deviceID: number;
+ private _deviceID: number;
get deviceID() {
return this._deviceID;
}
@@ -57,8 +75,23 @@ export const VMDeviceMixin = >(
this.updateDevice();
}
+ @state() private deviceSubscriptions: VMDeviceMixinSubscription[] = [];
+
+ subscribe(...sub: VMDeviceMixinSubscription[]) {
+ this.deviceSubscriptions = this.deviceSubscriptions.concat(sub);
+ }
+
+ // remove subscripotions matching the filter
+ unsubscribe(filter: (sub: VMDeviceMixinSubscription) => boolean) {
+ this.deviceSubscriptions = this.deviceSubscriptions.filter(
+ (sub) => !filter(sub),
+ );
+ }
+
device: DeviceRef;
+ @state() activeICId: number;
+
@state() name: string | null = null;
@state() nameHash: number | null = null;
@state() prefabName: string | null;
@@ -78,69 +111,154 @@ export const VMDeviceMixin = >(
connectedCallback(): void {
const root = super.connectedCallback();
- window.VM.get().then((vm) =>
+ window.VM.get().then((vm) => {
vm.addEventListener(
"vm-device-modified",
this._handleDeviceModified.bind(this),
- ),
- );
- window.VM.get().then((vm) =>
+ );
vm.addEventListener(
"vm-devices-update",
this._handleDevicesModified.bind(this),
- ),
- );
+ );
+ vm.addEventListener(
+ "vm-device-id-change",
+ this._handleDeviceIdChange.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),
+ );
+ });
+ }
+
_handleDeviceModified(e: CustomEvent) {
const id = e.detail;
+ const activeIcId = window.App.app.session.activeIC;
if (this.deviceID === id) {
this.updateDevice();
- } else {
- this.requestUpdate();
+ } else if (
+ id === activeIcId &&
+ this.deviceSubscriptions.includes("active-ic")
+ ) {
+ this.updateDevice();
}
}
- _handleDevicesModified(e: CustomEvent) {
+ _handleDevicesModified(e: CustomEvent) {
+ const activeIcId = window.App.app.session.activeIC;
const ids = e.detail;
- this.requestUpdate();
+ if (ids.includes(this.deviceID)) {
+ this.updateDevice();
+ } else if (
+ ids.includes(activeIcId) &&
+ this.deviceSubscriptions.includes("active-ic")
+ ) {
+ this.updateDevice();
+ }
+ }
+
+ _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) {
+ if (this.deviceID === e.detail.old) {
+ this.deviceID = e.detail.new;
+ }
}
updateDevice() {
this.device = window.VM.vm.devices.get(this.deviceID)!;
- const name = this.device.name ?? null;
- if (this.name !== name) {
- this.name = name;
+ if (typeof this.device === "undefined") {
+ return;
}
- const nameHash = this.device.nameHash ?? null;
- if (this.nameHash !== nameHash) {
- this.nameHash = nameHash;
- }
- const prefabName = this.device.prefabName ?? null;
- if (this.prefabName !== prefabName) {
- this.prefabName = prefabName;
- }
- const fields = this.device.fields;
- if (!structuralEqual(this.fields, fields)) {
- this.fields = fields;
- }
- const slots = this.device.slots;
- if (!structuralEqual(this.slots, slots)) {
- this.slots = slots;
- }
- const reagents = this.device.reagents;
- if (!structuralEqual(this.reagents, reagents)) {
- this.reagents = reagents;
- }
- const connections = this.device.connections;
- if (!structuralEqual(this.connections, connections)) {
- this.connections = connections;
- }
- if (typeof this.device.ic !== "undefined") {
- this.updateIC();
+
+ for (const sub of this.deviceSubscriptions) {
+ if (typeof sub === "string") {
+ if (sub == "name") {
+ const name = this.device.name ?? null;
+ if (this.name !== name) {
+ this.name = name;
+ }
+ } else if (sub === "nameHash") {
+ const nameHash = this.device.nameHash ?? null;
+ if (this.nameHash !== nameHash) {
+ this.nameHash = nameHash;
+ }
+ } else if (sub === "prefabName") {
+ const prefabName = this.device.prefabName ?? null;
+ if (this.prefabName !== prefabName) {
+ this.prefabName = prefabName;
+ }
+ } else if (sub === "fields") {
+ const fields = this.device.fields;
+ if (!structuralEqual(this.fields, fields)) {
+ this.fields = fields;
+ }
+ } else if (sub === "slots") {
+ const slots = this.device.slots;
+ if (!structuralEqual(this.slots, slots)) {
+ this.slots = slots;
+ }
+ } else if (sub === "slots-count") {
+ const slots = this.device.slots;
+ if (typeof this.slots === "undefined") {
+ this.slots = slots;
+ } else if (this.slots.length !== slots.length) {
+ this.slots = slots;
+ }
+ } else if (sub === "reagents") {
+ const reagents = this.device.reagents;
+ if (!structuralEqual(this.reagents, reagents)) {
+ this.reagents = reagents;
+ }
+ } else if (sub === "connections") {
+ const connections = this.device.connections;
+ if (!structuralEqual(this.connections, connections)) {
+ this.connections = connections;
+ }
+ } else if (sub === "ic") {
+ if (typeof this.device.ic !== "undefined") {
+ this.updateIC();
+ }
+ } else if (sub === "active-ic") {
+ const activeIc = window.VM.vm?.activeIC;
+ if (this.activeICId !== activeIc.id) {
+ this.activeICId = activeIc.id;
+ }
+ }
+ } else {
+ if ("field" in sub) {
+ const fields = this.device.fields;
+ if (this.fields.get(sub.field) !== fields.get(sub.field)) {
+ this.fields = fields;
+ }
+ } else if ("slot" in sub) {
+ const slots = this.device.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;
+ }
+ }
+ }
}
}
@@ -207,6 +325,19 @@ export const VMActiveICMixin = >(
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.deviceID !== id) {
@@ -216,5 +347,57 @@ export const VMActiveICMixin = >(
this.updateDevice();
}
}
+
return VMActiveICMixinClass as Constructor & T;
};
+
+export declare class VMDeviceDBMixinInterface {
+ deviceDB: DeviceDB;
+ _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.db !== "undefined") {
+ this.deviceDB = window.VM.vm.db!;
+ }
+ return root;
+ }
+
+ disconnectedCallback(): void {
+ window.VM.vm.removeEventListener(
+ "vm-device-db-loaded",
+ this._handleDeviceDBLoad.bind(this),
+ );
+ }
+
+ _handleDeviceDBLoad(e: CustomEvent) {
+ this.deviceDB = e.detail;
+ }
+
+ private _deviceDB: DeviceDB;
+
+ get deviceDB(): DeviceDB {
+ return this._deviceDB;
+ }
+
+ postDBSetUpdate(): void { }
+
+ @state()
+ set deviceDB(val: DeviceDB) {
+ this._deviceDB = val;
+ this.postDBSetUpdate();
+ }
+ }
+
+ return VMDeviceDBMixinClass as Constructor & T;
+};
diff --git a/www/src/ts/virtual_machine/controls.ts b/www/src/ts/virtual_machine/controls.ts
index ae67c45..067ebd1 100644
--- a/www/src/ts/virtual_machine/controls.ts
+++ b/www/src/ts/virtual_machine/controls.ts
@@ -1,21 +1,18 @@
import { html, css } from "lit";
import { customElement, query } from "lit/decorators.js";
-import { BaseElement, defaultCss } from "../components";
-import { VMActiveICMixin } from "./base_device";
+import { BaseElement, defaultCss } from "components";
+import { VMActiveICMixin } from "virtual_machine/base_device";
-import "@shoelace-style/shoelace/dist/components/card/card.js";
-import "@shoelace-style/shoelace/dist/components/button-group/button-group.js";
-import "@shoelace-style/shoelace/dist/components/button/button.js";
-import "@shoelace-style/shoelace/dist/components/icon/icon.js";
-import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
-import "@shoelace-style/shoelace/dist/components/divider/divider.js";
-import "@shoelace-style/shoelace/dist/components/select/select.js";
-import "@shoelace-style/shoelace/dist/components/badge/badge.js";
-import "@shoelace-style/shoelace/dist/components/option/option.js";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
@customElement("vm-ic-controls")
export class VMICControls extends VMActiveICMixin(BaseElement) {
+
+ constructor() {
+ super();
+ this.subscribe("ic", "active-ic")
+ }
+
static styles = [
...defaultCss,
css`
diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts
deleted file mode 100644
index 57b8415..0000000
--- a/www/src/ts/virtual_machine/device.ts
+++ /dev/null
@@ -1,1100 +0,0 @@
-import type {
- Connection,
- DeviceTemplate,
- LogicField,
- LogicFields,
- LogicType,
- Slot,
- SlotTemplate,
- SlotOccupant,
- SlotOccupantTemplate,
- SlotLogicType,
- ConnectionCableNetwork,
- SlotType,
-} from "ic10emu_wasm";
-import { html, css, HTMLTemplateResult } from "lit";
-import { customElement, property, query, state } from "lit/decorators.js";
-import { BaseElement, defaultCss } from "../components";
-import { VMDeviceMixin } from "./base_device";
-
-import { default as uFuzzy } from "@leeoniya/ufuzzy";
-
-import "@shoelace-style/shoelace/dist/components/card/card.js";
-import "@shoelace-style/shoelace/dist/components/icon/icon.js";
-import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
-import "@shoelace-style/shoelace/dist/components/input/input.js";
-import "@shoelace-style/shoelace/dist/components/details/details.js";
-import "@shoelace-style/shoelace/dist/components/tab/tab.js";
-import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
-import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
-import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js";
-import "@shoelace-style/shoelace/dist/components/select/select.js";
-import "@shoelace-style/shoelace/dist/components/badge/badge.js";
-import "@shoelace-style/shoelace/dist/components/option/option.js";
-import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
-import "@shoelace-style/shoelace/dist/components/icon/icon.js";
-
-import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
-import {
- displayNumber,
- parseIntWithHexOrBinary,
- parseNumber,
- structuralEqual,
-} from "../utils";
-import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
-import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
-import type { DeviceDB, DeviceDBEntry } from "./device_db";
-import { connectionFromDeviceDBConnection } from "./utils";
-import { SlDialog } from "@shoelace-style/shoelace";
-import { repeat } from "lit/directives/repeat.js";
-import { cache } from "lit/directives/cache.js";
-
-@customElement("vm-device-card")
-export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
- image_err: boolean;
-
- @property({ type: Boolean }) open: boolean;
-
- constructor() {
- super();
- this.open = false;
- }
-
- static styles = [
- ...defaultCss,
- css`
- :host {
- display: block;
- box-sizing: border-box;
- }
- .card {
- width: 100%;
- box-sizing: border-box;
- }
- .image {
- width: 4rem;
- height: 4rem;
- }
- .header {
- display: flex;
- flex-direction: row;
- flex-grow: 1;
- }
- .header-name {
- display: flex;
- flex-direction: row;
- width: 100%;
- flex-grow: 1;
- align-items: center;
- flex-wrap: wrap;
- }
- .device-card {
- --padding: var(--sl-spacing-small);
- }
- .device-name::part(input) {
- width: 10rem;
- }
- .device-id::part(input) {
- width: 7rem;
- }
- .device-name-hash::part(input) {
- width: 7rem;
- }
- .slot-header.image {
- width: 1.5rem;
- height: 1.5rem;
- border: var(--sl-panel-border-width) solid var(--sl-panel-border-color);
- border-radius: var(--sl-border-radius-medium);
- background-color: var(--sl-color-neutral-0);
- }
- sl-divider {
- --spacing: 0.25rem;
- }
- sl-button[variant="success"] {
- /* Changes the success theme color to purple using primitives */
- --sl-color-success-600: var(--sl-color-purple-700);
- }
- sl-button[variant="primary"] {
- /* Changes the success theme color to purple using primitives */
- --sl-color-primary-600: var(--sl-color-cyan-600);
- }
- sl-button[variant="warning"] {
- /* Changes the success theme color to purple using primitives */
- --sl-color-warning-600: var(--sl-color-amber-600);
- }
- sl-tab-group {
- margin-left: 1rem;
- margin-right: 1rem;
- --indicator-color: var(--sl-color-purple-600);
- --sl-color-primary-600: var(--sl-color-purple-600);
- }
- sl-tab::part(base) {
- padding: var(--sl-spacing-small) var(--sl-spacing-medium);
- }
- sl-tab-group::part(base) {
- max-height: 20rem;
- overflow-y: auto;
- }
- sl-icon-button.remove-button::part(base) {
- color: var(--sl-color-danger-600);
- }
- sl-icon-button.remove-button::part(base):hover,
- sl-icon-button.remove-button::part(base):focus {
- color: var(--sl-color-danger-500);
- }
- sl-icon-button.remove-button::part(base):active {
- color: var(--sl-color-danger-600);
- }
- .remove-dialog-body {
- display: flex;
- flex-direction: row;
- }
- .dialog-image {
- width: 3rem;
- height: 3rem;
- }
- `,
- ];
-
- private _deviceDB: DeviceDB;
-
- get deviceDB(): DeviceDB {
- return this._deviceDB;
- }
-
- @state()
- set deviceDB(val: DeviceDB) {
- this._deviceDB = val;
- this.updateDevice();
- this.requestUpdate();
- }
-
- connectedCallback(): void {
- super.connectedCallback();
- window.VM.vm.addEventListener(
- "vm-device-db-loaded",
- this._handleDeviceDBLoad.bind(this),
- );
- }
-
- _handleDeviceDBLoad(e: CustomEvent) {
- this.deviceDB = e.detail;
- }
-
- onImageErr(e: Event) {
- this.image_err = true;
- console.log("Image load error", e);
- }
-
- renderHeader(): HTMLTemplateResult {
- const activeIc = window.VM.vm.activeIC;
- const thisIsActiveIc = activeIc.id === this.deviceID;
- const badges: HTMLTemplateResult[] = [];
- if (this.deviceID == activeIc?.id) {
- badges.push(html`db`);
- }
- activeIc?.pins?.forEach((id, index) => {
- if (this.deviceID == id) {
- badges.push(
- html`d${index}`,
- );
- }
- }, this);
- return html`
-
-
-
-
-
-
-
-
-
- `;
- }
-
- renderFields(): HTMLTemplateResult {
- const fields = Array.from(this.fields.entries());
- const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
- return html`
- ${fields.map(([name, field], _index, _fields) => {
- return html`
- ${name}
-
- ${field.field_type}
- `;
- })}
- `;
- }
-
- lookupSlotOccupantImg(
- occupant: SlotOccupant | undefined,
- typ: SlotType,
- ): string {
- if (typeof occupant !== "undefined") {
- const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
- const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash";
- return `img/stationpedia/${prefabName}.png`;
- } else {
- return `img/stationpedia/SlotIcon_${typ}.png`;
- }
- }
-
- _onSlotImageErr(e: Event) {
- console.log("image_err", e);
- }
-
- static transparentImg =
- "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
-
- renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
- const _fields = this.device.getSlotFields(slotIndex);
- const fields = Array.from(_fields.entries());
- const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`;
- const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ);
- return html`
-
-
-
- ${
- typeof slot.occupant !== "undefined"
- ? html`
-
-
- `
- : ""
- }
-
- ${fields.map(
- ([name, field], _index, _fields) => html`
-
- ${name}
-
- ${field.field_type}
-
- `,
- )}
-
-
- `;
- }
-
- renderSlots(): HTMLTemplateResult {
- return html`
-
- ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))}
-
- `;
- }
-
- renderReagents(): HTMLTemplateResult {
- return html``;
- }
-
- renderNetworks(): HTMLTemplateResult {
- const vmNetworks = window.VM.vm.networks;
- const networks = this.connections.map((connection, index, _conns) => {
- const conn =
- typeof connection === "object" ? connection.CableNetwork : null;
- return html`
-
- Connection:${index}
- ${vmNetworks.map(
- (net) =>
- html`Network ${net}`,
- )}
- ${conn?.typ}
-
- `;
- });
- return html`
-
- ${networks}
-
- `;
- }
- renderPins(): HTMLTemplateResult {
- const pins = this.pins;
- const visibleDevices = window.VM.vm.visibleDevices(this.deviceID);
- const pinsHtml = pins?.map(
- (pin, index) =>
- html`
-
- d${index}
- ${visibleDevices.map(
- (device, _index) =>
- html`
-
- Device ${device.id} : ${device.name ?? device.prefabName}
-
- `,
- )}
- `,
- );
- return html`
-
- ${pinsHtml}
-
- `;
- }
-
- render(): HTMLTemplateResult {
- return html`
-
-
-
- Fields
- Slots
- Reagents
- Networks
- Pins
-
- ${this.renderFields()}
- ${this.renderSlots()}
- ${this.renderReagents()}
- ${this.renderNetworks()}
- ${this.renderPins()}
-
-
-
-
-

-
-
Are you sure you want to remove this device?
-
Id ${this.deviceID} : ${this.name ?? this.prefabName}
-
-
-
- Close
- Remove
-
-
- `;
- }
-
- @query(".remove-device-dialog") removeDialog: SlDialog;
-
- _preventOverlayClose(event: CustomEvent) {
- if (event.detail.source === "overlay") {
- event.preventDefault();
- }
- }
-
- _closeRemoveDialog() {
- this.removeDialog.hide();
- }
-
- _handleChangeID(e: CustomEvent) {
- const input = e.target as SlInput;
- const val = parseIntWithHexOrBinary(input.value);
- if (!isNaN(val)) {
- window.VM.get().then(vm => {
- if (!vm.changeDeviceId(this.deviceID, val)) {
- input.value = this.deviceID.toString();
- }
- });
- } else {
- input.value = this.deviceID.toString();
- }
- }
-
- _handleChangeName(e: CustomEvent) {
- const input = e.target as SlInput;
- const name = input.value.length === 0 ? undefined : input.value;
- window.VM.get().then(vm => {
- if (!vm.setDeviceName(this.deviceID, name)) {
- input.value = this.name;
- }
- this.updateDevice();
- });
- }
-
- _handleChangeField(e: CustomEvent) {
- const input = e.target as SlInput;
- const field = input.getAttribute("key")! as LogicType;
- const val = parseNumber(input.value);
- window.VM.get().then((vm) => {
- if (!vm.setDeviceField(this.deviceID, field, val, true)) {
- input.value = this.fields.get(field).value.toString();
- }
- this.updateDevice();
- });
- }
-
- _handleChangeSlotField(e: CustomEvent) {
- const input = e.target as SlInput;
- const slot = parseInt(input.getAttribute("slotIndex")!);
- const field = input.getAttribute("key")! as SlotLogicType;
- const val = parseNumber(input.value);
- window.VM.get().then((vm) => {
- if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) {
- input.value = this.device.getSlotField(slot, field).toString();
- }
- this.updateDevice();
- });
- }
-
- _handleDeviceRemoveButton(_e: Event) {
- this.removeDialog.show();
- }
-
- _removeDialogRemove() {
- this.removeDialog.hide();
- window.VM.get().then((vm) => vm.removeDevice(this.deviceID));
- }
-
- _handleChangeConnection(e: CustomEvent) {
- const select = e.target as SlSelect;
- const conn = parseInt(select.getAttribute("key")!);
- const val = select.value ? parseInt(select.value as string) : undefined;
- window.VM.get().then((vm) =>
- vm.setDeviceConnection(this.deviceID, conn, val),
- );
- this.updateDevice();
- }
-
- _handleChangePin(e: CustomEvent) {
- 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.deviceID, pin, val));
- this.updateDevice();
- }
-}
-
-@customElement("vm-device-list")
-export class VMDeviceList extends BaseElement {
- @state() devices: number[];
-
- static styles = [
- ...defaultCss,
- css`
- .header {
- margin-bottom: 1rem;
- padding: 0.25rem 0.25rem;
- align-items: center;
- display: flex;
- flex-direction: row;
- width: 100%;
- box-sizing: border-box;
- }
- .device-list {
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- }
- .device-list-card {
- width: 100%;
- }
- .device-filter-input {
- margin-left: auto;
- }
- `,
- ];
-
- constructor() {
- super();
- this.devices = [...window.VM.vm.deviceIds];
- }
-
- connectedCallback(): void {
- const root = super.connectedCallback();
- window.VM.get().then((vm) =>
- vm.addEventListener(
- "vm-devices-update",
- this._handleDevicesUpdate.bind(this),
- ),
- );
- return root;
- }
-
- _handleDevicesUpdate(e: CustomEvent) {
- const ids = e.detail;
- if (!structuralEqual(this.devices, ids)) {
- this.devices = ids;
- this.devices.sort();
- }
- }
-
- protected render(): HTMLTemplateResult {
- const deviceCards = repeat(
- this.filteredDeviceIds,
- (id) => id,
- (id) =>
- html`
- `,
- );
- const result = html`
-
- ${deviceCards}
- `;
-
- return result;
- }
-
- get filteredDeviceIds() {
- if (typeof this._filteredDeviceIds !== "undefined") {
- return this._filteredDeviceIds;
- } else {
- return this.devices;
- }
- }
-
- private _filteredDeviceIds: number[] | undefined;
- private _filter: string = "";
-
- @query(".device-filter-input") filterInput: SlInput;
- get filter() {
- return this._filter;
- }
-
- @state()
- set filter(val: string) {
- this._filter = val;
- this.performSearch();
- }
-
- private filterTimeout: number | undefined;
-
- _handleFilterInput(_e: CustomEvent) {
- if (this.filterTimeout) {
- clearTimeout(this.filterTimeout);
- }
- const that = this;
- this.filterTimeout = setTimeout(() => {
- that.filter = that.filterInput.value;
- that.filterTimeout = undefined;
- }, 500);
- }
-
- performSearch() {
- if (this._filter) {
- const datapoints: [string, number][] = [];
- for (const device_id of this.devices) {
- const device = window.VM.vm.devices.get(device_id);
- if (device) {
- if (typeof device.name !== "undefined") {
- datapoints.push([device.name, device.id]);
- }
- if (typeof device.prefabName !== "undefined") {
- datapoints.push([device.prefabName, device.id]);
- }
- }
- }
- const haystack: string[] = datapoints.map((data) => data[0]);
- const uf = new uFuzzy({});
- const [_idxs, info, order] = uf.search(haystack, this._filter, 0, 1e3);
-
- const filtered = order?.map((infoIdx) => datapoints[info.idx[infoIdx]]);
- const deviceIds: number[] =
- filtered
- ?.map((data) => data[1])
- ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
- this._filteredDeviceIds = deviceIds;
- } else {
- this._filteredDeviceIds = undefined;
- }
- }
-}
-
-@customElement("vm-add-device-button")
-export class VMAddDeviceButton extends BaseElement {
- static styles = [
- ...defaultCss,
- css`
- .add-device-drawer {
- --size: 32rem;
- }
-
- .search-results {
- display: flex;
- flex-direction: row;
- overflow-x: auto;
- }
-
- .card {
- margin-top: var(--sl-spacing-small);
- margin-right: var(--sl-spacing-small);
- }
-
- .card + .card {
- }
- `,
- ];
-
- @query("sl-drawer") drawer: SlDrawer;
- @query(".device-search-input") searchInput: SlInput;
-
- private _deviceDB: DeviceDB;
- private _strutures: Map = new Map();
- private _datapoints: [string, string][] = [];
- private _haystack: string[] = [];
- get deviceDB() {
- return this._deviceDB;
- }
-
- @state()
- set deviceDB(val: DeviceDB) {
- this._deviceDB = val;
- this._strutures = new Map(
- Object.values(this.deviceDB.db)
- .filter((entry) => this.deviceDB.structures.includes(entry.name), this)
- .filter(
- (entry) => this.deviceDB.logic_enabled.includes(entry.name),
- this,
- )
- .map((entry) => [entry.name, entry]),
- );
-
- const datapoints: [string, string][] = [];
- for (const entry of this._strutures.values()) {
- datapoints.push(
- [entry.title, entry.name],
- [entry.name, entry.name],
- [entry.desc, entry.name],
- );
- }
- const haystack: string[] = datapoints.map((data) => data[0]);
- this._datapoints = datapoints;
- this._haystack = haystack;
- this.performSearch();
- }
-
- private _filter: string = "";
-
- get filter() {
- return this._filter;
- }
-
- @state()
- set filter(val: string) {
- this._filter = val;
- this.performSearch();
- }
-
- private _searchResults: DeviceDBEntry[];
-
- private filterTimeout: number | undefined;
-
- performSearch() {
- if (this._filter) {
- const uf = new uFuzzy({});
- const [_idxs, info, order] = uf.search(
- this._haystack,
- this._filter,
- 0,
- 1e3,
- );
-
- const filtered = order?.map(
- (infoIdx) => this._datapoints[info.idx[infoIdx]],
- );
- const names =
- filtered
- ?.map((data) => data[1])
- ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
-
- this._searchResults = names.map((name) => this._strutures.get(name)!);
- } else {
- // clear our results and prefilter if the filter is empty
- this._searchResults = [];
- }
- }
-
- connectedCallback(): void {
- const root = super.connectedCallback();
- window.VM.get().then((vm) =>
- vm.addEventListener(
- "vm-device-db-loaded",
- this._handleDeviceDBLoad.bind(this),
- ),
- );
- return root;
- }
-
- _handleDeviceDBLoad(e: CustomEvent) {
- this.deviceDB = e.detail;
- }
-
- renderSearchResults() {
- return repeat(
- this._searchResults ?? [],
- (result) => result.name,
- (result) => cache(html`
-
-
- `)
- );
-
- }
-
- _handleDeviceAdd() {
- this.drawer.hide();
- }
-
- render() {
- return html`
-
- Add Device
-
-
-
- Search Structures
-
-
- ${this.renderSearchResults()}
- {
- this.drawer.hide();
- }}
- >
- Close
-
-
- `;
- }
-
- _handleSearchInput(e: CustomEvent) {
- if (this.filterTimeout) {
- clearTimeout(this.filterTimeout);
- }
- const that = this;
- this.filterTimeout = setTimeout(() => {
- that.filter = that.searchInput.value;
- that.filterTimeout = undefined;
- }, 200);
- }
-
- _handleAddButtonClick() {
- this.drawer.show();
- (this.drawer.querySelector(".device-search-input") as SlInput).select();
- }
-}
-
-@customElement("vm-device-template")
-export class VmDeviceTemplate extends BaseElement {
- private _deviceDB: DeviceDB;
- private image_err: boolean = false;
-
- static styles = [
- ...defaultCss,
- css`
- .template-card {
- --padding: var(--sl-spacing-small);
- }
- .image {
- width: 3rem;
- height: 3rem;
- }
- .header {
- display: flex;
- flex-direction: row;
- }
- .card-body {
- // height: 18rem;
- overflow-y: auto;
- }
- sl-tab::part(base) {
- padding: var(--sl-spacing-small) var(--sl-spacing-medium);
- }
- sl-tab-group::part(base) {
- height: 14rem;
- overflow-y: auto;
- }
- `,
- ];
-
- @state() fields: { [key in LogicType]?: LogicField };
- @state() slots: SlotTemplate[];
- @state() template: DeviceTemplate;
- @state() device_id: number | undefined;
- @state() device_name: string | undefined;
- @state() connections: Connection[];
-
- constructor() {
- super();
- this.deviceDB = window.VM.vm.db;
- }
-
- get deviceDB(): DeviceDB {
- return this._deviceDB;
- }
-
- @state()
- set deviceDB(val: DeviceDB) {
- this._deviceDB = val;
- this.setupState();
- }
-
- private _prefab_name: string;
-
- get prefab_name(): string {
- return this._prefab_name;
- }
-
- @property({ type: String })
- set prefab_name(val: string) {
- this._prefab_name = val;
- this.setupState();
- }
-
- get dbDevice(): DeviceDBEntry {
- return this.deviceDB.db[this.prefab_name];
- }
-
- setupState() {
- const slotlogicmap: { [key: number]: SlotLogicType[] } = {};
- for (const [slt, slotIndexes] of Object.entries(
- this.dbDevice?.slotlogic ?? {},
- )) {
- for (const slotIndex of slotIndexes) {
- const list = slotlogicmap[slotIndex] ?? [];
- list.push(slt as SlotLogicType);
- slotlogicmap[slotIndex] = list;
- }
- }
-
- this.fields = Object.fromEntries(
- Object.entries(this.dbDevice?.logic ?? {}).map(([lt, ft]) => {
- const value = lt === "PrefabHash" ? this.dbDevice.hash : 0.0;
- return [lt, { field_type: ft, value } as LogicField];
- }),
- );
-
- this.slots = (this.dbDevice?.slots ?? []).map(
- (slot, _index) =>
- ({
- typ: slot.typ,
- }) as SlotTemplate,
- );
-
- const connections = Object.entries(this.dbDevice?.conn ?? {}).map(
- ([index, conn]) =>
- [index, connectionFromDeviceDBConnection(conn)] as const,
- );
- connections.sort((a, b) => {
- if (a[0] < b[0]) {
- return -1;
- } else if (a[0] > b[0]) {
- return 1;
- } else {
- return 0;
- }
- });
-
- this.connections = connections.map((conn) => conn[1]);
- }
-
- connectedCallback(): void {
- super.connectedCallback();
- window.VM.get().then((vm) =>
- vm.addEventListener(
- "vm-device-db-loaded",
- this._handleDeviceDBLoad.bind(this),
- ),
- );
- }
-
- _handleDeviceDBLoad(e: CustomEvent) {
- this.deviceDB = e.detail;
- }
-
- renderFields(): HTMLTemplateResult {
- const fields = Object.entries(this.fields);
- return html`
- ${fields.map(([name, field], _index, _fields) => {
- return html`
-
- ${name}
- ${field.field_type}
-
- `;
- })}
- `;
- }
-
- _handleChangeField(e: CustomEvent) {
- const input = e.target as SlInput;
- const field = input.getAttribute("key")! as LogicType;
- const val = parseNumber(input.value);
- this.fields[field].value = val;
- if (field === "ReferenceId" && val !== 0) {
- this.device_id = val;
- }
- this.requestUpdate();
- }
-
- renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
- return html` `;
- }
-
- renderSlots(): HTMLTemplateResult {
- return html``;
- }
-
- renderReagents(): HTMLTemplateResult {
- return html``;
- }
-
- renderNetworks() {
- const vm = window.VM.vm;
- const vmNetworks = vm.networks;
- const connections = this.connections;
- return html`
-
- ${connections.map((connection, index, _conns) => {
- const conn =
- typeof connection === "object" ? connection.CableNetwork : null;
- return html`
-
- Connection:${index}
- ${vmNetworks.map(
- (net) =>
- html`Network ${net}`,
- )}
- ${conn?.typ}
-
- `;
- })}
-
- `;
- }
-
- _handleChangeConnection(e: CustomEvent) {
- const select = e.target as SlSelect;
- const conn = parseInt(select.getAttribute("key")!);
- const val = select.value ? parseInt(select.value as string) : undefined;
- (this.connections[conn] as ConnectionCableNetwork).CableNetwork.net = val;
- this.requestUpdate();
- }
-
- renderPins(): HTMLTemplateResult {
- const device = this.deviceDB.db[this.prefab_name];
- return html``;
- }
-
- render() {
- const device = this.dbDevice;
- return html`
-
-
-
-
- Fields
- Slots
-
- Networks
-
-
- ${this.renderFields()}
- ${this.renderSlots()}
-
- ${this.renderNetworks()}
-
-
-
-
- `;
- }
- _handleAddButtonClick() {
- this.dispatchEvent(
- new CustomEvent("add-device-template", { bubbles: true }),
- );
- const template: DeviceTemplate = {
- id: this.device_id,
- name: this.device_name,
- prefab_name: this.prefab_name,
- slots: this.slots,
- connections: this.connections,
- fields: this.fields,
- };
- window.VM.vm.addDeviceFromTemplate(template);
-
- // reset state for new device
- this.setupState();
- }
-}
diff --git a/www/src/ts/virtual_machine/device/add_device.ts b/www/src/ts/virtual_machine/device/add_device.ts
new file mode 100644
index 0000000..e1f294e
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/add_device.ts
@@ -0,0 +1,305 @@
+
+import { html, css } from "lit";
+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 SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
+import type { DeviceDBEntry } from "virtual_machine/device_db";
+import { repeat } from "lit/directives/repeat.js";
+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 { VMDeviceDBMixin } from "virtual_machine/base_device";
+
+
+@customElement("vm-add-device-button")
+export class VMAddDeviceButton extends VMDeviceDBMixin(BaseElement) {
+ static styles = [
+ ...defaultCss,
+ css`
+ .add-device-drawer {
+ --size: 36rem;
+ --footer-spacing: var(--sl-spacing-small);
+ }
+
+ .card {
+ margin-top: var(--sl-spacing-small);
+ margin-right: var(--sl-spacing-small);
+ }
+ `,
+ ];
+
+ @query("sl-drawer") drawer: SlDrawer;
+ @query(".device-search-input") searchInput: SlInput;
+
+ private _structures: Map = new Map();
+ private _datapoints: [string, string][] = [];
+ private _haystack: string[] = [];
+
+ postDBSetUpdate(): void {
+ this._structures = new Map(
+ Object.values(this.deviceDB.db)
+ .filter((entry) => this.deviceDB.structures.includes(entry.name), this)
+ .filter(
+ (entry) => this.deviceDB.logic_enabled.includes(entry.name),
+ this,
+ )
+ .map((entry) => [entry.name, entry]),
+ );
+
+ const datapoints: [string, string][] = [];
+ for (const entry of this._structures.values()) {
+ datapoints.push(
+ [entry.title, entry.name],
+ [entry.name, entry.name],
+ [entry.desc, entry.name],
+ );
+ }
+ const haystack: string[] = datapoints.map((data) => data[0]);
+ this._datapoints = datapoints;
+ this._haystack = haystack;
+ this.performSearch();
+ }
+
+ private _filter: string = "";
+
+ get filter() {
+ return this._filter;
+ }
+
+ @state()
+ set filter(val: string) {
+ this._filter = val;
+ this.page = 0;
+ this.performSearch();
+ }
+
+ private _searchResults: {
+ entry: DeviceDBEntry;
+ haystackEntry: string;
+ ranges: number[];
+ }[] = [];
+
+ private filterTimeout: number | undefined;
+
+ performSearch() {
+ if (this._filter) {
+ const uf = new uFuzzy({});
+ const [_idxs, info, order] = uf.search(
+ this._haystack,
+ this._filter,
+ 0,
+ 1e3,
+ );
+
+ const filtered = order?.map((infoIdx) => ({
+ name: this._datapoints[info.idx[infoIdx]][1],
+ haystackEntry: this._haystack[info.idx[infoIdx]],
+ ranges: info.ranges[infoIdx],
+ }));
+
+ const unique = [...new Set(filtered.map((obj) => obj.name))].map(
+ (result) => {
+ return filtered.find((obj) => obj.name === result);
+ },
+ );
+
+ 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.title,
+ ranges: [],
+ }));
+ }
+ }
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ window.VM.get().then((vm) =>
+ vm.addEventListener(
+ "vm-device-db-loaded",
+ this._handleDeviceDBLoad.bind(this),
+ ),
+ );
+ }
+
+ _handleDeviceDBLoad(e: CustomEvent) {
+ this.deviceDB = e.detail;
+ }
+
+ @state() private page = 0;
+
+ renderSearchResults() {
+ const perPage = 40;
+ const totalPages = Math.ceil((this._searchResults?.length ?? 0) / perPage);
+ let pageKeys = Array.from({ length: totalPages }, (_, index) => index);
+ const extra: {
+ entry: { title: string; name: string };
+ haystackEntry: string;
+ ranges: number[];
+ }[] = [];
+ if (this.page < totalPages - 1) {
+ extra.push({
+ entry: { title: "", name: this.filter },
+ haystackEntry: "...",
+ ranges: [],
+ });
+ }
+ return when(
+ typeof this._searchResults !== "undefined" &&
+ this._searchResults.length < 20,
+ () =>
+ repeat(
+ this._searchResults ?? [],
+ (result) => result.entry.name,
+ (result) =>
+ cache(html`
+
+
+ `),
+ ),
+ () => html`
+
+
+
+
+ results, filter more to get cards
+
+
+ Page:
+ ${pageKeys.map(
+ (key, index) => html`
+ ${key + 1}${index < totalPages - 1 ? "," : ""}
+ `,
+ )}
+
+
+
+ ${[
+ ...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.name;
+ return html`
+
+ ${result.entry.title} (
+ ${ranges.length
+ ? unsafeHTML(uFuzzy.highlight(hay, ranges))
+ : hay} )
+
+ `;
+ })}
+
+
+ `,
+ );
+ }
+
+ _handlePageChange(e: Event) {
+ const span = e.currentTarget as HTMLSpanElement;
+ const key = parseInt(span.getAttribute("key"));
+ this.page = key;
+ }
+
+ _handleHaystackClick(e: Event) {
+ const div = e.currentTarget as HTMLDivElement;
+ const key = div.getAttribute("key");
+ if (key === this.filter) {
+ this.page += 1;
+ } else {
+ this.filter = key;
+ this.searchInput.value = key;
+ }
+ }
+
+ _handleDeviceAdd() {
+ this.drawer.hide();
+ }
+
+ render() {
+ return html`
+
+ Add Device
+
+
+
+ Search Structures
+
+
+
+ ${this.renderSearchResults()}
+
+ {
+ this.drawer.hide();
+ }}
+ >
+ Close
+
+
+ `;
+ }
+
+ _handleSearchInput(e: CustomEvent) {
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ const that = this;
+ this.filterTimeout = setTimeout(() => {
+ that.filter = that.searchInput.value;
+ that.filterTimeout = undefined;
+ }, 200);
+ }
+
+ _handleAddButtonClick() {
+ this.drawer.show();
+ this.searchInput.select();
+ }
+}
diff --git a/www/src/ts/virtual_machine/device/card.ts b/www/src/ts/virtual_machine/device/card.ts
new file mode 100644
index 0000000..c66a636
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/card.ts
@@ -0,0 +1,409 @@
+import { html, css, HTMLTemplateResult } from "lit";
+import { customElement, property, query, state } from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
+import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
+import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
+import { parseIntWithHexOrBinary, parseNumber } 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";
+import "./fields";
+import { until } from "lit/directives/until.js";
+import { repeat } from "lit/directives/repeat.js";
+
+export type CardTab = "fields" | "slots" | "reagents" | "networks" | "pins";
+
+@customElement("vm-device-card")
+export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
+ image_err: boolean;
+
+ @property({ type: Boolean }) open: boolean;
+
+ constructor() {
+ super();
+ this.open = false;
+ this.subscribe(
+ "prefabName",
+ "name",
+ "nameHash",
+ "reagents",
+ "slots-count",
+ "reagents",
+ "connections",
+ "active-ic",
+ );
+ }
+
+ static styles = [
+ ...defaultCss,
+ css`
+ :host {
+ display: block;
+ box-sizing: border-box;
+ }
+ .card {
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .image {
+ width: 4rem;
+ height: 4rem;
+ }
+ .header {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ }
+ .header-name {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ flex-grow: 1;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+ .device-card {
+ --padding: var(--sl-spacing-small);
+ }
+ .device-name::part(input) {
+ width: 10rem;
+ }
+ .device-id::part(input) {
+ width: 7rem;
+ }
+ .device-name-hash::part(input) {
+ width: 7rem;
+ }
+ sl-divider {
+ --spacing: 0.25rem;
+ }
+ sl-button[variant="success"] {
+ /* Changes the success theme color to purple using primitives */
+ --sl-color-success-600: var(--sl-color-purple-700);
+ }
+ sl-button[variant="primary"] {
+ /* Changes the success theme color to purple using primitives */
+ --sl-color-primary-600: var(--sl-color-cyan-600);
+ }
+ sl-button[variant="warning"] {
+ /* Changes the success theme color to purple using primitives */
+ --sl-color-warning-600: var(--sl-color-amber-600);
+ }
+ sl-tab-group {
+ margin-left: 1rem;
+ margin-right: 1rem;
+ --indicator-color: var(--sl-color-purple-600);
+ --sl-color-primary-600: var(--sl-color-purple-600);
+ }
+ sl-tab::part(base) {
+ padding: var(--sl-spacing-small) var(--sl-spacing-medium);
+ }
+ sl-tab-group::part(base) {
+ max-height: 30rem;
+ overflow-y: auto;
+ }
+ sl-icon-button.remove-button::part(base) {
+ color: var(--sl-color-danger-600);
+ }
+ sl-icon-button.remove-button::part(base):hover,
+ sl-icon-button.remove-button::part(base):focus {
+ color: var(--sl-color-danger-500);
+ }
+ sl-icon-button.remove-button::part(base):active {
+ color: var(--sl-color-danger-600);
+ }
+ .remove-dialog-body {
+ display: flex;
+ flex-direction: row;
+ }
+ .dialog-image {
+ width: 3rem;
+ height: 3rem;
+ }
+ `,
+ ];
+
+ _handleDeviceDBLoad(e: CustomEvent): void {
+ super._handleDeviceDBLoad(e);
+ this.updateDevice();
+ }
+
+ onImageErr(e: Event) {
+ this.image_err = true;
+ console.log("Image load error", e);
+ }
+
+ renderHeader(): HTMLTemplateResult {
+ const thisIsActiveIc = this.activeICId === this.deviceID;
+ const badges: HTMLTemplateResult[] = [];
+ if (thisIsActiveIc) {
+ badges.push(html`db`);
+ }
+ const activeIc = window.VM.vm.activeIC;
+ activeIc?.pins?.forEach((id, index) => {
+ if (this.deviceID == id) {
+ badges.push(
+ html`d${index}`,
+ );
+ }
+ }, this);
+ return html`
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ renderFields() {
+ return this.delayRenderTab(
+ "fields",
+ html``,
+ );
+ }
+
+ _onSlotImageErr(e: Event) {
+ console.log("image_err", e);
+ }
+
+ static transparentImg =
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
+
+ async renderSlots() {
+ return this.delayRenderTab(
+ "slots",
+ html`
+
+ ${repeat(this.slots,
+ (slot, index) => slot.typ + index.toString(),
+ (_slot, index) => html`
+
+
+ `,
+ )}
+
+ `,
+ );
+ }
+
+ renderReagents() {
+ return this.delayRenderTab("reagents", html``);
+ }
+
+ renderNetworks() {
+ const vmNetworks = window.VM.vm.networks;
+ const networks = this.connections.map((connection, index, _conns) => {
+ const conn =
+ typeof connection === "object" ? connection.CableNetwork : null;
+ return html`
+
+ Connection:${index}
+ ${vmNetworks.map(
+ (net) =>
+ html`Network ${net}`,
+ )}
+ ${conn?.typ}
+
+ `;
+ });
+ return this.delayRenderTab(
+ "networks",
+ html`${networks}
`,
+ );
+ }
+
+ renderPins() {
+ const pins = this.pins;
+ const visibleDevices = window.VM.vm.visibleDevices(this.deviceID);
+ const pinsHtml = pins?.map(
+ (pin, index) =>
+ html`
+
+ d${index}
+ ${visibleDevices.map(
+ (device, _index) =>
+ html`
+
+ Device ${device.id} : ${device.name ?? device.prefabName}
+
+ `,
+ )}
+ `,
+ );
+ return this.delayRenderTab("pins", html`${pinsHtml}
`);
+ }
+
+ private tabsShown: CardTab[] = ["fields"];
+ private tabResolves: {
+ [key in CardTab]: {
+ result?: HTMLTemplateResult;
+ resolver?: (result: HTMLTemplateResult) => void;
+ };
+ } = {
+ fields: {},
+ slots: {},
+ reagents: {},
+ networks: {},
+ pins: {},
+ };
+
+ delayRenderTab(
+ name: CardTab,
+ result: HTMLTemplateResult,
+ ): Promise {
+ this.tabResolves[name].result = result;
+ return new Promise((resolve) => {
+ if (this.tabsShown.includes(name)) {
+ this.tabResolves[name].resolver = undefined;
+ resolve(result);
+ } else {
+ this.tabResolves[name].resolver = resolve;
+ }
+ });
+ }
+
+ resolveTab(name: CardTab) {
+ if (
+ typeof this.tabResolves[name].resolver !== "undefined" &&
+ typeof this.tabResolves[name].result !== "undefined"
+ ) {
+ this.tabResolves[name].resolver(this.tabResolves[name].result);
+ this.tabsShown.push(name);
+ }
+ }
+
+ render(): HTMLTemplateResult {
+ return html`
+
+
+
+ Fields
+ Slots
+ Reagents
+ Networks
+ Pins
+
+
+ ${until(this.renderFields(), html``)}
+
+
+ ${until(this.renderSlots(), html``)}
+
+
+ ${until(this.renderReagents(), html``)}
+
+
+ ${until(this.renderNetworks(), html``)}
+
+ ${this.renderPins()}
+
+
+
+
+

+
+
Are you sure you want to remove this device?
+
Id ${this.deviceID} : ${this.name ?? this.prefabName}
+
+
+
+ Close
+ Remove
+
+
+ `;
+ }
+
+ _handleTabChange(e: CustomEvent<{ name: string }>) {
+ setTimeout(() => this.resolveTab(e.detail.name as CardTab), 100);
+ }
+
+ @query(".remove-device-dialog") removeDialog: SlDialog;
+
+ _preventOverlayClose(event: CustomEvent) {
+ if (event.detail.source === "overlay") {
+ event.preventDefault();
+ }
+ }
+
+ _closeRemoveDialog() {
+ this.removeDialog.hide();
+ }
+
+ _handleChangeID(e: CustomEvent) {
+ const input = e.target as SlInput;
+ const val = parseIntWithHexOrBinary(input.value);
+ if (!isNaN(val)) {
+ window.VM.get().then((vm) => {
+ if (!vm.changeDeviceID(this.deviceID, val)) {
+ input.value = this.deviceID.toString();
+ }
+ });
+ } else {
+ input.value = this.deviceID.toString();
+ }
+ }
+
+ _handleChangeName(e: CustomEvent) {
+ const input = e.target as SlInput;
+ const name = input.value.length === 0 ? undefined : input.value;
+ window.VM.get().then((vm) => {
+ if (!vm.setDeviceName(this.deviceID, name)) {
+ input.value = this.name;
+ }
+ this.updateDevice();
+ });
+ }
+ _handleDeviceRemoveButton(_e: Event) {
+ this.removeDialog.show();
+ }
+
+ _removeDialogRemove() {
+ this.removeDialog.hide();
+ window.VM.get().then((vm) => vm.removeDevice(this.deviceID));
+ }
+
+ _handleChangeConnection(e: CustomEvent) {
+ const select = e.target as SlSelect;
+ const conn = parseInt(select.getAttribute("key")!);
+ const val = select.value ? parseInt(select.value as string) : undefined;
+ window.VM.get().then((vm) =>
+ vm.setDeviceConnection(this.deviceID, conn, val),
+ );
+ this.updateDevice();
+ }
+
+ _handleChangePin(e: CustomEvent) {
+ 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.deviceID, pin, val));
+ this.updateDevice();
+ }
+}
diff --git a/www/src/ts/virtual_machine/utils.ts b/www/src/ts/virtual_machine/device/dbutils.ts
similarity index 89%
rename from www/src/ts/virtual_machine/utils.ts
rename to www/src/ts/virtual_machine/device/dbutils.ts
index db81c9d..73b4e87 100644
--- a/www/src/ts/virtual_machine/utils.ts
+++ b/www/src/ts/virtual_machine/device/dbutils.ts
@@ -1,5 +1,5 @@
import { Connection } from "ic10emu_wasm";
-import { DeviceDBConnection } from "./device_db";
+import { DeviceDBConnection } from "../device_db";
const CableNetworkTypes: readonly string[] = Object.freeze(["Power", "Data", "PowerAndData"]);
export function connectionFromDeviceDBConnection(conn: DeviceDBConnection): Connection {
diff --git a/www/src/ts/virtual_machine/device/device_list.ts b/www/src/ts/virtual_machine/device/device_list.ts
new file mode 100644
index 0000000..04e0f64
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/device_list.ts
@@ -0,0 +1,177 @@
+import { html, css, HTMLTemplateResult, PropertyValueMap } from "lit";
+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 { repeat } from "lit/directives/repeat.js";
+import { default as uFuzzy } from "@leeoniya/ufuzzy";
+import { VMSlotAddDialog } from "./slot_add_dialog";
+import "./add_device"
+import { SlotModifyEvent } from "./slot";
+
+@customElement("vm-device-list")
+export class VMDeviceList extends BaseElement {
+ @state() devices: number[];
+
+ static styles = [
+ ...defaultCss,
+ css`
+ .header {
+ margin-bottom: 1rem;
+ padding: 0.25rem 0.25rem;
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .device-list {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ }
+ .device-list-card {
+ width: 100%;
+ }
+ .device-filter-input {
+ margin-left: auto;
+ }
+ `,
+ ];
+
+ constructor() {
+ super();
+ this.devices = [...window.VM.vm.deviceIds];
+ }
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ window.VM.get().then((vm) =>
+ vm.addEventListener(
+ "vm-devices-update",
+ this._handleDevicesUpdate.bind(this),
+ ),
+ );
+ }
+
+ protected firstUpdated(_changedProperties: PropertyValueMap | Map): void {
+ this.renderRoot.querySelector(".device-list").addEventListener(
+ "device-modify-slot",
+ this._showDeviceSlotDialog.bind(this),
+ );
+ }
+
+ _handleDevicesUpdate(e: CustomEvent) {
+ const ids = e.detail;
+ if (!structuralEqual(this.devices, ids)) {
+ this.devices = ids;
+ this.devices.sort();
+ }
+ }
+
+ protected render(): HTMLTemplateResult {
+ const deviceCards = repeat(
+ this.filteredDeviceIds,
+ (id) => id,
+ (id) =>
+ html`
+ `,
+ );
+ const result = html`
+
+ ${deviceCards}
+
+ `;
+
+ return result;
+ }
+
+ @query("vm-slot-add-dialog") slotDialog: VMSlotAddDialog;
+
+ _showDeviceSlotDialog(
+ e: CustomEvent,
+ ) {
+ this.slotDialog.show(e.detail.deviceID, e.detail.slotIndex);
+ }
+
+ get filteredDeviceIds() {
+ if (typeof this._filteredDeviceIds !== "undefined") {
+ return this._filteredDeviceIds;
+ } else {
+ return this.devices;
+ }
+ }
+
+ private _filteredDeviceIds: number[] | undefined;
+ private _filter: string = "";
+
+ @query(".device-filter-input") filterInput: SlInput;
+ get filter() {
+ return this._filter;
+ }
+
+ @state()
+ set filter(val: string) {
+ this._filter = val;
+ this.performSearch();
+ }
+
+ private filterTimeout: number | undefined;
+
+ _handleFilterInput(_e: CustomEvent) {
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ const that = this;
+ this.filterTimeout = setTimeout(() => {
+ that.filter = that.filterInput.value;
+ that.filterTimeout = undefined;
+ }, 500);
+ }
+
+ performSearch() {
+ if (this._filter) {
+ const datapoints: [string, number][] = [];
+ for (const device_id of this.devices) {
+ const device = window.VM.vm.devices.get(device_id);
+ if (device) {
+ if (typeof device.name !== "undefined") {
+ datapoints.push([device.name, device.id]);
+ }
+ if (typeof device.prefabName !== "undefined") {
+ datapoints.push([device.prefabName, device.id]);
+ }
+ }
+ }
+ const haystack: string[] = datapoints.map((data) => data[0]);
+ const uf = new uFuzzy({});
+ const [_idxs, info, order] = uf.search(haystack, this._filter, 0, 1e3);
+
+ const filtered = order?.map((infoIdx) => datapoints[info.idx[infoIdx]]);
+ const deviceIds: number[] =
+ filtered
+ ?.map((data) => data[1])
+ ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
+ this._filteredDeviceIds = deviceIds;
+ } else {
+ this._filteredDeviceIds = undefined;
+ }
+ }
+}
+
diff --git a/www/src/ts/virtual_machine/device/fields.ts b/www/src/ts/virtual_machine/device/fields.ts
new file mode 100644
index 0000000..e45c245
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/fields.ts
@@ -0,0 +1,42 @@
+import { html, css } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
+import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
+import { displayNumber, parseNumber } from "utils";
+import type { LogicType } from "ic10emu_wasm";
+import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
+
+@customElement("vm-device-fields")
+export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
+ constructor() {
+ super();
+ this.subscribe("fields");
+ }
+
+ render() {
+ const fields = Array.from(this.fields.entries());
+ const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
+ return html`
+ ${fields.map(([name, field], _index, _fields) => {
+ return html`
+ ${name}
+
+ ${field.field_type}
+ `;
+ })}
+ `;
+ }
+
+ _handleChangeField(e: CustomEvent) {
+ const input = e.target as SlInput;
+ const field = input.getAttribute("key")! as LogicType;
+ const val = parseNumber(input.value);
+ window.VM.get().then((vm) => {
+ if (!vm.setDeviceField(this.deviceID, field, val, true)) {
+ input.value = this.fields.get(field).value.toString();
+ }
+ this.updateDevice();
+ });
+ }
+}
diff --git a/www/src/ts/virtual_machine/device/index.ts b/www/src/ts/virtual_machine/device/index.ts
new file mode 100644
index 0000000..ef53140
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/index.ts
@@ -0,0 +1,15 @@
+import "./template"
+import "./card"
+import "./device_list"
+import "./add_device"
+import "./slot_add_dialog"
+import "./slot"
+
+import { VmDeviceTemplate } from "./template";
+import { VMDeviceCard } from "./card";
+import { VMDeviceList } from "./device_list";
+import { VMAddDeviceButton } from "./add_device";
+import { VMSlotAddDialog } from "./slot_add_dialog";
+
+export { VMDeviceCard, VmDeviceTemplate, VMDeviceList, VMAddDeviceButton, VMSlotAddDialog };
+
diff --git a/www/src/ts/virtual_machine/device/slot.ts b/www/src/ts/virtual_machine/device/slot.ts
new file mode 100644
index 0000000..d976a95
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/slot.ts
@@ -0,0 +1,299 @@
+import { html, css } from "lit";
+import { customElement, property} from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
+import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
+import {
+ clamp,
+ displayNumber,
+ parseNumber,
+} from "utils";
+import {
+ SlotLogicType,
+ SlotType,
+} from "ic10emu_wasm";
+import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
+import { VMDeviceCard } from "./card";
+import { when } from "lit/directives/when.js";
+
+export interface SlotModifyEvent {
+ deviceID: number;
+ slotIndex: number;
+}
+
+@customElement("vm-device-slot")
+export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
+ private _slotIndex: number;
+
+ get slotIndex() {
+ return this._slotIndex;
+ }
+
+ @property({ type: Number })
+ set slotIndex(val: number) {
+ this._slotIndex = val;
+ this.unsubscribe((sub) => typeof sub === "object" && "slot" in sub);
+ this.subscribe({ slot: val });
+ }
+
+
+ constructor() {
+ super();
+ this.subscribe("active-ic", "prefabName");
+ }
+
+ static styles = [
+ ...defaultCss,
+ css`
+ .slot-card {
+ --padding: var(--sl-spacing-x-small);
+ }
+ .slot-card::part(header) {
+ padding: var(--sl-spacing-x-small);
+ }
+ .slot-card::part(base) {
+ background-color: var(--sl-color-neutral-50);
+ }
+ .quantity-input sl-input::part(input) {
+ width: 3rem;
+ }
+ .clear-occupant::part(base) {
+ color: var(--sl-color-warning-500);
+ }
+ .clear-occupant::part(base):hover,
+ .clear-occupant::part(base):focus {
+ color: var(--sl-color-warning-400);
+ }
+ .clear-occupant::part(base):active {
+ color: var(--sl-color-warning-500);
+ }
+ `,
+ ];
+
+ slotOccupantImg(): string {
+ const slot = this.slots[this.slotIndex];
+ if (typeof slot.occupant !== "undefined") {
+ const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
+ const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash";
+ return `img/stationpedia/${prefabName}.png`;
+ } else {
+ return `img/stationpedia/SlotIcon_${slot.typ}.png`;
+ }
+ }
+
+ slotOccupantPrefabName(): string {
+ const slot = this.slots[this.slotIndex];
+ if (typeof slot.occupant !== "undefined") {
+ const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
+ const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash";
+ return prefabName;
+ } else {
+ return undefined;
+ }
+ }
+
+ slotOcccupantTemplate(): { name: string; typ: SlotType } | undefined {
+ if (this.deviceDB) {
+ const entry = this.deviceDB.db[this.prefabName];
+ return entry?.slots[this.slotIndex];
+ } else {
+ return undefined;
+ }
+ }
+
+ renderHeader() {
+ const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Head`;
+ const slot = this.slots[this.slotIndex];
+ const slotImg = this.slotOccupantImg();
+ const img = html`
`;
+ const template = this.slotOcccupantTemplate();
+
+ const thisIsActiveIc = this.activeICId === this.deviceID;
+
+ const enableQuantityInput = false;
+
+ return html`
+
+
+
+ ${this.slotIndex}
+
+
+ ${img}
+
+ ${when(
+ typeof slot.occupant !== "undefined",
+ () =>
+ html`
+ ${slot.occupant.quantity}/${slot.occupant
+ .max_quantity}
+
`,
+ )}
+
+
+
+
+ ${when(
+ typeof slot.occupant !== "undefined",
+ () => html` ${this.slotOccupantPrefabName()} `,
+ () => html` ${template?.name} `,
+ )}
+
+
+
+ ${when(
+ typeof slot.occupant !== "undefined",
+ () => html`
+
+ `,
+ () => html``,
+ )}
+
+ `;
+ }
+
+ _handleSlotOccupantRemove() {
+ window.VM.vm.removeDeviceSlotOccupant(this.deviceID, this.slotIndex);
+ }
+
+ _handleSlotClick(_e: Event) {
+ this.dispatchEvent(
+ new CustomEvent("device-modify-slot", {
+ bubbles: true,
+ composed: true,
+ detail: { deviceID: this.deviceID, slotIndex: this.slotIndex },
+ }),
+ );
+ }
+
+ _handleSlotQuantityChange(e: Event) {
+ const input = e.currentTarget as SlInput;
+ const slot = this.slots[this.slotIndex];
+ const val = clamp(input.valueAsNumber, 1, slot.occupant.max_quantity);
+ if (
+ !window.VM.vm.setDeviceSlotField(
+ this.deviceID,
+ this.slotIndex,
+ "Quantity",
+ val,
+ true,
+ )
+ ) {
+ input.value = this.device
+ .getSlotField(this.slotIndex, "Quantity")
+ .toString();
+ }
+ }
+
+ renderFields() {
+ const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Field`;
+ const _fields = this.device.getSlotFields(this.slotIndex);
+ const fields = Array.from(_fields.entries());
+
+ return html`
+
+ ${fields.map(
+ ([name, field], _index, _fields) => html`
+
+ ${name}
+
+ ${field.field_type}
+
+ `,
+ )}
+
+ `;
+ }
+
+ _handleChangeSlotField(e: CustomEvent) {
+ const input = e.target as SlInput;
+ const field = input.getAttribute("key")! as SlotLogicType;
+ let val = parseNumber(input.value);
+ if (field === "Quantity") {
+ const slot = this.slots[this.slotIndex];
+ val = clamp(input.valueAsNumber, 1, slot.occupant.max_quantity);
+ }
+ window.VM.get().then((vm) => {
+ if (
+ !vm.setDeviceSlotField(this.deviceID, this.slotIndex, field, val, true)
+ ) {
+ input.value = this.device
+ .getSlotField(this.slotIndex, field)
+ .toString();
+ }
+ this.updateDevice();
+ });
+ }
+
+ render() {
+ return html`
+
+
+ ${this.renderFields()}
+
+ `;
+ }
+
+}
diff --git a/www/src/ts/virtual_machine/device/slot_add_dialog.ts b/www/src/ts/virtual_machine/device/slot_add_dialog.ts
new file mode 100644
index 0000000..ecb744f
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/slot_add_dialog.ts
@@ -0,0 +1,261 @@
+import { html, css } from "lit";
+import { customElement, property, query, state } from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
+import { VMDeviceDBMixin } from "virtual_machine/base_device";
+import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db";
+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 { LogicField, SlotLogicType, SlotOccupantTemplate } from "ic10emu_wasm";
+
+@customElement("vm-slot-add-dialog")
+export class VMSlotAddDialog extends VMDeviceDBMixin(BaseElement) {
+ static styles = [
+ ...defaultCss,
+ css`
+ .slot-card {
+ --padding: var(--sl-spacing-x-small);
+ }
+ .slot-card::part(header) {
+ padding: var(--sl-spacing-x-small);
+ }
+ .slot-card::part(base) {
+ background-color: var(--sl-color-neutral-50);
+ }
+ .quantity-input sl-input::part(input) {
+ width: 3rem;
+ }
+ `,
+ ];
+
+ private _items: Map = new Map();
+ private _filteredItems: DeviceDBEntry[];
+ private _datapoints: [string, string][] = [];
+ private _haystack: string[] = [];
+
+ private _filter: string = "";
+ get filter() {
+ return this._filter;
+ }
+
+ @state()
+ set filter(val: string) {
+ this._filter = val;
+ this.performSearch();
+ }
+
+ private _searchResults: {
+ entry: DeviceDBEntry;
+ haystackEntry: string;
+ ranges: number[];
+ }[] = [];
+
+ postDBSetUpdate(): void {
+ this._items = new Map(
+ Object.values(this.deviceDB.db)
+ .filter((entry) => this.deviceDB.items.includes(entry.name), this)
+ .map((entry) => [entry.name, entry]),
+ );
+ this.setupSearch();
+ this.performSearch();
+ }
+
+
+ setupSearch() {
+ let filteredItemss = Array.from(this._items.values());
+ if( typeof this.deviceID !== "undefined" && typeof this.slotIndex !== "undefined") {
+ const device = window.VM.vm.devices.get(this.deviceID);
+ const dbDevice = this.deviceDB.db[device.prefabName]
+ const slot = dbDevice.slots[this.slotIndex]
+ const typ = slot.typ;
+
+ if (typeof typ === "string" && typ !== "None") {
+ filteredItemss = Array.from(this._items.values()).filter(item => item.item.slotclass === typ);
+ }
+
+ }
+ this._filteredItems= filteredItemss;
+ const datapoints: [string, string][] = [];
+ for (const entry of this._filteredItems) {
+ datapoints.push(
+ [entry.title, entry.name],
+ [entry.name, entry.name],
+ [entry.desc, entry.name],
+ );
+ }
+
+ const haystack: string[] = datapoints.map((data) => data[0]);
+ this._datapoints = datapoints;
+ this._haystack = haystack;
+ }
+
+ performSearch() {
+ if (this._filter) {
+
+ const uf = new uFuzzy({});
+ const [_idxs, info, order] = uf.search(
+ this._haystack,
+ this._filter,
+ 0,
+ 1e3,
+ );
+
+ const filtered = order?.map((infoIdx) => ({
+ name: this._datapoints[info.idx[infoIdx]][1],
+ haystackEntry: this._haystack[info.idx[infoIdx]],
+ ranges: info.ranges[infoIdx],
+ })) ?? [];
+
+ const uniqueNames = new Set(filtered.map((obj) => obj.name));
+ const unique = [...uniqueNames].map(
+ (result) => {
+ return filtered.find((obj) => obj.name === result);
+ },
+ );
+
+ this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({
+ entry: this._items.get(name)!,
+ haystackEntry,
+ ranges,
+ }));
+ } else {
+ // return everything
+ this._searchResults = [...this._filteredItems].map((st) => ({
+ entry: st,
+ haystackEntry: st.title,
+ ranges: [],
+ }));
+ }
+ }
+
+ renderSearchResults() {
+ const enableNone = false;
+ const none = html`
+
+ None
+
+ `;
+ return html`
+
+ ${enableNone ? none : ""}
+ ${this._searchResults.map((result) => {
+ const imgSrc = `img/stationpedia/${result.entry.name}.png`;
+ const img = html`
+

+ `;
+ return html`
+
+ ${img}
+
${result.entry.title}
+
+ `;
+ })}
+
+ `;
+ }
+
+ _handleClickNone() {
+ window.VM.vm.removeDeviceSlotOccupant(this.deviceID, this.slotIndex);
+ this.hide();
+ }
+
+ _handleClickItem(e: Event) {
+ const div = e.currentTarget as HTMLDivElement;
+ const key = div.getAttribute("key");
+ const entry = this.deviceDB.db[key];
+ const device = window.VM.vm.devices.get(this.deviceID);
+ const dbDevice = this.deviceDB.db[device.prefabName]
+ const sorting = this.deviceDB.enums["SortingClass"][entry.item.sorting ?? "Default"] ?? 0;
+ console.log("using entry", dbDevice);
+ const fields: { [key in SlotLogicType]?: LogicField } = Object.fromEntries(
+ Object.entries(dbDevice.slotlogic[this.slotIndex] ?? {})
+ .map(([slt_s, field_type]) => {
+ let slt = slt_s as SlotLogicType;
+ let value = 0.0
+ if (slt === "FilterType") {
+ value = this.deviceDB.enums["GasType"][entry.item.filtertype]
+ }
+ const field: LogicField = { field_type, value};
+ return [slt, field];
+ })
+ );
+ fields["PrefabHash"] = { field_type: "Read", value: entry.hash };
+ fields["MaxQuantity"] = { field_type: "Read", value: entry.item.maxquantity ?? 1.0 };
+ fields["SortingClass"] = { field_type: "Read", value: sorting };
+ fields["Quantity"] = { field_type: "Read", value: 1 };
+
+ const template: SlotOccupantTemplate = {
+ fields
+ }
+ window.VM.vm.setDeviceSlotOccupant(this.deviceID, this.slotIndex, template);
+ this.hide();
+ }
+
+ @query("sl-dialog.slot-add-dialog") dialog: SlDialog;
+ @query(".device-search-input") searchInput: SlInput;
+
+ render() {
+ const device = window.VM.vm.devices.get(this.deviceID);
+ const name = device?.name ?? device?.prefabName ?? "";
+ const id = this.deviceID ?? 0;
+ return html`
+
+
+ Search Items
+
+
+ ${when(
+ typeof this.deviceID !== "undefined" &&
+ typeof this.slotIndex !== "undefined",
+ () => html`
+
+ ${this.renderSearchResults()}
+
+ `,
+ () => html``,
+ )}
+
+ `;
+ }
+
+ private filterTimeout: number | undefined;
+
+ _handleSearchInput(_e: CustomEvent) {
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ const that = this;
+ this.filterTimeout = setTimeout(() => {
+ that.filter = that.searchInput.value;
+ that.filterTimeout = undefined;
+ }, 200);
+ }
+
+ _handleDialogHide() {
+ this.deviceID = undefined;
+ this.slotIndex = undefined;
+ }
+
+ @state() private deviceID: number;
+ @state() private slotIndex: number;
+
+ show(deviceID: number, slotIndex: number) {
+ this.deviceID = deviceID;
+ this.slotIndex = slotIndex;
+ this.setupSearch();
+ this.performSearch();
+ this.dialog.show();
+ this.searchInput.select();
+ }
+
+ hide() {
+ this.dialog.hide();
+ }
+}
diff --git a/www/src/ts/virtual_machine/device/template.ts b/www/src/ts/virtual_machine/device/template.ts
new file mode 100644
index 0000000..711cb31
--- /dev/null
+++ b/www/src/ts/virtual_machine/device/template.ts
@@ -0,0 +1,267 @@
+import type {
+ Connection,
+ DeviceTemplate,
+ LogicField,
+ LogicType,
+ Slot,
+ SlotTemplate,
+ ConnectionCableNetwork,
+} from "ic10emu_wasm";
+import { html, css, HTMLTemplateResult } from "lit";
+import { customElement, property, query, state } from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
+
+import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db";
+import { connectionFromDeviceDBConnection } from "./dbutils";
+import { displayNumber, parseNumber } 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 { VMDeviceDBMixin } from "virtual_machine/base_device";
+
+@customElement("vm-device-template")
+export class VmDeviceTemplate extends VMDeviceDBMixin(BaseElement) {
+
+ static styles = [
+ ...defaultCss,
+ css`
+ .template-card {
+ --padding: var(--sl-spacing-small);
+ }
+ .image {
+ width: 3rem;
+ height: 3rem;
+ }
+ .header {
+ display: flex;
+ flex-direction: row;
+ }
+ .card-body {
+ // height: 18rem;
+ overflow-y: auto;
+ }
+ sl-tab-group {
+ --indicator-color: var(--sl-color-purple-600);
+ --sl-color-primary-600: var(--sl-color-purple-600);
+ }
+ sl-tab::part(base) {
+ padding: var(--sl-spacing-small) var(--sl-spacing-medium);
+ }
+ sl-tab-group::part(base) {
+ height: 18rem;
+ overflow-y: auto;
+ }
+ `,
+ ];
+
+ @state() fields: { [key in LogicType]?: LogicField };
+ @state() slots: SlotTemplate[];
+ @state() template: DeviceTemplate;
+ @state() device_id: number | undefined;
+ @state() device_name: string | undefined;
+ @state() connections: Connection[];
+
+ constructor() {
+ super();
+ this.deviceDB = window.VM.vm.db;
+ }
+
+ private _prefab_name: string;
+
+ get prefab_name(): string {
+ return this._prefab_name;
+ }
+
+ @property({ type: String })
+ set prefab_name(val: string) {
+ this._prefab_name = val;
+ this.setupState();
+ }
+
+ get dbDevice(): DeviceDBEntry {
+ return this.deviceDB.db[this.prefab_name];
+ }
+
+ setupState() {
+
+ this.fields = Object.fromEntries(
+ Object.entries(this.dbDevice?.logic ?? {}).map(([lt, ft]) => {
+ const value = lt === "PrefabHash" ? this.dbDevice.hash : 0.0;
+ return [lt, { field_type: ft, value } as LogicField];
+ }),
+ );
+
+ this.slots = (this.dbDevice?.slots ?? []).map(
+ (slot, _index) =>
+ ({
+ typ: slot.typ,
+ }) as SlotTemplate,
+ );
+
+ const connections = Object.entries(this.dbDevice?.conn ?? {}).map(
+ ([index, conn]) =>
+ [index, connectionFromDeviceDBConnection(conn)] as const,
+ );
+ connections.sort((a, b) => {
+ if (a[0] < b[0]) {
+ return -1;
+ } else if (a[0] > b[0]) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ this.connections = connections.map((conn) => conn[1]);
+ }
+ renderFields(): HTMLTemplateResult {
+ const fields = Object.entries(this.fields);
+ return html`
+ ${fields.map(([name, field], _index, _fields) => {
+ return html`
+
+ ${name}
+ ${field.field_type}
+
+ `;
+ })}
+ `;
+ }
+
+ _handleChangeField(e: CustomEvent) {
+ const input = e.target as SlInput;
+ const field = input.getAttribute("key")! as LogicType;
+ const val = parseNumber(input.value);
+ this.fields[field].value = val;
+ if (field === "ReferenceId" && val !== 0) {
+ this.device_id = val;
+ }
+ this.requestUpdate();
+ }
+
+ renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
+ return html` `;
+ }
+
+ renderSlots(): HTMLTemplateResult {
+ return html``;
+ }
+
+ renderReagents(): HTMLTemplateResult {
+ return html``;
+ }
+
+ renderNetworks() {
+ const vm = window.VM.vm;
+ const vmNetworks = vm.networks;
+ const connections = this.connections;
+ return html`
+
+ ${connections.map((connection, index, _conns) => {
+ const conn =
+ typeof connection === "object" ? connection.CableNetwork : null;
+ return html`
+
+ Connection:${index}
+ ${vmNetworks.map(
+ (net) =>
+ html`Network ${net}`,
+ )}
+ ${conn?.typ}
+
+ `;
+ })}
+
+ `;
+ }
+
+ _handleChangeConnection(e: CustomEvent) {
+ const select = e.target as SlSelect;
+ const conn = parseInt(select.getAttribute("key")!);
+ const val = select.value ? parseInt(select.value as string) : undefined;
+ (this.connections[conn] as ConnectionCableNetwork).CableNetwork.net = val;
+ this.requestUpdate();
+ }
+
+ renderPins(): HTMLTemplateResult {
+ const device = this.deviceDB.db[this.prefab_name];
+ return html``;
+ }
+
+ render() {
+ const device = this.dbDevice;
+ return html`
+
+
+
+
+ Fields
+ Slots
+
+ Networks
+
+
+ ${this.renderFields()}
+ ${this.renderSlots()}
+
+ ${this.renderNetworks()}
+
+
+
+
+ `;
+ }
+ _handleAddButtonClick() {
+ this.dispatchEvent(
+ new CustomEvent("add-device-template", { bubbles: true }),
+ );
+ const template: DeviceTemplate = {
+ id: this.device_id,
+ name: this.device_name,
+ prefab_name: this.prefab_name,
+ slots: this.slots,
+ connections: this.connections,
+ fields: this.fields,
+ };
+ window.VM.vm.addDeviceFromTemplate(template);
+
+ // reset state for new device
+ this.setupState();
+ }
+}
diff --git a/www/src/ts/virtual_machine/device_db.ts b/www/src/ts/virtual_machine/device_db.ts
index 2bc0269..e72ae1c 100644
--- a/www/src/ts/virtual_machine/device_db.ts
+++ b/www/src/ts/virtual_machine/device_db.ts
@@ -1,4 +1,14 @@
-import { LogicType, SlotLogicType, SortingClass, SlotType, FieldType, ReagentMode, BatchMode, ConnectionType, ConnectionRole } from "ic10emu_wasm";
+import {
+ LogicType,
+ SlotLogicType,
+ SortingClass,
+ SlotType,
+ FieldType,
+ ReagentMode,
+ BatchMode,
+ ConnectionType,
+ ConnectionRole,
+} from "ic10emu_wasm";
export interface DeviceDBItem {
slotclass: SlotType;
sorting: SortingClass;
@@ -6,7 +16,7 @@ export interface DeviceDBItem {
filtertype?: string;
consumable?: boolean;
ingredient?: boolean;
- reagents?: { [key: string]: number};
+ reagents?: { [key: string]: number };
}
export interface DeviceDBDevice {
@@ -22,6 +32,22 @@ export interface DeviceDBConnection {
name: string;
}
+export interface DeviceDBInstruction {
+ typ: string;
+ value: number;
+ desc: string;
+}
+
+export interface DeviceDBMemory {
+ size: number;
+ sizeDisplay: string;
+ access: MemoryAccess
+ instructions?: { [key: string]: DeviceDBInstruction };
+}
+
+export type MemoryAccess = "Read" | "Write" | "ReadWrite" | "None";
+
+
export interface DeviceDBEntry {
name: string;
hash: number;
@@ -29,12 +55,15 @@ export interface DeviceDBEntry {
desc: string;
slots?: { name: string; typ: SlotType }[];
logic?: { [key in LogicType]?: FieldType };
- slotlogic?: { [key in SlotLogicType]?: number[] };
+ slotlogic?: { [key: number]: {[key in SlotLogicType]?: FieldType } };
modes?: { [key: number]: string };
- conn?: { [key: number]: DeviceDBConnection };
+ conn?: { [key: number]: DeviceDBConnection }
item?: DeviceDBItem;
device?: DeviceDBDevice;
-};
+ transmitter: boolean;
+ receiver: boolean;
+ memory?: DeviceDBMemory;
+}
export interface DBStates {
activate: boolean;
@@ -45,6 +74,12 @@ export interface DBStates {
open: boolean;
}
+export interface DeviceDBReagent {
+ Hash: number;
+ Unit: string;
+ Sources?: { [key: string]: number };
+}
+
export interface DeviceDB {
logic_enabled: string[];
slot_logic_enabled: string[];
@@ -55,6 +90,6 @@ export interface DeviceDB {
[key: string]: DeviceDBEntry;
};
names_by_hash: { [key: number]: string };
- reagent_hashes: { [key: string]: number}
-};
-
+ reagents: { [key: string]: DeviceDBReagent };
+ enums: { [key: string]: { [key: string]: number } };
+}
diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts
index f87f1fb..8e90d7c 100644
--- a/www/src/ts/virtual_machine/index.ts
+++ b/www/src/ts/virtual_machine/index.ts
@@ -4,13 +4,15 @@ import {
FrozenVM,
LogicType,
SlotLogicType,
+ SlotOccupantTemplate,
+ Slots,
VMRef,
init,
} from "ic10emu_wasm";
import { DeviceDB } from "./device_db";
import "./base_device";
-import { fromJson, toJson } from "../utils";
-import { App } from "../app";
+import "./device";
+import { App } from "app";
export interface ToastMessage {
variant: "warning" | "danger" | "success" | "primary" | "neutral";
icon: string;
@@ -19,9 +21,39 @@ export interface ToastMessage {
id: string;
}
+export interface CacheDeviceRef extends DeviceRef {
+ dirty: boolean;
+}
+
+function cachedDeviceRef(ref: DeviceRef) {
+ let slotsDirty = true;
+ let cachedSlots: Slots = undefined;
+ return new Proxy(ref, {
+ get(target, prop, receiver) {
+ if (prop === "slots") {
+ if (typeof cachedSlots === undefined || slotsDirty) {
+ cachedSlots = target.slots;
+ slotsDirty = false;
+ }
+ return cachedSlots;
+ } else if (prop === "dirty") {
+ return slotsDirty;
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ set(target, prop, value) {
+ if (prop === "dirty") {
+ slotsDirty = value;
+ return true;
+ }
+ return Reflect.set(target, prop, value);
+ },
+ }) as CacheDeviceRef;
+}
+
class VirtualMachine extends EventTarget {
ic10vm: VMRef;
- _devices: Map;
+ _devices: Map;
_ics: Map;
db: DeviceDB;
@@ -92,7 +124,7 @@ class VirtualMachine extends EventTarget {
const device_ids = this.ic10vm.devices;
for (const id of device_ids) {
if (!this._devices.has(id)) {
- this._devices.set(id, this.ic10vm.getDevice(id)!);
+ this._devices.set(id, cachedDeviceRef(this.ic10vm.getDevice(id)!));
update_flag = true;
}
}
@@ -104,6 +136,7 @@ class VirtualMachine extends EventTarget {
}
for (const [id, device] of this._devices) {
+ device.dirty = true;
if (typeof device.ic !== "undefined") {
if (!this._ics.has(id)) {
this._ics.set(id, device);
@@ -203,11 +236,13 @@ class VirtualMachine extends EventTarget {
);
}
}, this);
- this.updateDevice(this.activeIC, save);
+ this.updateDevice(this.activeIC.id, save);
if (save) this.app.session.save();
}
- updateDevice(device: DeviceRef, save: boolean = true) {
+ updateDevice(id: number, save: boolean = true) {
+ const device = this._devices.get(id);
+ device.dirty = true;
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: device.id }),
);
@@ -229,13 +264,22 @@ class VirtualMachine extends EventTarget {
this.dispatchEvent(new CustomEvent("vm-message", { detail: message }));
}
- changeDeviceId(old_id: number, new_id: number): boolean {
+ changeDeviceID(oldID: number, newID: number): boolean {
try {
- this.ic10vm.changeDeviceId(old_id, new_id);
- this.updateDevices();
- if (this.app.session.activeIC === old_id) {
- this.app.session.activeIC = new_id;
+ this.ic10vm.changeDeviceId(oldID, newID);
+ if (this.app.session.activeIC === oldID) {
+ this.app.session.activeIC = newID;
}
+ this.updateDevices();
+ this.dispatchEvent(
+ new CustomEvent("vm-device-id-change", {
+ detail: {
+ old: oldID,
+ new: newID,
+ },
+ }),
+ );
+ this.app.session.changeID(oldID, newID);
return true;
} catch (err) {
this.handleVmError(err);
@@ -247,7 +291,7 @@ class VirtualMachine extends EventTarget {
const ic = this.activeIC!;
try {
ic.setRegister(index, val);
- this.updateDevice(ic);
+ this.updateDevice(ic.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -259,7 +303,7 @@ class VirtualMachine extends EventTarget {
const ic = this.activeIC!;
try {
ic!.setStack(addr, val);
- this.updateDevice(ic);
+ this.updateDevice(ic.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -295,7 +339,7 @@ class VirtualMachine extends EventTarget {
if (device) {
try {
device.setField(field, val, force);
- this.updateDevice(device);
+ this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -315,8 +359,8 @@ class VirtualMachine extends EventTarget {
const device = this._devices.get(id);
if (device) {
try {
- device.setSlotField(slot, field, val, false);
- this.updateDevice(device);
+ device.setSlotField(slot, field, val, force);
+ this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -334,7 +378,7 @@ class VirtualMachine extends EventTarget {
if (typeof device !== "undefined") {
try {
this.ic10vm.setDeviceConnection(id, conn, val);
- this.updateDevice(device);
+ this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -348,7 +392,7 @@ class VirtualMachine extends EventTarget {
if (typeof device !== "undefined") {
try {
this.ic10vm.setPin(id, pin, val);
- this.updateDevice(device);
+ this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -369,7 +413,7 @@ class VirtualMachine extends EventTarget {
try {
console.log("adding device", template);
const id = this.ic10vm.addDeviceFromTemplate(template);
- this._devices.set(id, this.ic10vm.getDevice(id)!);
+ this._devices.set(id, cachedDeviceRef(this.ic10vm.getDevice(id)!));
const device_ids = this.ic10vm.devices;
this.dispatchEvent(
new CustomEvent("vm-devices-update", {
@@ -395,6 +439,39 @@ class VirtualMachine extends EventTarget {
}
}
+ setDeviceSlotOccupant(
+ id: number,
+ index: number,
+ template: SlotOccupantTemplate,
+ ): boolean {
+ const device = this._devices.get(id);
+ if (typeof device !== "undefined") {
+ try {
+ console.log("setting slot occupant", template);
+ this.ic10vm.setSlotOccupant(id, index, template);
+ this.updateDevice(device.id);
+ return true;
+ } catch (err) {
+ this.handleVmError(err);
+ }
+ }
+ return false;
+ }
+
+ removeDeviceSlotOccupant(id: number, index: number): boolean {
+ const device = this._devices.get(id);
+ if (typeof device !== "undefined") {
+ try {
+ this.ic10vm.removeSlotOccupant(id, index);
+ this.updateDevice(device.id);
+ return true;
+ } catch (err) {
+ this.handleVmError(err);
+ }
+ }
+ return false;
+ }
+
saveVMState(): FrozenVM {
return this.ic10vm.saveVMState();
}
diff --git a/www/src/ts/virtual_machine/registers.ts b/www/src/ts/virtual_machine/registers.ts
index fe1e320..6abae1b 100644
--- a/www/src/ts/virtual_machine/registers.ts
+++ b/www/src/ts/virtual_machine/registers.ts
@@ -1,15 +1,11 @@
import { html, css } from "lit";
import { customElement } from "lit/decorators.js";
-import { BaseElement, defaultCss } from "../components";
-import { VMActiveICMixin } from "./base_device";
+import { BaseElement, defaultCss } from "components";
+import { VMActiveICMixin } from "virtual_machine/base_device";
-import "@shoelace-style/shoelace/dist/components/card/card.js";
-import "@shoelace-style/shoelace/dist/components/icon/icon.js";
-import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
-import "@shoelace-style/shoelace/dist/components/input/input.js";
import { RegisterSpec } from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
-import { displayNumber, parseNumber } from "../utils";
+import { displayNumber, parseNumber } from "utils";
@customElement("vm-ic-registers")
export class VMICRegisters extends VMActiveICMixin(BaseElement) {
@@ -44,6 +40,7 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
constructor() {
super();
+ this.subscribe("ic", "active-ic")
}
protected render() {
diff --git a/www/src/ts/virtual_machine/stack.ts b/www/src/ts/virtual_machine/stack.ts
index 7d1083e..5d72b85 100644
--- a/www/src/ts/virtual_machine/stack.ts
+++ b/www/src/ts/virtual_machine/stack.ts
@@ -1,14 +1,10 @@
import { html, css } from "lit";
import { customElement } from "lit/decorators.js";
-import { BaseElement, defaultCss } from "../components";
-import { VMActiveICMixin } from "./base_device";
+import { BaseElement, defaultCss } from "components";
+import { VMActiveICMixin } from "virtual_machine/base_device";
-import "@shoelace-style/shoelace/dist/components/card/card.js";
-import "@shoelace-style/shoelace/dist/components/icon/icon.js";
-import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
-import "@shoelace-style/shoelace/dist/components/input/input.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
-import { displayNumber, parseNumber } from "../utils";
+import { displayNumber, parseNumber } from "utils";
@customElement("vm-ic-stack")
export class VMICStack extends VMActiveICMixin(BaseElement) {
@@ -41,6 +37,7 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
constructor() {
super();
+ this.subscribe("ic", "active-ic")
}
protected render() {
diff --git a/www/src/ts/virtual_machine/ui.ts b/www/src/ts/virtual_machine/ui.ts
index 9b1f3a9..1c1b729 100644
--- a/www/src/ts/virtual_machine/ui.ts
+++ b/www/src/ts/virtual_machine/ui.ts
@@ -1,11 +1,6 @@
-import { HTMLTemplateResult, html, css } from "lit";
-import { customElement, property, query, state } from "lit/decorators.js";
-import { BaseElement, defaultCss } from "../components";
-import "@shoelace-style/shoelace/dist/components/details/details.js";
-import "@shoelace-style/shoelace/dist/components/tab/tab.js";
-import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
-import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
-import "@shoelace-style/shoelace/dist/components/alert/alert.js";
+import { html, css } from "lit";
+import { customElement } from "lit/decorators.js";
+import { BaseElement, defaultCss } from "components";
import "./controls";
import "./registers";
diff --git a/www/stationpedia.py b/www/stationpedia.py
index fd1093c..536d456 100644
--- a/www/stationpedia.py
+++ b/www/stationpedia.py
@@ -5,6 +5,11 @@ from pathlib import Path
from pprint import pprint
from typing import Any, NotRequired, TypedDict # type: ignore[Any]
+try:
+ import markdown
+except ImportError:
+ markdown = None
+
class SlotInsert(TypedDict):
SlotIndex: str
@@ -40,6 +45,24 @@ class PediaPageDevice(TypedDict):
DevicesLength: NotRequired[int]
+class MemoryInstruction(TypedDict):
+ Type: str
+ Value: int
+ Description: str
+
+
+class PediaPageMemory(TypedDict):
+ MemorySize: int
+ MemorySizeReadable: str
+ MemoryAccess: str
+ Instructions: dict[str, MemoryInstruction] | None
+
+
+class PediaPageLogicInfo(TypedDict):
+ LogicSlotTypes: dict[str, dict[str, str]]
+ LogicTypes: dict[str, str]
+
+
class PediaPage(TypedDict):
Key: str
Title: str
@@ -51,13 +74,30 @@ class PediaPage(TypedDict):
LogicSlotInsert: list[LInsert]
ModeInsert: list[LInsert]
ConnectionInsert: list[LInsert]
- Device: NotRequired[PediaPageDevice]
+ LogicInfo: PediaPageLogicInfo | None
Item: NotRequired[PediaPageItem]
+ Device: NotRequired[PediaPageDevice]
+ WirelessLogic: bool | None
+ Memory: PediaPageMemory | None
+ TransmissionReceiver: bool | None
+
+
+class ScriptCommand(TypedDict):
+ desc: str
+ example: str
+
+
+class PediaReagent(TypedDict):
+ Hash: int
+ Unit: str
+ Sources: dict[str, float] | None
class Pedia(TypedDict):
pages: list[PediaPage]
reagents: dict[str, int]
+ scriptCommands: dict[str, ScriptCommand]
+
class DBSlot(TypedDict):
name: str
@@ -96,6 +136,19 @@ class DBPageItem(TypedDict):
reagents: NotRequired[dict[str, float]]
+class DBPageMemoryInstruction(TypedDict):
+ typ: str
+ value: int
+ desc: str
+
+
+class DBPageMemory(TypedDict):
+ size: int
+ sizeDisplay: str
+ access: str
+ instructions: dict[str, DBPageMemoryInstruction] | None
+
+
class DBPage(TypedDict):
name: str
hash: int
@@ -103,22 +156,92 @@ class DBPage(TypedDict):
desc: str
slots: list[DBSlot] | None
logic: dict[str, str] | None
- slotlogic: dict[str, list[int]] | None
+ slotlogic: dict[str, dict[str, str]] | None
modes: dict[int, str] | None
conn: dict[int, DBPageConnection] | None
item: NotRequired[DBPageItem]
device: NotRequired[DBPageDevice]
+ transmitter: bool
+ receiver: bool
+ memory: DBPageMemory | None
+
+
+translation_regex = re.compile(r"")
+translation_keys: set[str] = set()
+translation_codes: set[str] = set()
+
+
+def replace_translation(m: re.Match[str]) -> str:
+ match m.groups():
+ case (code, key):
+ translation_keys.add(key)
+ translation_codes.add(code)
+ return key
+ case _ as g:
+ print("bad translation match?", g, m.string)
+ return m.string
+
+
+def trans(s: str) -> str:
+ return re.sub(translation_regex, replace_translation, s)
+
+
+color_regex = re.compile(
+ r"((:?(?!).)+?)", re.DOTALL
+)
+link_regex = re.compile(r"(.+?)")
+
+
+def strip_color(s: str) -> str:
+ replacemnt = r"\2"
+ last = s
+ new = color_regex.sub(replacemnt, last)
+ while new != last:
+ last = new
+ new = color_regex.sub(replacemnt, last)
+ return new
+
+
+def color_to_html(s: str) -> str:
+ replacemnt = r"""\2
"""
+ last = s
+ new = color_regex.sub(replacemnt, last)
+ while new != last:
+ last = new
+ new = color_regex.sub(replacemnt, last)
+ return new
+
+
+def strip_link(s: str) -> str:
+ replacemnt = r"\2"
+ last = s
+ new = link_regex.sub(replacemnt, last)
+ while new != last:
+ last = new
+ new = link_regex.sub(replacemnt, last)
+ return new
def extract_all() -> None:
db: dict[str, DBPage] = {}
- pedia: Pedia = {"pages": [], "reagents": {}}
- linkPat = re.compile(r"(.+?)")
+ pedia: Pedia = {"pages": [], "reagents": {}, "scriptCommands": {}}
with (Path("data") / "Stationpedia.json").open("r") as f:
pedia = json.load(f)
for page in pedia["pages"]:
- item: DBPage = defaultdict(list) # type: ignore[reportAssignmentType]
-
+ item: DBPage = {
+ "name": "",
+ "hash": 0,
+ "title": "",
+ "desc": "",
+ "slots": None,
+ "logic": None,
+ "slotlogic": None,
+ "modes": None,
+ "conn": None,
+ "transmitter": False,
+ "receiver": False,
+ "memory": None,
+ }
match page:
case {
"Key": _,
@@ -132,7 +255,6 @@ def extract_all() -> None:
"ModeInsert": modes,
"ConnectionInsert": conninsert,
}:
-
connNames = {
int(insert["LogicAccessTypes"]): insert["LogicName"]
for insert in conninsert
@@ -140,10 +262,14 @@ def extract_all() -> None:
device = page.get("Device", None)
item_props = page.get("Item", None)
+ logicinfo = page.get("LogicInfo", None)
+ wireless = page.get("WirelessLogic", False)
+ receiver = page.get("TransmissionReceiver", False)
+ memory = page.get("Memory", None)
item["name"] = name
item["hash"] = name_hash
- item["title"] = title
- item["desc"] = re.sub(linkPat, r"\1", desc)
+ item["title"] = trans(title)
+ item["desc"] = trans(strip_link(strip_color(desc)))
match slots:
case []:
item["slots"] = None
@@ -151,7 +277,7 @@ def extract_all() -> None:
item["slots"] = [{}] * len(slots) # type: ignore[reportAssignmentType]
for slot in slots:
item["slots"][int(slot["SlotIndex"])] = {
- "name": slot["SlotName"],
+ "name": trans(slot["SlotName"]),
"typ": slot["SlotType"],
}
@@ -161,7 +287,7 @@ def extract_all() -> None:
case _:
item["logic"] = {}
for lat in logic:
- item["logic"][re.sub(linkPat, r"\1", lat["LogicName"])] = (
+ item["logic"][strip_link(strip_color(lat["LogicName"]))] = (
lat["LogicAccessTypes"].replace(" ", "")
)
@@ -172,8 +298,8 @@ def extract_all() -> None:
item["slotlogic"] = {}
for slt in slotlogic:
item["slotlogic"][
- re.sub(linkPat, r"\1", slt["LogicName"])
- ] = [int(s) for s in slt["LogicAccessTypes"].split(", ")]
+ strip_link(strip_color(slt["LogicName"]))
+ ] = {s: "Read" for s in slt["LogicAccessTypes"].split(", ")}
match modes:
case []:
@@ -199,7 +325,6 @@ def extract_all() -> None:
"HasActivateState": hasActivateState,
"HasColorState": hasColorState,
}:
-
match connections:
case []:
item["conn"] = None
@@ -239,7 +364,7 @@ def extract_all() -> None:
item["device"] = dbdevice
case _:
- print(f"NON-CONFORMING: ")
+ print("NON-CONFORMING: ")
pprint(device)
return
@@ -288,17 +413,75 @@ def extract_all() -> None:
item["item"] = dbitem
case _:
- print(f"NON-CONFORMING: ")
+ print("NON-CONFORMING: ")
pprint(item_props)
return
+ match logicinfo:
+ case None:
+ pass
+ case _:
+ for lt, access in logicinfo["LogicTypes"].items():
+ if item["logic"] is None:
+ item["logic"] = {}
+ item["logic"][lt] = access
+ for slot, slotlogicinfo in logicinfo["LogicSlotTypes"].items():
+ if item["slotlogic"] is None:
+ item["slotlogic"] = {}
+ if slot not in item["slotlogic"]:
+ item["slotlogic"][slot] = {}
+ for slt, access in slotlogicinfo.items():
+ item["slotlogic"][slot][slt] = access
+
+ if wireless:
+ item["transmitter"] = True
+ if receiver:
+ item["receiver"] = True
+
+ match memory:
+ case None:
+ pass
+ case _:
+ item["memory"] = {
+ "size": memory["MemorySize"],
+ "sizeDisplay": memory["MemorySizeReadable"],
+ "access": memory["MemoryAccess"],
+ "instructions": None,
+ }
+ instructions = memory.get("Instructions", None)
+ match instructions:
+ case None:
+ pass
+ case _:
+
+ def condense_lines(s: str) -> str:
+ return "\r\n".join(
+ [" ".join(line.split()) for line in s.splitlines()]
+ )
+
+ item["memory"]["instructions"] = {
+ inst: {
+ "typ": info["Type"],
+ "value": info["Value"],
+ "desc": condense_lines(
+ strip_color(strip_link(info["Description"]))
+ ),
+ }
+ for inst, info in instructions.items()
+ }
+
case _:
- print(f"NON-CONFORMING: ")
+ print("NON-CONFORMING: ")
pprint(page)
return
db[name] = item
+ print("Translation codes:")
+ pprint(translation_codes)
+ print("Translations keys:")
+ pprint(translation_keys)
+
logicable = [item["name"] for item in db.values() if item["logic"] is not None]
slotlogicable = [
item["name"] for item in db.values() if item["slotlogic"] is not None
@@ -317,11 +500,28 @@ def extract_all() -> None:
return [clean_nones(x) for x in value if x is not None] # type: ignore[unknown]
elif isinstance(value, dict):
return {
- key: clean_nones(val) for key, val in value.items() if val is not None # type: ignore[unknown]
+ key: clean_nones(val)
+ for key, val in value.items() # type:ignore[reportUnknownVariable]
+ if val is not None
}
else:
return value # type: ignore[Any]
+ enums: dict[str, dict[str, int]] = {}
+ with open("data/Enums.json", "r") as f:
+ exported_enums: dict[str, dict[str, int]] = json.load(f)
+ for cat, cat_enums in exported_enums.items():
+ for enum, val in cat_enums.items():
+ key = cat
+ if cat == "Enums":
+ if "." in enum:
+ key, enum = enum.split(".")
+ else :
+ key = "Condition"
+ if key not in enums:
+ enums[key] = {}
+ enums[key][enum] = val
+
with open("data/database.json", "w") as f:
json.dump(
clean_nones(
@@ -335,7 +535,8 @@ def extract_all() -> None:
"names_by_hash": {
page["hash"]: page["name"] for page in db.values()
},
- "reagent_hashes": pedia["reagents"]
+ "reagents": pedia["reagents"],
+ "enums": enums,
}
),
f,
diff --git a/www/tsconfig.json b/www/tsconfig.json
index 2a7db01..34772dd 100644
--- a/www/tsconfig.json
+++ b/www/tsconfig.json
@@ -1,5 +1,7 @@
{
"compilerOptions": {
+ "baseUrl": "./src/ts",
+ "rootDir": "./src/ts",
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index c070801..ab9d2d0 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -19,6 +19,7 @@ struct Args {
}
const PACKAGES: &[&str] = &["ic10lsp_wasm", "ic10emu_wasm"];
+const VALID_VERSION_TYPE: &[&str] = &["patch", "minor", "major"];
#[derive(Debug, Subcommand)]
enum Task {
@@ -41,6 +42,13 @@ enum Task {
Start {},
/// Runs production page under 'www/dist', Run `build` first.
Deploy {},
+ /// bump the cargo.toml and package,json versions
+ Version {
+ #[arg(last = true, default_value = "patch", value_parser = clap::builder::PossibleValuesParser::new(VALID_VERSION_TYPE))]
+ version: String,
+ },
+ /// update changelog
+ Changelog {},
}
#[derive(thiserror::Error)]
@@ -66,6 +74,7 @@ impl std::fmt::Debug for Error {
}
}
+const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
fn main() -> Result<(), Error> {
let args = Args::parse();
let workspace = {
@@ -100,7 +109,7 @@ fn main() -> Result<(), Error> {
cmd.args(["run", "start"]).status().map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
- },
+ }
Task::Deploy {} => {
pnpm_install(&args, &workspace)?;
eprintln!("Production Build");
@@ -109,6 +118,34 @@ fn main() -> Result<(), Error> {
cmd.args(["run", "build"]).status().map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
+ }
+ Task::Version { version } => {
+ let mut cmd = Command::new("cargo");
+ cmd.current_dir(&workspace);
+ cmd.args(["set-version", "--bump", &version])
+ .status()
+ .map_err(|e| {
+ Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
+ })?;
+ let mut cmd = Command::new(&args.manager);
+ cmd.current_dir(&workspace.join("www"));
+ cmd.args(["version", &version]).status().map_err(|e| {
+ Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
+ })?;
+ },
+ Task::Changelog { } => {
+ let mut cmd = Command::new("git-changelog");
+ cmd.current_dir(&workspace);
+ cmd.args([
+ "-io", "CHANGELOG.md",
+ "-t", "path:CHANGELOG.md.jinja",
+ "-c", "conventional",
+ "--bump", VERSION.unwrap_or("auto"),
+ "--parse-refs",
+ "--trailers"
+ ]).status().map_err(|e| {
+ Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
+ })?;
},
}
Ok(())
@@ -160,10 +197,7 @@ fn build + std::fmt::Debug + std::fmt::Display>(
Ok(())
}
-fn pnpm_install(
- args: &Args,
- workspace: &std::path::Path,
-) -> Result {
+fn pnpm_install(args: &Args, workspace: &std::path::Path) -> Result {
eprintln!("Running `pnpm install`");
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));