@@ -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}}
|
||||
@@ -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}}
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -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
52
CHANGELOG.md.jinja
Normal 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
8
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
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
22649
www/data/database.json
22649
www/data/database.json
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
305
www/src/ts/virtual_machine/device/add_device.ts
Normal file
305
www/src/ts/virtual_machine/device/add_device.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
409
www/src/ts/virtual_machine/device/card.ts
Normal file
409
www/src/ts/virtual_machine/device/card.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
177
www/src/ts/virtual_machine/device/device_list.ts
Normal file
177
www/src/ts/virtual_machine/device/device_list.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
www/src/ts/virtual_machine/device/fields.ts
Normal file
42
www/src/ts/virtual_machine/device/fields.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
15
www/src/ts/virtual_machine/device/index.ts
Normal file
15
www/src/ts/virtual_machine/device/index.ts
Normal 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 };
|
||||
|
||||
299
www/src/ts/virtual_machine/device/slot.ts
Normal file
299
www/src/ts/virtual_machine/device/slot.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
261
www/src/ts/virtual_machine/device/slot_add_dialog.ts
Normal file
261
www/src/ts/virtual_machine/device/slot_add_dialog.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
267
www/src/ts/virtual_machine/device/template.ts
Normal file
267
www/src/ts/virtual_machine/device/template.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 } };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src/ts",
|
||||
"rootDir": "./src/ts",
|
||||
"outDir": "./dist/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user