Merge pull request #30 from Ryex/slots-inprovment

Slots inprovment
This commit is contained in:
Rachel Powers
2024-04-28 13:54:01 -07:00
committed by GitHub
54 changed files with 31096 additions and 8969 deletions

View File

@@ -10,6 +10,8 @@ permissions:
packages: read
checks: write
statuses: write
issues: write
pull-requests: write
jobs:
build:
@@ -20,7 +22,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: '8.15.7'
version: "8.15.7"
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install wasm-pack
@@ -45,19 +47,32 @@ jobs:
uses: actions/upload-pages-artifact@v3
with:
path: www/dist
- name: Deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v2.0.0
id: netlify-deploy
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: "www/dist"
production-branch: develop
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: Deployed Develop
enable-pull-request-comment: true
enable-commit-comment: false
overwrites-pull-request-comment: true
alias: deploy-preview-${{ github.event.number }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN_SECRET }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_DEPLOY_TO_PROD: true
install_command: "echo Skipping installing the dependencies"
build_command: "echo Skipping building the web files"
build_directory: www/dist
- name: Status check
uses: Sibz/github-status-action@v1.1.1
timeout-minutes: 2
- run: echo netlify URL ${{ steps.netlify-deploy.outputs.deploy-url }}
- name: Update commit status for non-default branches
uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
context: Netlify preview
state: success
target_url: ${{ env.NETLIFY_PREVIEW_URL }}
context: "Netlify"
description: "Preview Deployed"
state: "success"
target_url: ${{ steps.netlify-deploy.outputs.deploy-url }}
sha: ${{github.event.pull_request.head.sha || github.sha}}

View File

@@ -1,8 +1,7 @@
name: netlify deploy-preview
on:
pull_request:
types: ['opened', 'edited', 'synchronize']
types: ["opened", "edited", "synchronize"]
branches:
- develop
- "!main"
@@ -12,6 +11,8 @@ permissions:
packages: read
checks: write
statuses: write
issues: write
pull-requests: write
jobs:
build:
@@ -22,7 +23,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: '8.15.7'
version: "8.15.7"
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install wasm-pack
@@ -47,19 +48,31 @@ jobs:
uses: actions/upload-pages-artifact@v3
with:
path: www/dist
- name: Deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v2.0.0
id: netlify-deploy
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: "www/dist"
production-branch: develop
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: ${{ github.event.pull_request.title }}
enable-pull-request-comment: true
enable-commit-comment: false
overwrites-pull-request-comment: true
alias: deploy-preview-${{ github.event.number }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN_SECRET }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
install_command: "echo Skipping installing the dependencies"
build_command: "echo Skipping building the web files"
build_directory: www/dist
deploy_alias: ${{ env.BRANCH_NAME }}
- name: Status check
uses: Sibz/github-status-action@v1.1.1
timeout-minutes: 2
- run: echo netlify URL ${{ steps.netlify-deploy.outputs.deploy-url }}
- name: Update commit status for non-default branches
uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
context: Netlify preview
state: success
target_url: ${{ env.NETLIFY_PREVIEW_URL }}
context: "Netlify"
description: "Preview Deployed"
state: "success"
target_url: ${{ steps.netlify-deploy.outputs.deploy-url }}
sha: ${{github.event.pull_request.head.sha || github.sha}}

View File

