refactor(vm, frontend): internally tag template enum, finish remap frontend

This commit is contained in:
Rachel Powers
2024-05-31 21:41:09 -07:00
parent 337ca50560
commit d618f7b091
20 changed files with 3059 additions and 451 deletions

176
Cargo.lock generated
View File

@@ -160,28 +160,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
version = "0.64.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
dependencies = [
"bitflags 1.3.2",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
"which",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -232,15 +210,6 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -260,17 +229,6 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "clang-sys"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.4"
@@ -445,16 +403,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -588,12 +536,6 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gloo-utils"
version = "0.1.7"
@@ -643,15 +585,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "httparse"
version = "1.8.0"
@@ -837,34 +770,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.52.5",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -916,12 +827,6 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
@@ -942,16 +847,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -1065,27 +960,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "onig"
version = "6.4.0"
source = "git+https://github.com/rust-onig/rust-onig#fa90c0e97e90a056af89f183b23cd417b59ee6a2"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "git+https://github.com/rust-onig/rust-onig#fa90c0e97e90a056af89f183b23cd417b59ee6a2"
dependencies = [
"bindgen",
"cc",
"pkg-config",
]
[[package]]
name = "owo-colors"
version = "3.5.0"
@@ -1121,12 +995,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1261,12 +1129,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -1387,25 +1249,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.14"
@@ -1557,12 +1400,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -2140,18 +1977,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -2310,7 +2135,6 @@ dependencies = [
"indexmap 2.2.6",
"num",
"num-integer",
"onig",
"phf_codegen",
"prettyplease",
"proc-macro2",

View File

@@ -18,7 +18,3 @@ opt-level = "s"
lto = true
[profile.dev]
opt-level = 1
[patch.crates-io]
onig_sys = { git = "https://github.com/rust-onig/rust-onig", revision = "fa90c0e97e90a056af89f183b23cd417b59ee6a2" }

View File

@@ -7,6 +7,7 @@ use crate::vm::{
},
};
use serde_derive::{Deserialize, Serialize};
use stationeers_data::templates::ObjectTemplate;
use std::error::Error as StdError;
use std::fmt::Display;
use thiserror::Error;
@@ -77,8 +78,8 @@ pub enum TemplateError {
NoTemplateForPrefab(Prefab),
#[error("no prefab provided")]
MissingPrefab,
#[error("incorrect template for concreet impl {0} from prefab {1}")]
IncorrectTemplate(String, Prefab),
#[error("incorrect template for concrete impl {0} from prefab {1}: {2:?}")]
IncorrectTemplate(String, Prefab, ObjectTemplate),
#[error("frozen memory size error: {0} is not {1}")]
MemorySize(usize, usize)

View File

@@ -46,7 +46,7 @@ pub struct ICInfo {
pub defines: BTreeMap<String, f64>,
pub labels: BTreeMap<String, u32>,
pub state: ICState,
pub yield_instruciton_count: u16,
pub yield_instruction_count: u16,
}
impl Display for ICState {

View File

@@ -45,6 +45,7 @@ pub fn object_from_frozen(
return Err(TemplateError::IncorrectTemplate(
"ItemIntegratedCircuit10".to_string(),
Prefab::Name("ItemIntegratedCircuit10".to_string()),
template,
));
};
@@ -101,7 +102,7 @@ pub fn object_from_frozen(
ic: obj
.circuit
.as_ref()
.map(|circuit| circuit.yield_instruciton_count)
.map(|circuit| circuit.yield_instruction_count)
.unwrap_or(0),
aliases: obj
.circuit

View File

@@ -70,6 +70,7 @@ pub struct ObjectInfo {
pub name: Option<String>,
pub id: Option<ObjectID>,
pub prefab: Option<String>,
pub prefab_hash: Option<i32>,
pub slots: Option<BTreeMap<u32, SlotOccupantInfo>>,
pub damage: Option<f32>,
pub device_pins: Option<BTreeMap<u32, ObjectID>>,
@@ -93,6 +94,7 @@ impl From<&VMObject> for ObjectInfo {
name: Some(obj_ref.get_name().value.clone()),
id: Some(*obj_ref.get_id()),
prefab: Some(obj_ref.get_prefab().value.clone()),
prefab_hash: Some(obj_ref.get_prefab().hash),
slots: None,
damage: None,
device_pins: None,
@@ -114,6 +116,14 @@ impl From<&VMObject> for ObjectInfo {
impl ObjectInfo {
/// Build empty info with a prefab name
pub fn with_prefab(prefab: Prefab) -> Self {
let prefab_hash = match &prefab {
Prefab::Name(name) => name
.parse::<StationpediaPrefab>()
.ok()
.map(|p| p as i32)
.unwrap_or_else(|| const_crc32::crc32(name.as_bytes()) as i32),
Prefab::Hash(hash) => *hash,
};
let prefab_name = match prefab {
Prefab::Name(name) => name,
Prefab::Hash(hash) => StationpediaPrefab::from_repr(hash)
@@ -124,6 +134,7 @@ impl ObjectInfo {
name: None,
id: None,
prefab: Some(prefab_name),
prefab_hash: Some(prefab_hash),
slots: None,
damage: None,
device_pins: None,
@@ -141,7 +152,7 @@ impl ObjectInfo {
}
}
/// update the object info from the relavent implimented interfaces of a dyn object
/// update the object info from the relevant implemented interfaces of a dyn object
/// use `ObjectInterfaces::from_object` with a `&dyn Object` (`&*VMObject.borrow()`)
/// to obtain the interfaces
pub fn update_from_interfaces(&mut self, interfaces: &ObjectInterfaces<'_>) -> &mut Self {
@@ -344,7 +355,7 @@ impl ObjectInfo {
.map(|(key, val)| (key.clone(), *val))
.collect(),
state: circuit.get_state(),
yield_instruciton_count: circuit.get_instructions_since_yield(),
yield_instruction_count: circuit.get_instructions_since_yield(),
});
self
}
@@ -405,6 +416,16 @@ impl FrozenObject {
)
})
.transpose()?
.map_or_else(
|| {
self.obj_info.prefab_hash.as_ref().map(|hash| {
vm.get_template(Prefab::Hash(*hash))
.ok_or(TemplateError::NoTemplateForPrefab(Prefab::Hash(*hash)))
})
},
|template| Some(Ok(template)),
)
.transpose()?
.ok_or(TemplateError::MissingPrefab)
},
|template| Ok(template.clone()),

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ use wasm_bindgen::prelude::*;
#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
#[cfg_attr(feature = "tsify", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[serde(untagged)]
#[serde(tag = "templateType")]
pub enum ObjectTemplate {
Structure(StructureTemplate),
StructureSlots(StructureSlotsTemplate),

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { HTMLTemplateResult, html, css, CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMState } from "session";
import { SessionDB } from "session";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { repeat } from "lit/directives/repeat.js";
@@ -34,21 +34,21 @@ export class SaveDialog extends BaseElement {
`,
];
private _saves: { name: string; date: Date; session: VMState }[];
private _saves: { name: string; date: Date; session: SessionDB.CurrentDBVmState }[];
get saves() {
return this._saves;
}
@state()
set saves(val: { name: string; date: Date; session: VMState }[]) {
set saves(val: { name: string; date: Date; session: SessionDB.CurrentDBVmState }[]) {
this._saves = val;
this.performSearch();
}
@state() mode: SaveDialogMode;
private searchResults: { name: string; date: Date; session: VMState }[];
private searchResults: { name: string; date: Date; session: SessionDB.CurrentDBVmState }[];
constructor() {
super();

View File

@@ -1,4 +1,5 @@
import { VMState } from "../session";
import { ObjectInfo } from "ic10emu_wasm";
import { SessionDB } from "../session";
export const demoCode = `# Highlighting Demo
@@ -63,92 +64,83 @@ j ra
`;
export const demoVMState: VMState = {
export const demoVMState: SessionDB.CurrentDBVmState = {
vm: {
ics: [
objects: [
{
device: 1,
id: 2,
registers: Array(18).fill(0),
ip: 0,
ic: 0,
stack: Array(512).fill(0),
aliases: new Map(),
defines: new Map(),
pins: Array(6).fill(undefined),
state: "Start",
code: demoCode,
},
],
devices: [
{
id: 1,
prefab_name: "StructureCircuitHousing",
slots: [
{
typ: "ProgrammableChip",
occupant: {
id: 2,
fields: {
"PrefabHash": {
field_type: "Read",
value: -744098481,
},
"Quantity":{
field_type: "Read",
value: 1
},
"MaxQuantity": {
field_type: "Read",
value: 1,
},
"SortingClass": {
field_type: "Read",
value: 0,
},
},
},
},
],
connections: [
{
CableNetwork: {
net: 1,
typ: "Data",
},
},
{
CableNetwork: {
net: undefined,
typ: "Power",
},
},
],
fields: {
"PrefabHash": {
field_type: "Read",
value: -128473777,
},
"Setting": {
field_type: "ReadWrite",
value: 0,
},
"RequiredPower": {
field_type: "Read",
value: 0,
}
obj_info: {
id: 1,
prefab: "StructureCircuitHousing",
socketed_ic: 2,
slots: new Map([[0, { id: 2, quantity: 1 }]]),
connections: new Map([[0, 1]]),
// unused, provided to make compiler happy
name: undefined,
prefab_hash: undefined,
compile_errors: undefined,
damage: undefined,
device_pins: undefined,
reagents: undefined,
logic_values: undefined,
slot_logic_values: undefined,
entity: undefined,
visible_devices: undefined,
memory: undefined,
source_code: undefined,
circuit: undefined
},
template: undefined,
database_template: true,
},
{
obj_info: {
id: 2,
prefab: "ItemIntegratedCircuit10",
source_code: demoCode,
memory: new Array(512).fill(0),
circuit: {
instruction_pointer: 0,
yield_instruction_count: 0,
state: "Start",
aliases: new Map(),
defines: new Map(),
labels: new Map(),
registers: new Array(18).fill(0)
},
// unused, provided to make compiler happy
name: undefined,
prefab_hash: undefined,
compile_errors: undefined,
slots: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,
reagents: undefined,
logic_values: undefined,
slot_logic_values: undefined,
entity: undefined,
socketed_ic: undefined,
visible_devices: undefined,
},
template: undefined,
database_template: true
}
],
networks: [
{
id: 1,
devices: [1],
power_only: [],
channels: Array(8).fill(NaN),
},
channels: Array(8).fill(NaN) as [number, number, number, number, number, number, number, number],
}
],
default_network: 1,
program_holders: [2],
circuit_holders: [1],
default_network_key: 1,
wireless_receivers: [],
wireless_transmitters: [],
},
activeIC: 1,
};
}

View File

@@ -1,15 +1,22 @@
import type { ICError, FrozenVM, Class } from "ic10emu_wasm";
import type { ICError, FrozenVM, RegisterSpec, DeviceSpec, LogicType, LogicSlotType, LogicField, Class as SlotType, FrozenCableNetwork, FrozenObject, ObjectInfo, ICState, ObjectID } from "ic10emu_wasm";
import { App } from "./app";
import { openDB, DBSchema } from "idb";
import { fromJson, toJson } from "./utils";
import { openDB, DBSchema, IDBPTransaction, IDBPDatabase } from "idb";
import { TypedEventTarget, crc32, dispatchTypedEvent, fromJson, toJson } from "./utils";
import * as presets from "./presets";
const { demoVMState } = presets;
const LOCAL_DB_VERSION = 1;
export interface SessionEventMap {
"sessions-local-update": CustomEvent,
"session-active-ic": CustomEvent<ObjectID>,
"session-id-change": CustomEvent<{ old: ObjectID, new: ObjectID }>,
"session-errors": CustomEvent<ObjectID[]>,
"session-load": CustomEvent<Session>,
"active-line": CustomEvent<ObjectID>,
}
export class Session extends EventTarget {
export class Session extends TypedEventTarget<SessionEventMap>() {
private _programs: Map<number, string>;
private _errors: Map<number, ICError[]>;
private _activeIC: number;
@@ -51,9 +58,7 @@ export class Session extends EventTarget {
set activeIC(val: number) {
this._activeIC = val;
this.dispatchEvent(
new CustomEvent("session-active-ic", { detail: this.activeIC }),
);
this.dispatchCustomEvent("session-active-ic", this.activeIC);
}
changeID(oldID: number, newID: number) {
@@ -61,11 +66,7 @@ export class Session extends EventTarget {
this.programs.set(newID, this.programs.get(oldID));
this.programs.delete(oldID);
}
this.dispatchEvent(
new CustomEvent("session-id-change", {
detail: { old: oldID, new: newID },
}),
);
this.dispatchCustomEvent("session-id-change", { old: oldID, new: newID })
}
onIDChange(
@@ -108,11 +109,7 @@ export class Session extends EventTarget {
}
_fireOnErrors(ids: number[]) {
this.dispatchEvent(
new CustomEvent("session-errors", {
detail: ids,
}),
);
this.dispatchCustomEvent("session-errors", ids);
}
onErrors(callback: (e: CustomEvent<number[]>) => any) {
@@ -124,11 +121,7 @@ export class Session extends EventTarget {
}
_fireOnLoad() {
this.dispatchEvent(
new CustomEvent("session-load", {
detail: this,
}),
);
this.dispatchCustomEvent("session-load", this);
}
onActiveLine(callback: (e: CustomEvent<number>) => any) {
@@ -136,11 +129,7 @@ export class Session extends EventTarget {
}
_fireOnActiveLine(id: number) {
this.dispatchEvent(
new CustomEvent("active-line", {
detail: id,
}),
);
this.dispatchCustomEvent("active-line", id);
}
save() {
@@ -164,7 +153,7 @@ export class Session extends EventTarget {
}
}
async load(data: VMState | OldPrograms | string) {
async load(data: SessionDB.CurrentDBVmState | OldPrograms | string) {
if (typeof data === "string") {
this._activeIC = 1;
this.app.vm.restoreVMState(demoVMState.vm);
@@ -207,7 +196,7 @@ export class Session extends EventTarget {
this.load(data as OldPrograms);
return;
} else if ("vm" in data && "activeIC" in data) {
this.load(data as VMState);
this.load(data as SessionDB.CurrentDBVmState);
} else {
console.log("Bad session data:", data);
}
@@ -216,40 +205,56 @@ export class Session extends EventTarget {
}
async openIndexDB() {
return await openDB<AppDBSchemaV1>("ic10-vm-sessions", LOCAL_DB_VERSION, {
upgrade(db, oldVersion, newVersion, transaction, event) {
// only db verison currently known is v1
if (oldVersion < 1) {
return await openDB<SessionDB.CurrentDBSchema>("ic10-vm-sessions", SessionDB.LOCAL_DB_VERSION, {
async upgrade(db, oldVersion, newVersion, transaction, event) {
if (oldVersion < SessionDB.DBVersion.V1) {
const sessionStore = db.createObjectStore("sessions");
sessionStore.createIndex("by-date", "date");
sessionStore.createIndex("by-name", "name");
}
if (oldVersion < SessionDB.DBVersion.V2) {
const v1Transaction = transaction as unknown as IDBPTransaction<SessionDB.AppDBSchemaV1>;
const v1SessionStore = v1Transaction.objectStore("sessions");
const v1Sessions = await v1SessionStore.getAll();
const v2SessionStore = db.createObjectStore("sessionsV2");
v2SessionStore.createIndex("by-date", "date");
v2SessionStore.createIndex("by-name", "name");
for (const v1Session of v1Sessions) {
await v2SessionStore.add({
name: v1Session.name,
date: v1Session.date,
version: SessionDB.DBVersion.V2,
session: SessionDB.V2.fromV1State(v1Session.session)
})
}
}
},
});
}
async saveLocal(name: string) {
const state: VMState = {
const state: SessionDB.CurrentDBVmState = {
vm: await (await window.VM.get()).ic10vm.saveVMState(),
activeIC: this.activeIC,
};
const db = await this.openIndexDB();
const transaction = db.transaction(["sessions"], "readwrite");
const sessionStore = transaction.objectStore("sessions");
const transaction = db.transaction([SessionDB.LOCAL_DB_SESSION_STORE], "readwrite");
const sessionStore = transaction.objectStore(SessionDB.LOCAL_DB_SESSION_STORE);
await sessionStore.put(
{
name,
date: new Date(),
version: SessionDB.LOCAL_DB_VERSION,
session: state,
},
name,
);
this.dispatchEvent(new CustomEvent("sessions-local-update"));
this.dispatchCustomEvent("sessions-local-update");
}
async loadFromLocal(name: string) {
const db = await this.openIndexDB();
const save = await db.get("sessions", name);
const save = await db.get(SessionDB.LOCAL_DB_SESSION_STORE, name);
if (typeof save !== "undefined") {
const { session } = save;
this.load(session);
@@ -258,37 +263,323 @@ export class Session extends EventTarget {
async deleteLocalSave(name: string) {
const db = await this.openIndexDB();
const transaction = db.transaction(["sessions"], "readwrite");
const sessionStore = transaction.objectStore("sessions");
const transaction = db.transaction([SessionDB.LOCAL_DB_SESSION_STORE], "readwrite");
const sessionStore = transaction.objectStore(SessionDB.LOCAL_DB_SESSION_STORE);
await sessionStore.delete(name);
this.dispatchEvent(new CustomEvent("sessions-local-update"));
this.dispatchCustomEvent("sessions-local-update");
}
async getLocalSaved() {
const db = await this.openIndexDB();
const sessions = await db.getAll("sessions");
const sessions = await db.getAll(SessionDB.LOCAL_DB_SESSION_STORE);
return sessions;
}
}
export interface VMState {
activeIC: number;
vm: FrozenVM;
export namespace SessionDB {
export namespace V1 {
export interface VMState {
activeIC: number;
vm: FrozenVM;
}
export interface FrozenVM {
ics: FrozenIC[];
devices: DeviceTemplate[];
networks: FrozenNetwork[];
default_network: number;
}
export interface FrozenNetwork {
id: number;
devices: number[];
power_only: number[];
channels: number[];
}
export type RegisterSpec = {
readonly RegisterSpec: {
readonly indirection: number;
readonly target: number;
};
};
export type DeviceSpec = {
readonly DeviceSpec: {
readonly device:
| "Db"
| { readonly Numbered: number }
| {
readonly Indirect: {
readonly indirection: number;
readonly target: number;
};
};
readonly connection: number | undefined;
};
};
export type Alias = RegisterSpec | DeviceSpec;
export type Aliases = Map<string, Alias>;
export type Defines = Map<string, number>;
export type Pins = (number | undefined)[];
export interface SlotOccupantTemplate {
id?: number;
fields: { [key in LogicSlotType]?: LogicField };
}
export interface ConnectionCableNetwork {
CableNetwork: {
net: number | undefined;
typ: string;
};
}
export type Connection = ConnectionCableNetwork | "Other";
export interface SlotTemplate {
typ: SlotType;
occupant?: SlotOccupantTemplate;
}
export interface DeviceTemplate {
id?: number;
name?: string;
prefab_name?: string;
slots: SlotTemplate[];
// reagents: { [key: string]: float}
connections: Connection[];
fields: { [key in LogicType]?: LogicField };
}
export interface FrozenIC {
device: number;
id: number;
registers: number[];
ip: number;
ic: number;
stack: number[];
aliases: Aliases;
defines: Defines;
pins: Pins;
state: string;
code: string;
}
}
export namespace V2 {
export interface VMState {
activeIC: number;
vm: FrozenVM;
}
function objectFromIC(ic: SessionDB.V1.FrozenIC): FrozenObject {
return {
obj_info: {
name: undefined,
id: ic.id,
prefab: "ItemIntegratedCircuit10",
prefab_hash: crc32("ItemIntegratedCircuit10"),
memory: ic.stack,
source_code: ic.code,
compile_errors: undefined,
circuit: {
instruction_pointer: ic.ip,
yield_instruction_count: ic.ic,
state: ic.state as ICState,
aliases: ic.aliases,
defines: ic.defines,
labels: new Map(),
registers: ic.registers,
},
// unused
slots: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,
reagents: undefined,
logic_values: undefined,
slot_logic_values: undefined,
entity: undefined,
socketed_ic: undefined,
visible_devices: undefined,
},
database_template: true,
template: undefined,
}
}
function objectsFromV1Template(template: SessionDB.V1.DeviceTemplate, idFn: () => number, socketedIcFn: (id: number) => number | undefined): FrozenObject[] {
const slotOccupantsPairs = new Map(template.slots.flatMap((slot, index) => {
if (typeof slot.occupant !== "undefined") {
return [
[
index,
[
{
obj_info: {
name: undefined,
id: slot.occupant.id ?? idFn(),
prefab: undefined,
prefab_hash: slot.occupant.fields.PrefabHash?.value,
damage: slot.occupant.fields.Damage?.value,
socketed_ic: undefined,
// unused
memory: undefined,
source_code: undefined,
compile_errors: undefined,
circuit: undefined,
slots: undefined,
device_pins: undefined,
connections: undefined,
reagents: undefined,
logic_values: undefined,
slot_logic_values: undefined,
entity: undefined,
visible_devices: undefined,
},
database_template: true,
template: undefined
},
slot.occupant.fields.Quantity ?? 1
]
]
] as [number, [FrozenObject, number]][];
} else {
return [] as [number, [FrozenObject, number]][];
}
}));
return [
...Array.from(slotOccupantsPairs.entries()).map(([_index, [obj, _quantity]]) => obj),
{
obj_info: {
name: template.name,
id: template.id,
prefab: template.prefab_name,
prefab_hash: undefined,
slots: new Map(
Array.from(slotOccupantsPairs.entries())
.map(([index, [obj, quantity]]) => [index, {
quantity,
id: obj.obj_info.id,
}])
),
socketed_ic: socketedIcFn(template.id),
logic_values: new Map(Object.entries(template.fields).map(([key, val]) => {
return [key as LogicType, val.value]
})),
// unused
memory: undefined,
source_code: undefined,
compile_errors: undefined,
circuit: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,
reagents: undefined,
slot_logic_values: undefined,
entity: undefined,
visible_devices: undefined,
},
database_template: true,
template: undefined,
}
];
}
export function fromV1State(v1State: SessionDB.V1.VMState): VMState {
const highestObjetId = Math.max(...
v1State.vm
.devices
.map(device => device.id ?? -1)
.concat(
v1State.vm
.ics
.map(ic => ic.id ?? -1)
)
);
let nextId = highestObjetId + 1;
const deviceIcs = new Map(v1State.vm.ics.map(ic => [ic.device, objectFromIC(ic)]));
const objects = v1State.vm.devices.flatMap(device => {
return objectsFromV1Template(device, () => nextId++, (id) => deviceIcs.get(id)?.obj_info.id ?? undefined)
})
const vm: FrozenVM = {
objects,
circuit_holders: objects.flatMap(obj => "socketed_ic" in obj.obj_info && typeof obj.obj_info.socketed_ic !== "undefined" ? [obj.obj_info.id] : []),
program_holders: objects.flatMap(obj => "source_code" in obj.obj_info && typeof obj.obj_info.source_code !== "undefined" ? [obj.obj_info.id] : []),
default_network_key: v1State.vm.default_network,
networks: v1State.vm.networks as FrozenCableNetwork[],
wireless_receivers: [],
wireless_transmitters: [],
};
const v2State: VMState = {
activeIC: v1State.activeIC,
vm,
};
return v2State;
}
}
export enum DBVersion {
V1 = 1,
V2 = 2,
}
export const LOCAL_DB_VERSION = DBVersion.V2 as const;
export type CurrentDBSchema = AppDBSchemaV2;
export type CurrentDBVmState = V2.VMState;
export const LOCAL_DB_SESSION_STORE = "sessionsV2" as const
export interface AppDBSchemaV1 extends DBSchema {
sessions: {
key: string;
value: {
name: string;
date: Date;
session: V1.VMState;
};
indexes: {
"by-date": Date;
"by-name": string;
};
};
}
export interface AppDBSchemaV2 extends DBSchema {
sessions: {
key: string;
value: {
name: string;
date: Date;
session: V1.VMState;
};
indexes: {
"by-date": Date;
"by-name": string;
};
};
sessionsV2: {
key: string;
value: {
name: string;
date: Date;
version: DBVersion.V2;
session: V2.VMState;
};
indexes: {
"by-date": Date;
"by-name": string;
};
};
}
}
interface AppDBSchemaV1 extends DBSchema {
sessions: {
key: string;
value: {
name: string;
date: Date;
session: VMState;
};
indexes: {
"by-date": Date;
"by-name": string;
};
};
}
export interface OldPrograms {
programs: [number, string][];

View File

@@ -100,18 +100,18 @@ export function compareMaps(map1: Map<any, any>, map2: Map<any, any>): boolean {
}
for (let [key, val] of map1) {
testVal = map2.get(key);
if((testVal === undefined && !map2.has(key)) || !structuralEqual(val, testVal)) {
if ((testVal === undefined && !map2.has(key)) || !structuralEqual(val, testVal)) {
return false;
}
}
return true;
}
export function compareArrays( array1: any[], array2: any[]): boolean {
export function compareArrays(array1: any[], array2: any[]): boolean {
if (array1.length !== array2.length) {
return false;
}
for ( let i = 0, len = array1.length; i < len; i++) {
for (let i = 0, len = array1.length; i < len; i++) {
if (!structuralEqual(array1[i], array2[i])) {
return false;
}
@@ -127,7 +127,7 @@ export function compareStructuralObjects(a: object, b: object): boolean {
export function structuralEqual(a: any, b: any): boolean {
const aType = typeof a;
const bType = typeof b;
if ( aType !== typeof bType) {
if (aType !== typeof bType) {
return false;
}
if (a instanceof Map && b instanceof Map) {
@@ -315,9 +315,29 @@ export function clamp(val: number, min: number, max: number) {
return Math.min(Math.max(val, min), max);
}
export type TypedEventTarget<EventMap extends object> = {
new (): TypedEventTargetInterface<EventMap>;
};
// export type TypedEventTarget<EventMap extends object> = {
// new(): TypedEventTargetInterface<EventMap>;
// };
type Constructor<T = {}> = new (...args: any[]) => T
export const TypedEventTarget = <EventMap extends object>(
) => {
class TypedEventTargetClass extends EventTarget {
dispatchCustomEvent<K extends keyof EventMap>(
type: K,
data?: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never,
eventInitDict?: EventInit,
): boolean {
return this.dispatchEvent(new CustomEvent(type as string, {
detail: data,
bubbles: eventInitDict?.bubbles,
cancelable: eventInitDict?.cancelable,
composed: eventInitDict?.composed
}))
}
}
return TypedEventTargetClass as Constructor<TypedEventTargetInterface<EventMap>>;
}
interface TypedEventTargetInterface<EventMap> extends EventTarget {
addEventListener<K extends keyof EventMap>(
@@ -336,15 +356,35 @@ interface TypedEventTargetInterface<EventMap> extends EventTarget {
options?: boolean | AddEventListenerOptions,
): void;
/**
* @deprecated This method is untyped because the event name is not present in the event map
*/
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean,
): void;
/**
* @deprecated This method is untyped because the event name is not present in the event map
*/
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean,
): void;
dispatchCustomEvent<K extends keyof EventMap>(
type: K,
data?: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never,
eventInitDict?: EventInit,
): boolean;
}
export function dispatchTypedEvent<EventMap, K extends keyof EventMap>(
target: TypedEventTargetInterface<EventMap>,
type: K,
data: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never
): boolean {
return target.dispatchEvent(new CustomEvent(type as string, { detail: data }))
}

View File

@@ -133,7 +133,7 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
const root = super.connectedCallback();
window.VM.get().then((vm) => {
vm.addEventListener(
"vm-objects-modified",
"vm-object-modified",
this._handleDeviceModified.bind(this),
);
vm.addEventListener(
@@ -156,7 +156,7 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
disconnectedCallback(): void {
window.VM.get().then((vm) => {
vm.removeEventListener(
"vm-objects-modified",
"vm-object-modified",
this._handleDeviceModified.bind(this),
);
vm.removeEventListener(
@@ -467,7 +467,7 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
this.icIP = ip;
}
const opCount =
this.obj.obj_info.circuit?.yield_instruciton_count ?? null;
this.obj.obj_info.circuit?.yield_instruction_count ?? null;
if (this.icOpCount !== opCount) {
this.icOpCount = opCount;
}

View File

@@ -154,7 +154,7 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
<sl-divider></sl-divider>
<div class="vstack">
<span>Errors</span>
${this.errors.map(
${this.errors?.map(
(err) =>
typeof err === "object"
&& "ParseError" in err

View File

@@ -23,18 +23,19 @@ export interface ToastMessage {
id: string;
}
export interface VirtualMachinEventMap {
export interface VirtualMachineEventMap {
"vm-template-db-loaded": CustomEvent<TemplateDatabase>;
"vm-objects-update": CustomEvent<number[]>;
"vm-objects-removed": CustomEvent<number[]>;
"vm-objects-modified": CustomEvent<number>;
"vm-object-modified": CustomEvent<number>;
"vm-run-ic": CustomEvent<number>;
"vm-object-id-change": CustomEvent<{ old: number; new: number }>;
"vm-networks-update": CustomEvent<number[]>;
"vm-networks-removed": CustomEvent<number[]>;
"vm-message": CustomEvent<ToastMessage>;
}
class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEventMap>) {
class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
ic10vm: Comlink.Remote<VMRef>;
templateDBPromise: Promise<TemplateDatabase>;
templateDB: TemplateDatabase;
@@ -51,7 +52,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
constructor(app: App) {
super();
this.app = app;
this.vm_worker = new Worker("./vm_worker.ts");
this.vm_worker = new Worker( new URL("./vm_worker.ts", import.meta.url));
const vm = Comlink.wrap<VMRef>(this.vm_worker);
this.ic10vm = vm;
window.VM.set(this);
@@ -155,17 +156,9 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
if (updateFlag) {
const ids = Array.from(updatedNetworks);
ids.sort();
this.dispatchEvent(
new CustomEvent("vm-networks-update", {
detail: ids,
}),
);
this.dispatchCustomEvent("vm-networks-update", ids);
if (removedNetworks.length > 0) {
this.dispatchEvent(
new CustomEvent("vm-networks-removed", {
detail: removedNetworks,
}),
);
this.dispatchCustomEvent("vm-networks-removed", removedNetworks);
}
this.app.session.save();
}
@@ -240,17 +233,9 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
if (updateFlag) {
const ids = Array.from(updatedObjects);
ids.sort();
this.dispatchEvent(
new CustomEvent("vm-objects-update", {
detail: ids,
}),
);
this.dispatchCustomEvent("vm-objects-update", ids);
if (removedObjects.length > 0) {
this.dispatchEvent(
new CustomEvent("vm-objects-removed", {
detail: removedObjects,
}),
);
this.dispatchCustomEvent("vm-objects-removed", removedObjects);
}
this.app.session.save();
}
@@ -272,9 +257,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
await this.ic10vm.setCodeInvalid(id, progs.get(id)!);
const errors = await this.ic10vm.getCompileErrors(id);
this.app.session.setProgramErrors(id, errors);
this.dispatchEvent(
new CustomEvent("vm-object-modified", { detail: id }),
);
this.dispatchCustomEvent("vm-object-modified", id);
} catch (err) {
this.handleVmError(err);
} finally {
@@ -294,9 +277,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
this.handleVmError(err);
}
this.update();
this.dispatchEvent(
new CustomEvent("vm-run-ic", { detail: this.activeIC!.obj_info.id }),
);
this.dispatchCustomEvent("vm-run-ic", this.activeIC!.obj_info.id);
}
}
@@ -309,9 +290,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
this.handleVmError(err);
}
this.update();
this.dispatchEvent(
new CustomEvent("vm-run-ic", { detail: this.activeIC!.obj_info.id }),
);
this.dispatchCustomEvent("vm-run-ic", this.activeIC!.obj_info.id);
}
}
@@ -345,9 +324,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
this.handleVmError(e);
}
const device = this._objects.get(id);
this.dispatchEvent(
new CustomEvent("vm-object-modified", { detail: device.obj_info.id }),
);
this.dispatchCustomEvent("vm-object-modified", device.obj_info.id);
if (typeof device.obj_info.socketed_ic !== "undefined") {
const ic = this._objects.get(device.obj_info.socketed_ic);
const ip = ic.obj_info.circuit?.instruction_pointer;
@@ -365,7 +342,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
msg: err.message,
id: Date.now().toString(16),
};
this.dispatchEvent(new CustomEvent("vm-message", { detail: message }));
this.dispatchCustomEvent("vm-message", message);
}
// return the data connected oject ids for a network
@@ -380,14 +357,10 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
this.app.session.activeIC = newID;
}
await this.updateObjects();
this.dispatchEvent(
new CustomEvent("vm-object-id-change", {
detail: {
old: oldID,
new: newID,
},
}),
);
this.dispatchCustomEvent("vm-object-id-change", {
old: oldID,
new: newID,
});
this.app.session.changeID(oldID, newID);
return true;
} catch (err) {
@@ -521,9 +494,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
setupTemplateDatabase(db: TemplateDatabase) {
this.templateDB = db;
console.log("Loaded Template Database", this.templateDB);
this.dispatchEvent(
new CustomEvent("vm-template-db-loaded", { detail: this.templateDB }),
);
this.dispatchCustomEvent("vm-template-db-loaded", this.templateDB);
}
async addObjectFrozen(frozen: FrozenObject): Promise<ObjectID | undefined> {
@@ -533,11 +504,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
const refrozen = await this.ic10vm.freezeObject(id);
this._objects.set(id, refrozen);
const device_ids = await this.ic10vm.objects;
this.dispatchEvent(
new CustomEvent("vm-objects-update", {
detail: Array.from(device_ids),
}),
);
this.dispatchCustomEvent("vm-objects-update", Array.from(device_ids));
this.app.session.save();
return id;
} catch (err) {
@@ -555,11 +522,7 @@ class VirtualMachine extends (EventTarget as TypedEventTarget<VirtualMachinEvent
this._objects.set(id, refrozen[index]);
})
const device_ids = await this.ic10vm.objects;
this.dispatchEvent(
new CustomEvent("vm-objects-update", {
detail: Array.from(device_ids),
}),
);
this.dispatchCustomEvent("vm-objects-update", Array.from(device_ids));
this.app.session.save();
return Array.from(ids);
} catch (err) {

View File

@@ -36,6 +36,12 @@ const template_database = new Map(
return [parseInt(hash), prefab_database.prefabs[name]];
}),
);
console.info("Loading Prefab Template Database into VM", template_database);
const start_time = performance.now();
vm.importTemplateDatabase(template_database);
const now = performance.now();
const time_elapsed = (now - start_time) / 1000;
console.log(`Prefab Templat Database loaded in ${time_elapsed} seconds`);
Comlink.expose(vm);

View File

@@ -21,7 +21,6 @@ serde_with = "3.8.1"
textwrap = { version = "0.16.1", default-features = false }
thiserror = "1.0.61"
onig = { git = "https://github.com/rust-onig/rust-onig", revision = "fa90c0e97e90a056af89f183b23cd417b59ee6a2" }
tracing = "0.1.40"
quote = "1.0.36"
prettyplease = "0.2.20"

View File

@@ -205,7 +205,7 @@ pub fn generate_database(
//
// https://regex101.com/r/WFpjHV/1
//
let null_matcher = regex::Regex::new(r#"(?:(?:,?\n)\s*"\w+":\snull)+(,?)"#).unwrap();
let null_matcher = regex::Regex::new(r#"(?:,\n\s*"\w+":\snull)+(,?)|(?:(?:\n)?\s*"\w+":\snull),"#).unwrap();
let json = null_matcher.replace_all(&json, "$1");
write!(&mut database_file, "{json}")?;
database_file.flush()?;

View File

@@ -1,60 +1,28 @@
use onig::{Captures, Regex, RegexOptions, Syntax};
use regex::{Captures, Regex};
pub fn strip_color(s: &str) -> String {
let color_regex = Regex::with_options(
r#"<color=(#?\w+)>((:?(?!<color=(?:#?\w+)>).)+?)</color>"#,
RegexOptions::REGEX_OPTION_MULTILINE | RegexOptions::REGEX_OPTION_CAPTURE_GROUP,
Syntax::default(),
)
.unwrap();
let mut new = s.to_owned();
loop {
new = color_regex.replace_all(&new, |caps: &Captures| caps.at(2).unwrap_or("").to_string());
if !color_regex.is_match(&new) {
break;
}
}
new
let color_re = Regex::new(r"<color=.*?>|</color>").unwrap();
color_re.replace_all(s, "").to_string()
}
#[allow(dead_code)]
pub fn color_to_heml(s: &str) -> String {
let color_regex = Regex::with_options(
r#"<color=(#?\w+)>((:?(?!<color=(?:#?\w+)>).)+?)</color>"#,
RegexOptions::REGEX_OPTION_MULTILINE | RegexOptions::REGEX_OPTION_CAPTURE_GROUP,
Syntax::default(),
)
.unwrap();
let mut new = s.to_owned();
loop {
new = color_regex.replace_all(&new, |caps: &Captures| {
format!(
r#"<div style="color: {};">{}</div>"#,
caps.at(1).unwrap_or(""),
caps.at(2).unwrap_or("")
)
});
if !color_regex.is_match(&new) {
break;
}
}
new
pub fn color_to_html(s: &str) -> String {
// not currently used
// onig regex: r#"<color=(#?\w+)>((:?(?!<color=(?:#?\w+)>).)+?)</color>"#
let color_re_start = Regex::new(r#"<color=(?<color>#?\w+)>"#).unwrap();
let color_re_end = Regex::new("</color>").unwrap();
let start_replaced = color_re_start.replace_all(s, |caps: &Captures| {
format!(
r#"<div style="color: {};">"#,
caps.name("color").unwrap().as_str()
)
});
let replaced = color_re_end.replace_all(&start_replaced, "</div>");
replaced.to_string()
}
#[allow(dead_code)]
pub fn strip_link(s: &str) -> String {
let link_regex = Regex::with_options(
r#"<link=(\w+)>(.+?)</link>"#,
RegexOptions::REGEX_OPTION_MULTILINE | RegexOptions::REGEX_OPTION_CAPTURE_GROUP,
Syntax::default(),
)
.unwrap();
let mut new = s.to_owned();
loop {
new = link_regex.replace_all(&new, |caps: &Captures| caps.at(2).unwrap_or("").to_string());
if !link_regex.is_match(&new) {
break;
}
}
new
let link_re = Regex::new(r"<link=.*?>|</link>").unwrap();
link_re.replace_all(s, "").to_string()
}