@@ -1,10 +1,41 @@
## v0.2.1
<!-- insertion marker -->
- prevent borrow panics in VM during batch operations
- fix Maximize batch mode
- fix panic in parsing invalid numbers
## [0.2.2] - 2024-04-28
## v0.2.0
### Summary
This update brings with it functional slots in the UI! Add items to Stackers, Sorters, Vending machines etc. and interact with the relevant data.
** Note: This does not mean that chute networks and internal inventory mechanics are simulated
There was also some work done on the device search UI to vastly improve it's performance.
<small>[Compare with v0.2.1](https://github.com/Ryex/ic10emu/compare/v0.2.1...0.2.2)</small>
### Features
- better slot UI ([c87d3f8](https://github.com/Ryex/ic10emu/commit/c87d3f8bd88a64ad421e5999d7a040de205d4e03) by Rachel Powers).
- much better slot occupant card ([1790715](https://github.com/Ryex/ic10emu/commit/17907151b34bb6efdbd4370cd449e21dcc8eed54) by Rachel Powers).
### Bug Fixes
- device id change UI event chain fixed; changing the Active IC's ID no longer breaks the UI ([4ac823a](https://github.com/Ryex/ic10emu/commit/4ac823a1bc9d3b572de713ac59a5aabd5f0ff599) by Rachel Powers).
### Performance Improvements
- performance improvments ([cfa240c](https://github.com/Ryex/ic10emu/commit/cfa240c5794817ce4221cdac8be2e96e320edf5c) by Rachel Powers).
- vastly improve load speed ([6cc2189](https://github.com/Ryex/ic10emu/commit/6cc21899214296f51e93b70a3f9f67c39ba243d3) by Rachel Powers).
- improve slot UI + device search speedup ([eb4463c](https://github.com/Ryex/ic10emu/commit/eb4463c8ab318e8093e93c1ecaac139cf6dbb74d) by Rachel Powers).
## [v0.2.1]
- prevent borrow panics in VM during batch operations
- fix Maximize batch mode
- fix panic in parsing invalid numbers
<small>[Compare with v0.2.0](https://github.com/Ryex/ic10emu/compare/v0.2.0...v0.2.1)</small>
## [v0.2.0]
### Share VM State!
@@ -12,21 +43,21 @@ New in this release is the ability to share the entire VM with you share a link.
Additionally you can now save and load any number of sessions in your browser. Access this functionality from the main menu.
Also! the project has officially moved to https://ic10emu.dev . Old share links *should* redirect, but if not simply copy the fragment (the part of the url starting with the `#` symbol)
Also! the project has officially moved to https://ic10emu.dev . Old share links _should_ redirect, but if not simply copy the fragment (the part of the url starting with the `#` symbol)
#### List of changes
- Move build system from Webpack to [Rsbuild](https://rsbuild.dev/) (way faster build times).
- VM now supports exporting and restoring a frozen state.
- Share links updates to use frozen vm state.
- Save and load sessions from the browser's IndexedDB storage.
- project now includes tailwindcss to make frontend dev easier.
- Changelog dialog to notify users of updates.
- Move build system from Webpack to [Rsbuild](https://rsbuild.dev/) (way faster build times).
- VM now supports exporting and restoring a frozen state.
- Share links updates to use frozen vm state.
- Save and load sessions from the browser's IndexedDB storage.
- project now includes tailwindcss to make frontend dev easier.
- Changelog dialog to notify users of updates.
## v0.1.0
## [v0.1.0]
### **Initial Release**:
IC10emu is released to the public! edit and share your IC10 scripts!
- view and edit stack and registers
- view and edit stack and registers

52
CHANGELOG.md.jinja Normal file
View File

@@ -0,0 +1,52 @@
{#- macro: render_commit -#}
{%- macro render_commit(commit) -%}
- {{ commit.convention.subject|default(commit.subject) }} ([{{ commit.hash|truncate(7, True, '') }}]({{ commit.url }}) by {{ commit.author_name }}).
{%- if commit.text_refs.issues_not_in_subject %} Related issues/PRs: {% for issue in commit.text_refs.issues_not_in_subject -%}
{% if issue.url %}[{{ issue.ref }}]({{ issue.url }}){% else %}{{ issue.ref }}{% endif %}{% if not loop.last %}, {% endif -%}
{%- endfor -%}{%- endif -%}
{%- for trailer_name, trailer_value in commit.trailers.items() -%}
{%- if trailer_value|is_url %} [{{ trailer_name }}]({{ trailer_value }})
{%- else %} {{ trailer_name }}: {{ trailer_value }}{% endif %}
{%- if not loop.last %},{% endif %}
{%- endfor -%}
{%- endmacro -%}
{#- macro: render_section -#}
{%- macro render_section(section) -%}
### {{ section.type or "Misc" }}
{% for commit in section.commits|sort(attribute='author_date',reverse=true)|unique(attribute='subject') -%}
{{ render_commit(commit) }}
{% endfor %}
{%- endmacro -%}
{#- macro: render_version -#}
{%- macro render_version(version) -%}
{%- if version.tag or version.planned_tag -%}
## [{{ version.tag or version.planned_tag }}]{% if version.date %} - {{ version.date }}{% endif %}
<small>[Compare with {{ version.previous_version.tag|default("first commit") }}]({{ version.compare_url }})</small>
{%- else -%}
## Unreleased
<small>[Compare with latest]({{ version.compare_url }})</small>
{%- endif %}
{% for type in changelog.sections %}
{%- if type in version.sections_dict %}
{%- with section = version.sections_dict[type] %}
{{ render_section(section) }}
{%- endwith %}
{%- endif %}
{%- endfor %}
{%- if not (version.tag or version.planned_tag) %}
<!-- insertion marker -->{% endif %}
{% endmacro -%}
{#- template -#}
{%- if not in_place -%}
# Changelog
{% endif %}<!-- insertion marker -->
{% for version in changelog.versions_list -%}
{{ render_version(version) }}
{%- endfor -%}

8
Cargo.lock generated
View File

@@ -560,7 +560,7 @@ dependencies = [
[[package]]
name = "ic10emu"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"const-crc32",
"convert_case",
@@ -580,7 +580,7 @@ dependencies = [
[[package]]
name = "ic10emu_wasm"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"console_error_panic_hook",
"ic10emu",
@@ -617,7 +617,7 @@ dependencies = [
[[package]]
name = "ic10lsp_wasm"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"console_error_panic_hook",
"futures",
@@ -1844,7 +1844,7 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "xtask"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"clap",
"thiserror",

View File

@@ -3,7 +3,7 @@ members = ["ic10lsp_wasm", "ic10emu_wasm", "ic10emu", "xtask"]
resolver = "2"
[workspace.package]
version = "0.2.1"
version = "0.2.2"
edition = "2021"
[profile.release]

View File

@@ -30,7 +30,7 @@ fn write_repr_enum<'a, T: std::io::Write, I, P>(
let additional_strum = if use_phf { "#[strum(use_phf)]\n" } else { "" };
write!(
writer,
"#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, EnumString, AsRefStr, EnumProperty, EnumIter, Serialize, Deserialize)]\n\
"#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, EnumString, AsRefStr, EnumProperty, EnumIter, Serialize, Deserialize)]\n\
{additional_strum}\
pub enum {name} {{\n"
)

View File

@@ -2,11 +2,13 @@
"english": {
"bapz": "Branch to line c if abs(a) <= max(b * abs(a), float.epsilon * 8)",
"bapzal": "Branch to line c if abs(a) <= max(b * abs(a), float.epsilon * 8) and store next line number in ra",
"bnaz": "Branch to line c if abs(a) > max b * abs(a), float.epsilon * 8)",
"bnazal": "Branch to line c if abs(a) > max b * abs(a), float.epsilon * 8) and store next line number in ra",
"bnaz": "Branch to line c if abs(a) > max (b * abs(a), float.epsilon * 8)",
"bnazal": "Branch to line c if abs(a) > max (b * abs(a), float.epsilon * 8) and store next line number in ra",
"brapz": "Relative branch to line c if abs(a) <= max(b * abs(a), float.epsilon * 8)",
"brnaz": "Relative branch to line c if abs(a) > max(b * abs(a), float.epsilon * 8)",
"sapz": "Register = 1 if |a| <= max(b * abs(a), float.epsilon * 8), otherwise 0",
"snaz": "Register = 1 if |a| > max(b * abs(a), float.epsilon), otherwise 0"
"sapz": "Register = 1 if abs(a) <= max(b * abs(a), float.epsilon * 8), otherwise 0",
"snaz": "Register = 1 if abs(a) > max(b * abs(a), float.epsilon), otherwise 0",
"log": "Register = base e log(a) or ln(a)",
"exp": "Register = exp(a) or e^a"
}
}

View File

@@ -37,8 +37,8 @@ bltzal Branch to line b if a < 0 and store next line number in ra
bna Branch to line d if abs(a - b) > max(c * max(abs(a), abs(b)), float.epsilon * 8)
bnaal Branch to line d if abs(a - b) <= max(c * max(abs(a), abs(b)), float.epsilon * 8) and store next line number in ra
bnan Branch to line b if a is not a number (NaN)
bnaz Branch to line c if abs(a) > max b * abs(a), float.epsilon * 8)
bnazal Branch to line c if abs(a) > max b * abs(a), float.epsilon * 8) and store next line number in ra
bnaz Branch to line c if abs(a) > max (b * abs(a), float.epsilon * 8)
bnazal Branch to line c if abs(a) > max (b * abs(a), float.epsilon * 8) and store next line number in ra
bne Branch to line c if a != b
bneal Branch to line c if a != b and store next line number in ra
bnez branch to line b if a != 0
@@ -66,7 +66,7 @@ ceil Register = smallest integer greater than a
cos Returns the cosine of the specified angle (radians)
define Creates a label that will be replaced throughout the program with the provided value.
div Register = a / b
exp Register = exp(a)
exp Register = exp(a) or e^a
floor Register = largest integer less than a
hcf Halt and catch fire
j Jump execution to line a
@@ -79,7 +79,7 @@ lbn Loads LogicType from all output network devices with provided type and name
lbns Loads LogicSlotType from slotIndex from all output network devices with provided type and name hashes using the provide batch mode. Average (0), Sum (1), Minimum (2), Maximum (3). Can use either the word, or the number.
lbs Loads LogicSlotType from slotIndex from all output network devices with provided type hash using the provide batch mode. Average (0), Sum (1), Minimum (2), Maximum (3). Can use either the word, or the number.
ld Loads device LogicType to register by direct ID reference.
log Register = log(a)
log Register = base e log(a) or ln(a)
lr Loads reagent of device's ReagentMode where a hash of the reagent type to check for. ReagentMode can be either Contents (0), Required (1), Recipe (2). Can use either the word, or the number.
ls Loads slot LogicSlotType on device to register.
max Register = max of a or b
@@ -97,7 +97,7 @@ rand Register = a random value x with 0 <= x < 1
round Register = a rounded to nearest integer
s Stores register value to LogicType on device by housing index value.
sap Register = 1 if abs(a - b) <= max(c * max(abs(a), abs(b)), float.epsilon * 8), otherwise 0
sapz Register = 1 if |a| <= max(b * abs(a), float.epsilon * 8), otherwise 0
sapz Register = 1 if abs(a) <= max(b * abs(a), float.epsilon * 8), otherwise 0
sb Stores register value to LogicType on all output network devices with provided type hash.
sbn Stores register value to LogicType on all output network devices with provided type hash and name.
sbs Stores register value to LogicSlotType on all output network devices with provided type hash in the provided slot.
@@ -122,7 +122,7 @@ sltz Register = 1 if a < 0, otherwise 0
sna Register = 1 if abs(a - b) > max(c * max(abs(a), abs(b)), float.epsilon * 8), otherwise 0
snan Register = 1 if a is NaN, otherwise 0
snanz Register = 0 if a is NaN, otherwise 1
snaz Register = 1 if |a| > max(b * abs(a), float.epsilon), otherwise 0
snaz Register = 1 if abs(a) > max(b * abs(a), float.epsilon), otherwise 0
sne Register = 1 if a != b, otherwise 0
snez Register = 1 if a != 0, otherwise 0
sqrt Register = square root of a

View File

@@ -46,6 +46,20 @@ def main():
extract_data(install_path, data_path, lang)
translation_regex = re.compile(r"<N:([A-Z]{2}):(\w+)>")
def replace_translation(m: re.Match[str]) -> str:
match m.groups():
case (_code, key):
return key
case _:
return m.string
def trans(s: str) -> str:
return re.sub(translation_regex, replace_translation, s)
def extract_data(install_path: Path, data_path: Path, language: str):
tree = ET.parse(data_path / f"{language}.xml")
root = tree.getroot()
@@ -74,18 +88,18 @@ def extract_data(install_path: Path, data_path: Path, language: str):
if key is None or value is None:
continue
if match := logic_type.match(key):
enum_help_strings[f"LogicType.{match.group(1)}"] = value
enum_help_strings[f"LogicType.{match.group(1)}"] = trans(value)
logictypes[match.group(1)] = (None, value)
if match := logic_slot_type.match(key):
enum_help_strings[f"LogicSlotType.{match.group(1)}"] = value
enum_help_strings[f"LogicSlotType.{match.group(1)}"] = trans(value)
slotlogictypes[match.group(1)] = (None, value)
if match := color.match(key):
enum_help_strings[f"Color.{match.group(1)}"] = value
enum_help_strings[f"Color.{match.group(1)}"] = trans(value)
if match := script_command.match(key):
if not match.group(1).lower() == "command":
operation_help_strings[f"{match.group(1).lower()}"] = value
operation_help_strings[f"{match.group(1).lower()}"] = trans(value)
if match := script_desc.match(key):
operation_help_strings[f"{match.group(1).lower()}"] = value
operation_help_strings[f"{match.group(1).lower()}"] = trans(value)
op_help_patch_path = Path("data") / "instruction_help_patches.json"
if op_help_patch_path.exists():
@@ -205,8 +219,8 @@ def extract_data(install_path: Path, data_path: Path, language: str):
exported_stationpedia_path = install_path / "Stationpedia" / "Stationpedia.json"
if exported_stationpedia_path.exists():
with exported_stationpedia_path.open(mode="r") as f:
exported: dict[str, list[dict[str, Any]]] = json.load(f) # type:ignore[reportAny]
for page in exported["pages"]: # type:ignore[reportUnknownVariableType]
exported: dict[str, list[dict[str, Any]]] = json.load(f)
for page in exported["pages"]:
stationpedia[page["PrefabHash"]] = (page["PrefabName"], page["Title"])
hashables_path = Path("data") / "stationpedia.txt"

View File

@@ -4,7 +4,7 @@ use crate::{
network::{CableConnectionType, Connection},
vm::VM,
};
use std::{collections::HashMap, ops::Deref};
use std::{collections::BTreeMap, ops::Deref};
use itertools::Itertools;
@@ -32,7 +32,7 @@ pub struct SlotOccupant {
pub max_quantity: u32,
pub sorting_class: SortingClass,
pub damage: f64,
fields: HashMap<SlotLogicType, LogicField>,
fields: BTreeMap<SlotLogicType, LogicField>,
}
impl SlotOccupant {
@@ -71,7 +71,7 @@ impl SlotOccupant {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotOccupantTemplate {
pub id: Option<u32>,
pub fields: HashMap<SlotLogicType, LogicField>,
pub fields: BTreeMap<SlotLogicType, LogicField>,
}
impl SlotOccupant {
@@ -83,7 +83,7 @@ impl SlotOccupant {
max_quantity: 1,
damage: 0.0,
sorting_class: SortingClass::Default,
fields: HashMap::new(),
fields: BTreeMap::new(),
}
}
@@ -106,13 +106,13 @@ impl SlotOccupant {
}
/// chainable constructor
pub fn with_fields(mut self, fields: HashMap<SlotLogicType, LogicField>) -> Self {
pub fn with_fields(mut self, fields: BTreeMap<SlotLogicType, LogicField>) -> Self {
self.fields.extend(fields);
self
}
/// chainable constructor
pub fn get_fields(&self) -> HashMap<SlotLogicType, LogicField> {
pub fn get_fields(&self) -> BTreeMap<SlotLogicType, LogicField> {
let mut copy = self.fields.clone();
copy.insert(
SlotLogicType::PrefabHash,
@@ -152,13 +152,17 @@ impl SlotOccupant {
copy
}
pub fn set_field(
&mut self,
field: SlotLogicType,
val: f64,
force: bool,
) -> Result<(), ICError> {
if let Some(logic) = self.fields.get_mut(&field) {
pub fn set_field(&mut self, typ: SlotLogicType, val: f64, force: bool) -> Result<(), ICError> {
if (typ == SlotLogicType::Quantity) && force {
self.quantity = val as u32;
Ok(())
} else if (typ == SlotLogicType::MaxQuantity) && force {
self.max_quantity = val as u32;
Ok(())
} else if (typ == SlotLogicType::Damage) && force {
self.damage = val;
Ok(())
} else if let Some(logic) = self.fields.get_mut(&typ) {
match logic.field_type {
FieldType::ReadWrite | FieldType::Write => {
logic.value = val;
@@ -169,13 +173,13 @@ impl SlotOccupant {
logic.value = val;
Ok(())
} else {
Err(ICError::ReadOnlyField(field.to_string()))
Err(ICError::ReadOnlyField(typ.to_string()))
}
}
}
} else if force {
self.fields.insert(
field,
typ,
LogicField {
field_type: FieldType::ReadWrite,
value: val,
@@ -183,7 +187,7 @@ impl SlotOccupant {
);
Ok(())
} else {
Err(ICError::ReadOnlyField(field.to_string()))
Err(ICError::ReadOnlyField(typ.to_string()))
}
}
@@ -230,7 +234,7 @@ impl Slot {
}
}
pub fn get_fields(&self) -> HashMap<SlotLogicType, LogicField> {
pub fn get_fields(&self) -> BTreeMap<SlotLogicType, LogicField> {
let mut copy = self
.occupant
.as_ref()
@@ -392,27 +396,20 @@ impl Slot {
}
}
pub fn set_field(
&mut self,
field: SlotLogicType,
val: f64,
force: bool,
) -> Result<(), ICError> {
pub fn set_field(&mut self, typ: SlotLogicType, val: f64, force: bool) -> Result<(), ICError> {
if matches!(
field,
typ,
SlotLogicType::Occupied
| SlotLogicType::OccupantHash
| SlotLogicType::Quantity
| SlotLogicType::MaxQuantity
| SlotLogicType::Class
| SlotLogicType::PrefabHash
| SlotLogicType::SortingClass
| SlotLogicType::ReferenceId
) {
return Err(ICError::ReadOnlyField(field.to_string()));
return Err(ICError::ReadOnlyField(typ.to_string()));
}
if let Some(occupant) = self.occupant.as_mut() {
occupant.set_field(field, val, force)
occupant.set_field(typ, val, force)
} else {
Err(ICError::SlotNotOccupied)
}
@@ -549,10 +546,10 @@ pub struct Device {
pub name_hash: Option<i32>,
pub prefab: Option<Prefab>,
pub slots: Vec<Slot>,
pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
pub reagents: BTreeMap<ReagentMode, BTreeMap<i32, f64>>,
pub ic: Option<u32>,
pub connections: Vec<Connection>,
fields: HashMap<LogicType, LogicField>,
fields: BTreeMap<LogicType, LogicField>,
}
impl Device {
@@ -562,9 +559,9 @@ impl Device {
name: None,
name_hash: None,
prefab: None,
fields: HashMap::new(),
fields: BTreeMap::new(),
slots: Vec::new(),
reagents: HashMap::new(),
reagents: BTreeMap::new(),
ic: None,
connections: vec![Connection::CableNetwork {
net: None,
@@ -620,7 +617,7 @@ impl Device {
device
}
pub fn get_fields(&self, vm: &VM) -> HashMap<LogicType, LogicField> {
pub fn get_fields(&self, vm: &VM) -> BTreeMap<LogicType, LogicField> {
let mut copy = self.fields.clone();
if let Some(ic_id) = &self.ic {
let ic = vm.ics.get(ic_id).expect("our own ic to exist").borrow();
@@ -822,7 +819,7 @@ impl Device {
&self,
index: f64,
vm: &VM,
) -> Result<HashMap<SlotLogicType, LogicField>, ICError> {
) -> Result<BTreeMap<SlotLogicType, LogicField>, ICError> {
let slot = self
.slots
.get(index as usize)
@@ -911,9 +908,9 @@ pub struct DeviceTemplate {
pub name: Option<String>,
pub prefab_name: Option<String>,
pub slots: Vec<SlotTemplate>,
// pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
// pub reagents: BTreeMap<ReagentMode, BTreeMap<i32, f64>>,
pub connections: Vec<Connection>,
pub fields: HashMap<LogicType, LogicField>,
pub fields: BTreeMap<LogicType, LogicField>,
}
impl Device {
@@ -962,7 +959,7 @@ impl Device {
prefab: template.prefab_name.map(|name| Prefab::new(&name)),
slots,
// reagents: template.reagents,
reagents: HashMap::new(),
reagents: BTreeMap::new(),
ic,
connections: template.connections,
fields,

View File

@@ -29,7 +29,7 @@ pub mod generated {
fn try_from(value: f64) -> Result<Self, <LogicType as TryFrom<f64>>::Error> {
if let Some(lt) = LogicType::iter().find(|lt| {
lt.get_str("value")
.map(|val| val.parse::<u8>().unwrap() as f64 == value)
.map(|val| val.parse::<u16>().unwrap() as f64 == value)
.unwrap_or(false)
}) {
Ok(lt)

View File

@@ -2,7 +2,7 @@ use core::f64;
use serde::{Deserialize, Serialize};
use std::{cell::{Cell, RefCell}, ops::Deref, string::ToString};
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, HashSet},
error::Error,
fmt::Display,
u32,
@@ -13,8 +13,7 @@ use itertools::Itertools;
use time::format_description;
use crate::{
grammar::{self, ParseError},
vm::VM,
device::SlotType, grammar::{self, LogicType, ParseError, SlotLogicType}, vm::VM
};
use serde_with::serde_as;
@@ -190,8 +189,8 @@ pub struct IC {
/// Instruction Count since last yield
pub ic: Cell<u16>,
pub stack: RefCell<[f64; 512]>,
pub aliases: RefCell<HashMap<String, grammar::Operand>>,
pub defines: RefCell<HashMap<String, f64>>,
pub aliases: RefCell<BTreeMap<String, grammar::Operand>>,
pub defines: RefCell<BTreeMap<String, f64>>,
pub pins: RefCell<[Option<u32>; 6]>,
pub code: RefCell<String>,
pub program: RefCell<Program>,
@@ -210,8 +209,8 @@ pub struct FrozenIC {
pub ic: u16,
#[serde_as(as = "[_; 512]")]
pub stack: [f64; 512],
pub aliases: HashMap<String, grammar::Operand>,
pub defines: HashMap<String, f64>,
pub aliases: BTreeMap<String, grammar::Operand>,
pub defines: BTreeMap<String, f64>,
pub pins: [Option<u32>; 6],
pub state: ICState,
pub code: String,
@@ -261,7 +260,7 @@ impl From<FrozenIC> for IC {
pub struct Program {
pub instructions: Vec<grammar::Instruction>,
pub errors: Vec<ICError>,
pub labels: HashMap<String, u32>,
pub labels: BTreeMap<String, u32>,
}
impl Default for Program {
@@ -275,14 +274,14 @@ impl Program {
Program {
instructions: Vec::new(),
errors: Vec::new(),
labels: HashMap::new(),
labels: BTreeMap::new(),
}
}
pub fn try_from_code(code: &str) -> Result<Self, ICError> {
let parse_tree = grammar::parse(code)?;
let mut labels_set = HashSet::new();
let mut labels = HashMap::new();
let mut labels = BTreeMap::new();
let errors = Vec::new();
let instructions = parse_tree
.into_iter()
@@ -320,7 +319,7 @@ impl Program {
pub fn from_code_with_invalid(code: &str) -> Self {
let parse_tree = grammar::parse_with_invlaid(code);
let mut labels_set = HashSet::new();
let mut labels = HashMap::new();
let mut labels = BTreeMap::new();
let mut errors = Vec::new();
let instructions = parse_tree
.into_iter()
@@ -380,8 +379,8 @@ impl IC {
pins: RefCell::new([None; 6]),
program: RefCell::new(Program::new()),
code: RefCell::new(String::new()),
aliases: RefCell::new(HashMap::new()),
defines: RefCell::new(HashMap::new()),
aliases: RefCell::new(BTreeMap::new()),
defines: RefCell::new(BTreeMap::new()),
state: RefCell::new(ICState::Start),
}
}
@@ -391,8 +390,8 @@ impl IC {
self.ic.replace(0);
self.registers.replace([0.0; 18]);
self.stack.replace([0.0; 512]);
self.aliases.replace(HashMap::new());
self.defines.replace(HashMap::new());
self.aliases.replace(BTreeMap::new());
self.defines.replace(BTreeMap::new());
self.state.replace(ICState::Start);
}
@@ -522,6 +521,16 @@ impl IC {
}
}
pub fn propgate_line_number(&self, vm: &VM) {
if let Some(device) = vm.devices.get(&self.device) {
let mut device_ref = device.borrow_mut();
let _ = device_ref.set_field(LogicType::LineNumber, self.ip.get() as f64, vm, true);
if let Some(slot) = device_ref.slots.iter_mut().find(|slot| slot.typ == SlotType::ProgrammableChip) {
let _ = slot.set_field(SlotLogicType::LineNumber, self.ip.get() as f64, true);
}
}
}
/// processes one line of the contained program
pub fn step(&self, vm: &VM, advance_ip_on_err: bool) -> Result<bool, LineError> {
// TODO: handle sleep
@@ -2552,6 +2561,7 @@ impl IC {
if result.is_ok() || advance_ip_on_err {
self.ic.set(self.ic.get() + 1);
self.set_ip(next_ip);
self.propgate_line_number(vm);
}
result
}

View File

@@ -1,12 +1,12 @@
use crate::{
device::{Device, DeviceTemplate},
device::{Device, DeviceTemplate, SlotOccupant, SlotOccupantTemplate},
grammar::{BatchMode, LogicType, SlotLogicType},
interpreter::{self, FrozenIC, ICError, LineError},
network::{CableConnectionType, Connection, FrozenNetwork, Network},
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
collections::{BTreeMap, HashSet},
rc::Rc,
};
@@ -40,9 +40,9 @@ pub enum VMError {
#[derive(Debug)]
pub struct VM {
pub ics: HashMap<u32, Rc<RefCell<interpreter::IC>>>,
pub devices: HashMap<u32, Rc<RefCell<Device>>>,
pub networks: HashMap<u32, Rc<RefCell<Network>>>,
pub ics: BTreeMap<u32, Rc<RefCell<interpreter::IC>>>,
pub devices: BTreeMap<u32, Rc<RefCell<Device>>>,
pub networks: BTreeMap<u32, Rc<RefCell<Network>>>,
pub default_network: u32,
id_space: IdSpace,
network_id_space: IdSpace,
@@ -64,12 +64,12 @@ impl VM {
let mut network_id_space = IdSpace::default();
let default_network_key = network_id_space.next();
let default_network = Rc::new(RefCell::new(Network::new(default_network_key)));
let mut networks = HashMap::new();
let mut networks = BTreeMap::new();
networks.insert(default_network_key, default_network);
let mut vm = VM {
ics: HashMap::new(),
devices: HashMap::new(),
ics: BTreeMap::new(),
devices: BTreeMap::new(),
networks,
default_network: default_network_key,
id_space: id_gen,
@@ -285,11 +285,15 @@ impl VM {
device.borrow_mut().id = new_id;
self.devices.insert(new_id, device);
self.ics.iter().for_each(|(_id, ic)| {
ic.borrow().pins.borrow_mut().iter_mut().for_each(|pin| {
let mut ic_ref = ic.borrow_mut();
if ic_ref.device == old_id {
ic_ref.device = new_id;
}
ic_ref.pins.borrow_mut().iter_mut().for_each(|pin| {
if pin.is_some_and(|d| d == old_id) {
pin.replace(new_id);
}
})
});
});
self.networks.iter().for_each(|(_net_id, net)| {
if let Ok(mut net_ref) = net.try_borrow_mut() {
@@ -747,6 +751,52 @@ impl VM {
Ok(())
}
pub fn set_slot_occupant(
&mut self,
id: u32,
index: usize,
template: SlotOccupantTemplate,
) -> Result<(), VMError> {
let Some(device) = self.devices.get(&id) else {
return Err(VMError::UnknownId(id));
};
let mut device_ref = device.borrow_mut();
let slot = device_ref
.slots
.get_mut(index)
.ok_or(ICError::SlotIndexOutOfRange(index as f64))?;
if let Some(id) = template.id.as_ref() {
self.id_space.use_id(*id)?;
}
let occupant = SlotOccupant::from_template(template, || self.id_space.next());
if let Some(last) = slot.occupant.as_ref() {
self.id_space.free_id(last.id);
}
slot.occupant = Some(occupant);
Ok(())
}
pub fn remove_slot_occupant(&mut self, id: u32, index: usize) -> Result<(), VMError> {
let Some(device) = self.devices.get(&id) else {
return Err(VMError::UnknownId(id));
};
let mut device_ref = device.borrow_mut();
let slot = device_ref
.slots
.get_mut(index)
.ok_or(ICError::SlotIndexOutOfRange(index as f64))?;
if let Some(last) = slot.occupant.as_ref() {
self.id_space.free_id(last.id);
}
slot.occupant = None;
Ok(())
}
pub fn save_vm_state(&self) -> FrozenVM {
FrozenVM {
ics: self.ics.values().map(|ic| ic.borrow().into()).collect(),

View File

@@ -3,7 +3,7 @@ mod utils;
mod types;
use ic10emu::{
device::{Device, DeviceTemplate},
device::{Device, DeviceTemplate, SlotOccupantTemplate},
grammar::{LogicType, SlotLogicType},
vm::{FrozenVM, VMError, VM},
};
@@ -13,6 +13,7 @@ use types::{Registers, Stack};
use std::{cell::RefCell, rc::Rc, str::FromStr};
use itertools::Itertools;
// use std::iter::FromIterator;
// use itertools::Itertools;
use wasm_bindgen::prelude::*;
@@ -86,17 +87,9 @@ impl DeviceRef {
serde_wasm_bindgen::to_value(&self.device.borrow().get_fields(&self.vm.borrow())).unwrap()
}
#[wasm_bindgen(getter, skip_typescript)]
pub fn slots(&self) -> Vec<JsValue> {
self.device
.borrow()
.slots
.iter()
.map(|slot| {
let flat_slot: types::Slot = slot.into();
serde_wasm_bindgen::to_value(&flat_slot).unwrap()
})
.collect_vec()
#[wasm_bindgen(getter)]
pub fn slots(&self) -> types::Slots {
types::Slots::from_iter(self.device.borrow().slots.iter())
}
#[wasm_bindgen(getter, skip_typescript)]
@@ -489,6 +482,25 @@ impl VMRef {
Ok(self.vm.borrow_mut().remove_device(id)?)
}
#[wasm_bindgen(js_name = "setSlotOccupant", skip_typescript)]
pub fn set_slot_occupant(
&self,
id: u32,
index: usize,
template: JsValue,
) -> Result<(), JsError> {
let template: SlotOccupantTemplate = serde_wasm_bindgen::from_value(template)?;
Ok(self
.vm
.borrow_mut()
.set_slot_occupant(id, index, template)?)
}
#[wasm_bindgen(js_name = "removeSlotOccupant")]
pub fn remove_slot_occupant(&self, id: u32, index: usize) -> Result<(), JsError> {
Ok(self.vm.borrow_mut().remove_slot_occupant(id, index)?)
}
#[wasm_bindgen(js_name = "saveVMState", skip_typescript)]
pub fn save_vm_state(&self) -> JsValue {
let state = self.vm.borrow().save_vm_state();

View File

@@ -1,5 +1,8 @@
use std::collections::HashMap;
#![allow(non_snake_case)]
use std::collections::BTreeMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use tsify::Tsify;
@@ -15,14 +18,16 @@ pub struct Stack(#[serde_as(as = "[_; 512]")] pub [f64; 512]);
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Registers(#[serde_as(as = "[_; 18]")] pub [f64; 18]);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde_as]
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct SlotOccupant {
pub id: u32,
pub prefab_hash: i32,
pub quantity: u32,
pub max_quantity: u32,
pub damage: f64,
pub fields: HashMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
pub fields: BTreeMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
}
impl From<&ic10emu::device::SlotOccupant> for SlotOccupant {
@@ -38,11 +43,13 @@ impl From<&ic10emu::device::SlotOccupant> for SlotOccupant {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde_as]
#[derive(Tsify, Debug, Clone, Default, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Slot {
pub typ: ic10emu::device::SlotType,
pub occupant: Option<SlotOccupant>,
pub fields: HashMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
pub fields: BTreeMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
}
impl From<&ic10emu::device::Slot> for Slot {
@@ -55,5 +62,15 @@ impl From<&ic10emu::device::Slot> for Slot {
}
}
include!(concat!(env!("OUT_DIR"), "/ts_types.rs"));
#[serde_as]
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Slots(pub Vec<Slot>);
impl<'a> FromIterator<&'a ic10emu::device::Slot> for Slots {
fn from_iter<T: IntoIterator<Item = &'a ic10emu::device::Slot>>(iter: T) -> Self {
Slots(iter.into_iter().map(|slot| slot.into()).collect_vec())
}
}
include!(concat!(env!("OUT_DIR"), "/ts_types.rs"));

View File

@@ -7,20 +7,6 @@ export interface LogicField {
export type LogicFields = Map<LogicType, LogicField>;
export type SlotLogicFields = Map<SlotLogicType, LogicField>;
export interface SlotOccupant {
readonly id: number;
readonly prefab_hash: number;
readonly quantity: number;
readonly max_quantity: number;
readonly damage: number;
readonly fields: SlotLogicFields;
}
export interface Slot {
readonly typ: SlotType;
readonly occupant: SlotOccupant | undefined;
readonly fields: SlotLogicFields;
}
export type Reagents = Map<string, Map<number, number>>;
export interface ConnectionCableNetwork {
@@ -177,6 +163,7 @@ export interface FrozenVM {
export interface VMRef {
addDeviceFromTemplate(template: DeviceTemplate): number;
setSlotOccupant(id: number, index: number, template: SlotOccupantTemplate);
saveVMState(): FrozenVM;
restoreVMState(state: FrozenVM): void;
}

1
www/data/Enums.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ic10emu",
"version": "0.2.1",
"version": "0.2.2",
"description": "an IC10 emulator for IC10 mips from Stationeers",
"main": "index.js",
"scripts": {

View File

@@ -4,14 +4,6 @@ import { BaseElement, defaultCss } from "../components";
import "./nav";
import "./share";
import { ShareSessionDialog } from "./share";
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
// Set the base path to the folder you copied Shoelace's assets to
setBasePath("shoelace");
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import "../editor";
import { IC10Editor } from "../editor";
import { Session } from "../session";
@@ -79,7 +71,7 @@ export class App extends BaseElement {
window.App.set(this);
}
protected createRenderRoot(): HTMLElement | DocumentFragment {
createRenderRoot(): HTMLElement | DocumentFragment {
const root = super.createRenderRoot();
root.addEventListener("app-share-session", this._handleShare.bind(this));
root.addEventListener("app-open-file", this._handleOpenFile.bind(this));

View File

@@ -2,5 +2,4 @@ import { App } from "./app";
import { Nav } from "./nav";
import { SaveDialog } from "./save";
import { ShareSessionDialog } from "./share";
import "./icons";
export { App, Nav, ShareSessionDialog }

View File

@@ -1,15 +1,7 @@
import { HTMLTemplateResult, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseElement, defaultCss } from "../components";
import { BaseElement, defaultCss } from "components";
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/menu/menu.js";
import "@shoelace-style/shoelace/dist/components/divider/divider.js";
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
import "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js";
import "@shoelace-style/shoelace/dist/components/relative-time/relative-time.js";
import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
@customElement("app-nav")

View File

@@ -1,13 +1,8 @@
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 { BaseElement, defaultCss } from "components";
import { VMState } from "session";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/format-date/format-date.js";
import "@shoelace-style/shoelace/dist/components/relative-time/relative-time.js";
import "@shoelace-style/shoelace/dist/components/format-bytes/format-bytes.js";
import "@shoelace-style/shoelace/dist/components/spinner/spinner.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { repeat } from "lit/directives/repeat.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.js";

View File

@@ -1,12 +1,7 @@
import { HTMLTemplateResult, html, css } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "../components";
import { BaseElement, defaultCss } from "components";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/input/input.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 SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";

View File

@@ -1,13 +1,10 @@
import { html, css } from "lit";
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "../components";
import { customElement, property, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { SlDialog, SlSwitch } from "@shoelace-style/shoelace";
import { until } from "lit/directives/until.js";
import "@shoelace-style/shoelace/dist/components/spinner/spinner.js";
import '@shoelace-style/shoelace/dist/components/switch/switch.js';
import { marked } from "marked";
import { gfmStyles } from "./gfm-styles";
@@ -55,10 +52,7 @@ export class AppWelcome extends BaseElement {
render() {
return html`
<sl-dialog class="welcome-dialog" label="Changelog">
<h6>Hey there!</h6>
<p>Looks like there have been some updates since you've last visit.</p>
<br />
<p>Check out the changelog below.</p>
<div class="p-4 border-1 border-solid rounded-lg max-h-80 mt-4 overflow-y-auto bg-neutral-900 markdown-body">
${until(this.getChangelog(), html`<sl-spinner class="ml-2 my-4" style="font-size: 2rem;"></sl-spinner>`)}
</div>

View File

@@ -1,9 +1,6 @@
import {
html,
css,
HTMLTemplateResult,
PropertyValueMap,
CSSResultGroup,
} from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";

View File

@@ -2,13 +2,6 @@ import { ace, Ace, Range, AceLanguageClient, setupLspWorker } from "./ace";
import { LanguageProvider } from "ace-linters/types/language-provider";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.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/input/input.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/switch/switch.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
@@ -42,9 +35,9 @@ export class IC10Editor extends BaseElement {
};
sessions: Map<number, Ace.EditSession>;
@state() active_session: number = 1;
@state() activeSession: number = 1;
active_line_markers: Map<number, number | null> = new Map();
activeLineMarkers: Map<number, number | null> = new Map();
languageProvider?: LanguageProvider;
// ui: IC10EditorUI;
@@ -85,56 +78,35 @@ export class IC10Editor extends BaseElement {
};
this.sessions = new Map();
this.active_line_markers = new Map();
this.activeLineMarkers = new Map();
// this.ui = new IC10EditorUI(this);
}
protected render() {
const result = html`
<div
id="editorContainer"
style="height: 100%; width: 100%; position: relative; z-index: auto;"
>
<div
id="editor"
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 0; isolation: isolate;"
></div>
<div id="editorContainer" style="height: 100%; width: 100%; position: relative; z-index: auto;">
<div id="editor" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 0; isolation: isolate;">
</div>
<div id="editorStatusbar"></div>
</div>
<sl-dialog label="Editor Settings" class="dialog-focus e-settings-dialog">
<sl-radio-group
id="editorKeyboardRadio"
label="Editor Keyboard Bindings"
value=${this.settings.keyboard}
>
<sl-radio-group id="editorKeyboardRadio" label="Editor Keyboard Bindings" value=${this.settings.keyboard}>
<sl-radio-button value="ace">Ace</sl-radio-button>
<sl-radio-button value="vim">Vim</sl-radio-button>
<sl-radio-button value="emacs">Emacs</sl-radio-button>
<sl-radio-button value="sublime">Sublime</sl-radio-button>
<sl-radio-button value="vscode">VS Code</sl-radio-button>
</sl-radio-group>
<sl-radio-group
id="editorCursorRadio"
label="Editor Cursor Style"
value=${this.settings.cursor}
>
<sl-radio-group id="editorCursorRadio" label="Editor Cursor Style" value=${this.settings.cursor}>
<sl-radio-button value="ace">Ace</sl-radio-button>
<sl-radio-button value="slim">Slim</sl-radio-button>
<sl-radio-button value="smooth">Smooth</sl-radio-button>
<sl-radio-button value="smooth slim">Smooth And Slim</sl-radio-button>
<sl-radio-button value="wide">Wide</sl-radio-button>
</sl-radio-group>
<sl-input
id="editorFontSize"
label="Font Size"
type="number"
value="${this.settings.fontSize}"
></sl-input>
<sl-switch
id="editorRelativeLineNumbers"
?checked=${this.settings.relativeLineNumbers}
>
<sl-input id="editorFontSize" label="Font Size" type="number" value="${this.settings.fontSize}"></sl-input>
<sl-switch id="editorRelativeLineNumbers" ?checked=${this.settings.relativeLineNumbers}>
Relative Line Numbers
</sl-switch>
</sl-dialog>
@@ -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();

View File

@@ -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 {

View File

@@ -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: [

View File

@@ -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<number>) => any,) {
this.addEventListener("session-active-ic", callback);
}
@@ -98,11 +115,11 @@ export class Session extends EventTarget {
);
}
onErrors(callback: EventListenerOrEventListenerObject) {
onErrors(callback: (e: CustomEvent<number[]>) => any) {
this.addEventListener("session-errors", callback);
}
onLoad(callback: EventListenerOrEventListenerObject) {
onLoad(callback: (e: CustomEvent<Session>) => any) {
this.addEventListener("session-load", callback);
}
@@ -114,7 +131,7 @@ export class Session extends EventTarget {
);
}
onActiveLine(callback: EventListenerOrEventListenerObject) {
onActiveLine(callback: (e: CustomEvent<number>) => any) {
this.addEventListener("active-line", callback);
}

View File

@@ -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);
}

View File

@@ -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<T = {}> = 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 = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMDeviceMixinClass extends superClass {
_deviceID: number;
private _deviceID: number;
get deviceID() {
return this._deviceID;
}
@@ -57,8 +75,23 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
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 = <T extends Constructor<LitElement>>(
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<number[]>) {
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 = <T extends Constructor<LitElement>>(
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 = <T extends Constructor<LitElement>>(
this.updateDevice();
}
}
return VMActiveICMixinClass as Constructor<VMDeviceMixinInterface> & T;
};
export declare class VMDeviceDBMixinInterface {
deviceDB: DeviceDB;
_handleDeviceDBLoad(e: CustomEvent): void;
postDBSetUpdate(): void;
}
export const VMDeviceDBMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMDeviceDBMixinClass extends superClass {
connectedCallback(): void {
const root = super.connectedCallback();
window.VM.vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
if (typeof window.VM.vm.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<VMDeviceDBMixinInterface> & T;
};

View File

@@ -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`

File diff suppressed because it is too large Load Diff

View File

@@ -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<string, DeviceDBEntry> = 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`
<vm-device-template
prefab_name=${result.entry.name}
class="card"
@add-device-template=${this._handleDeviceAdd}
>
</vm-device-template>
`),
),
() => html`
<div class="p-2">
<div class="flex flex-row">
<p class="p-2">
<sl-format-number
.value=${this._searchResults?.length}
></sl-format-number>
results, filter more to get cards
</p>
<div class="p-2 ml-2">
Page:
${pageKeys.map(
(key, index) => html`
<span
class="p-2 cursor-pointer hover:text-purple-400 ${index ===
this.page
? " text-purple-500"
: ""}"
key=${key}
@click=${this._handlePageChange}
>${key + 1}${index < totalPages - 1 ? "," : ""}</span
>
`,
)}
</div>
</div>
<div class="flex flex-row flex-wrap">
${[
...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`
<div
class="m-2 text-neutral-200/90 italic cursor-pointer rounded bg-neutral-700 hover:bg-purple-500 px-1"
key=${key}
@click=${this._handleHaystackClick}
>
${result.entry.title} (<small class="text-sm">
${ranges.length
? unsafeHTML(uFuzzy.highlight(hay, ranges))
: hay} </small
>)
</div>
`;
})}
</div>
</div>
`,
);
}
_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`
<sl-button
variant="neutral"
outline
pill
@click=${this._handleAddButtonClick}
>
Add Device
</sl-button>
<sl-drawer class="add-device-drawer" placement="bottom" no-header>
<sl-input
class="device-search-input"
autofocus
placeholder="filter"
clearable
@sl-input=${this._handleSearchInput}
>
<span slot="prefix">Search Structures</span>
<sl-icon slot="suffix" name="search"></sl-icon>
</sl-input>
<div class="flex flex-row overflow-x-auto">
${this.renderSearchResults()}
</div>
<sl-button
slot="footer"
variant="primary"
@click=${() => {
this.drawer.hide();
}}
>
Close
</sl-button>
</sl-drawer>
`;
}
_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();
}
}

View File

@@ -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<any>): 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`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
const activeIc = window.VM.vm.activeIC;
activeIc?.pins?.forEach((id, index) => {
if (this.deviceID == id) {
badges.push(
html`<sl-badge variant="success" pill>d${index}</sl-badge>`,
);
}
}, this);
return html`
<sl-tooltip content="${this.prefabName}">
<img class="image me-2" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
</sl-tooltip>
<div class="header-name">
<sl-input id="vmDeviceCard${this.deviceID}Id" class="device-id me-1" size="small" pill value=${this.deviceID}
@sl-change=${this._handleChangeID}>
<span slot="prefix">Id</span>
<sl-copy-button slot="suffix" .value=${this.deviceID}></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}Name" class="device-name me-1" size="small" pill
placeholder=${this.prefabName} value=${this.name} @sl-change=${this._handleChangeName}>
<span slot="prefix">Name</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}Name.value"></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash me-1"
value="${this.nameHash}" readonly>
<span slot="prefix">Hash</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}NameHash.value"></sl-copy-button>
</sl-input>
${badges.map((badge) => badge)}
</div>
<div class="ms-auto mt-auto mb-auto me-2">
<sl-tooltip content=${thisIsActiveIc ? "Removing the selected Active IC is disabled" : "Remove Device" }>
<sl-icon-button class="remove-button" name="trash" label="Remove Device" ?disabled=${thisIsActiveIc}
@click=${this._handleDeviceRemoveButton}></sl-icon-button>
</sl-tooltip>
</div>
`;
}
renderFields() {
return this.delayRenderTab(
"fields",
html`<vm-device-fields .deviceID=${this.deviceID}></vm-device-fields>`,
);
}
_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`
<div class="flex flex-row flex-wrap">
${repeat(this.slots,
(slot, index) => slot.typ + index.toString(),
(_slot, index) => html`
<vm-device-slot .deviceID=${this.deviceID} .slotIndex=${index} class-"flex flex-row max-w-lg mr-2 mb-2">
</vm-device-slot>
`,
)}
</div>
`,
);
}
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`
<sl-select hoist placement="top" clearable key=${index} value=${conn?.net} ?disabled=${conn===null}
@sl-change=${this._handleChangeConnection}>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net.toString()}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
});
return this.delayRenderTab(
"networks",
html`<div class="networks">${networks}</div>`,
);
}
renderPins() {
const pins = this.pins;
const visibleDevices = window.VM.vm.visibleDevices(this.deviceID);
const pinsHtml = pins?.map(
(pin, index) =>
html`
<sl-select hoist placement="top" clearable key=${index} value=${pin} @sl-change=${this._handleChangePin}>
<span slot="prefix">d${index}</span>
${visibleDevices.map(
(device, _index) =>
html`
<sl-option value=${device.id}>
Device ${device.id} : ${device.name ?? device.prefabName}
</sl-option>
`,
)}
</sl-select>`,
);
return this.delayRenderTab("pins", html`<div class="pins">${pinsHtml}</div>`);
}
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<HTMLTemplateResult> {
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`
<ic10-details class="device-card" ?open=${this.open}>
<div class="header" slot="summary">${this.renderHeader()}</div>
<sl-tab-group @sl-tab-show=${this._handleTabChange}>
<sl-tab slot="nav" panel="fields" active>Fields</sl-tab>
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
<sl-tab slot="nav" panel="reagents" disabled>Reagents</sl-tab>
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
<sl-tab slot="nav" panel="pins" ?disabled=${!this.pins}>Pins</sl-tab>
<sl-tab-panel name="fields" active>
${until(this.renderFields(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="slots">
${until(this.renderSlots(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="reagents">
${until(this.renderReagents(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="networks">
${until(this.renderNetworks(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="pins"> ${this.renderPins()} </sl-tab-panel>
</sl-tab-group>
</ic10-details>
<sl-dialog class="remove-device-dialog" no-header @sl-request-close=${this._preventOverlayClose}>
<div class="remove-dialog-body">
<img class="dialog-image mt-auto mb-auto me-2" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<div class="flex-g">
<p><strong>Are you sure you want to remove this device?</strong></p>
<span>Id ${this.deviceID} : ${this.name ?? this.prefabName}</span>
</div>
</div>
<div slot="footer">
<sl-button variant="primary" autofocus @click=${this._closeRemoveDialog}>Close</sl-button>
<sl-button variant="danger" @click=${this._removeDialogRemove}>Remove</sl-button>
</div>
</sl-dialog>
`;
}
_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();
}
}

View File

@@ -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 {

View File

@@ -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<any> | Map<PropertyKey, unknown>): 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`<vm-device-card .deviceID=${id} class="device-list-card">
</vm-device-card>`,
);
const result = html`
<div class="header">
<span>
Devices:
<sl-badge variant="neutral" pill>${this.devices.length}</sl-badge>
</span>
<sl-input
class="device-filter-input"
placeholder="Filter Devices"
clearable
@sl-input=${this._handleFilterInput}
>
<sl-icon slot="suffix" name="search"></sl-icon>"
</sl-input>
<vm-add-device-button class="ms-auto"></vm-add-device-button>
</div>
<div class="device-list">${deviceCards}</div>
<vm-slot-add-dialog></vm-slot-add-dialog>
`;
return result;
}
@query("vm-slot-add-dialog") slotDialog: VMSlotAddDialog;
_showDeviceSlotDialog(
e: CustomEvent<SlotModifyEvent>,
) {
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;
}
}
}

View File

@@ -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` <sl-input id="${inputIdBase}${name}" key="${name}" value="${displayNumber(field.value)}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`;
}
_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();
});
}
}

View File

@@ -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 };

View File

@@ -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`<img
class="w-10 h-10"
src="${slotImg}"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>`;
const template = this.slotOcccupantTemplate();
const thisIsActiveIc = this.activeICId === this.deviceID;
const enableQuantityInput = false;
return html`
<div class="flex flex-row me-2">
<div
class="relative shrink-0 border border-neutral-200/40 rounded-lg p-1
hover:ring-2 hover:ring-purple-500 hover:ring-offset-1
hover:ring-offset-purple-500 cursor-pointer me-2"
@click=${this._handleSlotClick}
>
<div
class="absolute top-0 left-0 ml-1 mt-1 text-xs
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
>
<small>${this.slotIndex}</small>
</div>
<sl-tooltip content="${this.slotOccupantPrefabName() ?? slot.typ}">
${img}
</sl-tooltip>
${when(
typeof slot.occupant !== "undefined",
() =>
html`<div
class="absolute bottom-0 right-0 mr-1 mb-1 text-xs
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
>
<small
>${slot.occupant.quantity}/${slot.occupant
.max_quantity}</small
>
</div>`,
)}
<div></div>
</div>
<div class="flex flex-col justify-end">
<div class="text-sm mt-auto mb-auto">
${when(
typeof slot.occupant !== "undefined",
() => html` <span> ${this.slotOccupantPrefabName()} </span> `,
() => html` <span> ${template?.name} </span> `,
)}
</div>
<div class="text-neutral-400 text-xs mt-auto flex flex-col mb-1">
<div>
<strong class="mt-auto mb-auto">Type:</strong
><span class="p-1">${slot.typ}</span>
</div>
</div>
</div>
${when(
typeof slot.occupant !== "undefined",
() => html`
<div class="quantity-input ms-auto pl-2 mt-auto mb-auto me-2">
${enableQuantityInput
? html` <sl-input
type="number"
size="small"
.value=${slot.occupant.quantity.toString()}
.min=${1}
.max=${slot.occupant.max_quantity}
@sl-change=${this._handleSlotQuantityChange}
>
<div slot="help-text">
<span>Max Quantity: ${slot.occupant.max_quantity}</span>
</div>
</sl-input>`
: ""}
<sl-tooltip
content=${thisIsActiveIc && slot.typ === "ProgrammableChip"
? "Removing the selected Active IC is disabled"
: "Remove Occupant"}
>
<sl-icon-button
class="clear-occupant"
name="x-octagon"
label="Remove"
?disabled=${thisIsActiveIc && slot.typ === "ProgrammableChip"}
@click=${this._handleSlotOccupantRemove}
></sl-icon-button>
</sl-tooltip>
</div>
`,
() => html``,
)}
</div>
`;
}
_handleSlotOccupantRemove() {
window.VM.vm.removeDeviceSlotOccupant(this.deviceID, this.slotIndex);
}
_handleSlotClick(_e: Event) {
this.dispatchEvent(
new CustomEvent<SlotModifyEvent>("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`
<div class="slot-fields">
${fields.map(
([name, field], _index, _fields) => html`
<sl-input
id="${inputIdBase}${name}"
key="${name}"
value="${displayNumber(field.value)}"
size="small"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<sl-copy-button
slot="suffix"
from="${inputIdBase}${name}.value"
></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`,
)}
</div>
`;
}
_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`
<ic10-details
class="slot-card"
>
<div class="slot-header w-full" slot="summary">
${this.renderHeader()}
</div>
<div class="slot-body">${this.renderFields()}</div>
</ic10-details>
`;
}
}

View File

@@ -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<string, DeviceDBEntry> = 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`
<div class="cursor-pointer hover:bg-neutral-600 rounded px-2 py-1 me-1" @click=${this._handleClickNone}>
None
</div>
`;
return html`
<div class="mt-2 max-h-48 overflow-y-auto w-full">
${enableNone ? none : ""}
${this._searchResults.map((result) => {
const imgSrc = `img/stationpedia/${result.entry.name}.png`;
const img = html`
<img class="w-8 h-8 mr-2" src=${imgSrc} onerror="this.src = '${VMDeviceCard.transparentImg}'" />
`;
return html`
<div class="cursor-pointer hover:bg-neutral-600 rounded px-2 py-1 me-1 flex flex-row" key=${result.entry.name} @click=${this._handleClickItem}>
${img}
<div>${result.entry.title}</div>
</div>
`;
})}
</div>
`;
}
_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`
<sl-dialog
label="Edit device ${id} : ${name} Slot ${this.slotIndex}"
class="slot-add-dialog"
@sl-hide=${this._handleDialogHide}
>
<sl-input class="device-search-input" autofocus placeholder="filter" clearable
@sl-input=${this._handleSearchInput}>
<span slot="prefix">Search Items</span>
<sl-icon slot="suffix" name="search"></sl-icon>
</sl-input>
${when(
typeof this.deviceID !== "undefined" &&
typeof this.slotIndex !== "undefined",
() => html`
<div class="flex flex-row overflow-x-auto">
${this.renderSearchResults()}
</div>
`,
() => html``,
)}
</sl-dialog>
`;
}
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();
}
}

View File

@@ -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`
<sl-input
key="${name}"
value="${displayNumber(field.value)}"
size="small"
@sl-change=${this._handleChangeField}
?disabled=${name === "PrefabHash"}
>
<span slot="prefix">${name}</span>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`;
})}
`;
}
_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`<sl-card class="slot-card"> </sl-card>`;
}
renderSlots(): HTMLTemplateResult {
return html`<div clas="slots"></div>`;
}
renderReagents(): HTMLTemplateResult {
return html``;
}
renderNetworks() {
const vm = window.VM.vm;
const vmNetworks = vm.networks;
const connections = this.connections;
return html`
<div class="networks">
${connections.map((connection, index, _conns) => {
const conn =
typeof connection === "object" ? connection.CableNetwork : null;
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn?.net}
?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}
>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
})}
</div>
`;
}
_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`<div class="pins"></div>`;
}
render() {
const device = this.dbDevice;
return html`
<sl-card class="template-card">
<div class="header h-20 w-96" slot="header">
<sl-tooltip content="${device?.name}">
<img
class="image me-2"
src="img/stationpedia/${device?.name}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
</sl-tooltip>
<div class="vstack">
<span class="prefab-title">${device.title}</span>
<span class="prefab-name"><small>${device?.name}</small></span>
<span class="prefab-hash"><small>${device?.hash}</small></span>
</div>
<sl-button
class="ms-auto mt-auto mb-auto"
pill
variant="success"
@click=${this._handleAddButtonClick}
>Add <sl-icon slot="prefix" name="plus-lg"></sl-icon>
</sl-button>
</div>
<div class="card-body">
<sl-tab-group>
<sl-tab slot="nav" panel="fields">Fields</sl-tab>
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
<!-- <sl-tab slot="nav" panel="reagents">Reagents</sl-tab> -->
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
<!-- <sl-tab slot="nav" panel="pins">Pins</sl-tab> -->
<sl-tab-panel name="fields">${this.renderFields()}</sl-tab-panel>
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
<!-- <sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel> -->
<sl-tab-panel name="networks"
>${this.renderNetworks()}</sl-tab-panel
>
<!-- <sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel> -->
</sl-tab-group>
</div>
</sl-card>
`;
}
_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();
}
}

View File

@@ -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 } };
}

View File

@@ -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<DeviceRef>(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<number, DeviceRef>;
_devices: Map<number, CacheDeviceRef>;
_ics: Map<number, DeviceRef>;
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();
}

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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";

View File

@@ -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"<N:([A-Z]{2}):(\w+)>")
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"<color=(#?\w+)>((:?(?!<color=(?:#?\w+)>).)+?)</color>", re.DOTALL
)
link_regex = re.compile(r"<link=(\w+)>(.+?)</link>")
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"""<div style="color: \1;">\2</div>"""
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"<link=\w+><color=[\w#]+>(.+?)</color></link>")
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,

View File

@@ -1,5 +1,7 @@
{
"compilerOptions": {
"baseUrl": "./src/ts",
"rootDir": "./src/ts",
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,

View File

@@ -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<P: AsRef<std::ffi::OsStr> + std::fmt::Debug + std::fmt::Display>(
Ok(())
}
fn pnpm_install(
args: &Args,
workspace: &std::path::Path,
) -> Result<ExitStatus, Error> {
fn pnpm_install(args: &Args, workspace: &std::path::Path) -> Result<ExitStatus, Error> {
eprintln!("Running `pnpm install`");
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));