diff --git a/.github/resources/banner.gif b/.github/resources/banner.gif
index 35d6d5a..260cc9b 100644
Binary files a/.github/resources/banner.gif and b/.github/resources/banner.gif differ
diff --git a/.github/resources/screenshot.png b/.github/resources/screenshot.png
new file mode 100644
index 0000000..cfe32ef
Binary files /dev/null and b/.github/resources/screenshot.png differ
diff --git a/.github/resources/screenshot2.png b/.github/resources/screenshot2.png
new file mode 100644
index 0000000..c7f4677
Binary files /dev/null and b/.github/resources/screenshot2.png differ
diff --git a/.github/resources/screenshots.png b/.github/resources/screenshots.png
deleted file mode 100644
index 98a686a..0000000
Binary files a/.github/resources/screenshots.png and /dev/null differ
diff --git a/.github/workflows/check-test-format.yaml b/.github/workflows/check.yaml
similarity index 59%
rename from .github/workflows/check-test-format.yaml
rename to .github/workflows/check.yaml
index a1e3f61..f643406 100644
--- a/.github/workflows/check-test-format.yaml
+++ b/.github/workflows/check.yaml
@@ -1,11 +1,11 @@
# shoutout @wermipls
-name: check + test + format
+name: check
on:
push:
- branches: [ main ]
+ branches: [main]
pull_request:
- branches: [ main ]
+ branches: [main]
jobs:
windows:
@@ -17,13 +17,16 @@ jobs:
with:
toolchain: nightly
- - name: run cargo check
+ - name: install required rustup components
+ run: |
+ rustup component add rustfmt --toolchain nightly
+ rustup target add x86_64-pc-windows-gnu
+
+ - name: check
run: cargo check
- - name: run unit tests
+ - name: test
run: cargo test --all -- --nocapture
- - name: verify code is formatted
- run: |
- rustup component add rustfmt --toolchain nightly
- cargo fmt --all -- --check
+ - name: fmt
+ run: cargo fmt --all -- --check
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index ab37fc6..f58547d 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -26,7 +26,7 @@ jobs:
#
# This is disgusting - need to assume nothing else in Cargo.toml has
# "version = x.x.x" from start of line
- $project_version = cat .\cascade_cli\Cargo.toml | grep -oE '^version = \"?[0-9]+\.[0-9]+\.[0-9]+\"' | grep -oE "[0-9]+\.[0-9]+\.[0-9]+"
+ $project_version = cat .\cascade_app\Cargo.toml | grep -oE '^version = \"?[0-9]+\.[0-9]+\.[0-9]+\"' | grep -oE "[0-9]+\.[0-9]+\.[0-9]+"
"project_version=$project_version" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: install nightly rust toolchain
@@ -34,12 +34,16 @@ jobs:
with:
toolchain: nightly
+ - name: install required rustup components
+ run: |
+ rustup component add rustfmt --toolchain nightly
+ rustup target add x86_64-pc-windows-gnu
+
- name: cargo build
- run: cargo build --release
+ run: cargo build --target x86_64-pc-windows-gnu --release
- name: create release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
- # Probably a better way to do this
- files: ./target/release/cascade.exe
+ files: ./target/x86_64-pc-windows-gnu/release/cascade.exe
diff --git a/.justfile b/.justfile
new file mode 100644
index 0000000..b995f43
--- /dev/null
+++ b/.justfile
@@ -0,0 +1,13 @@
+default: check test fmt
+
+check:
+ cargo check
+
+test:
+ cargo test --all -- --nocapture
+
+fmt:
+ cargo fmt --all -- --check
+
+build-windows:
+ cargo build --target x86_64-pc-windows-gnu --release
diff --git a/Cargo.toml b/Cargo.toml
index ee8bb2e..f595aeb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
[workspace]
-members = ["cascade", "cascade_cli", "cascade_gui"]
+members = ["cascade", "cascade_app"]
resolver = "2"
diff --git a/README.md b/README.md
index e8b6422..ded0f3c 100644
--- a/README.md
+++ b/README.md
@@ -1,198 +1,129 @@
-
+
- π | a thug pro trickset copier.
+ A bulk save modifier for THUG Pro.
-# `πΈ screenshots`
+## `πΈ screenshots`
-
+
-# `β¬ download`
+## `π table of contents`
-download `cascade.exe` from
-[here](https://github.com/1borgy/cascade/releases/latest).
+* [installation](#installation)
+* [usage](#usage)
+* [backups](#backups)
+* [contact](#contact)
+* [faq](#faq)
+* [shoutouts](#shoutouts)
+* [development](#development)
-# `π€ usage`
+## `β¬ installation`
->β οΈ **warning** β οΈ
->
-> please make a manual backup of your saves! i've tested this along with a few community
-> members and have yet to observe any issues, but your results may vary, as cascade is
-> still experimental software.
+Download `cascade.exe` from [here](https://github.com/1borgy/cascade/releases/latest).
-cascade will automatically detect your THUG Pro installation folder if it is in
-`%localappdata%/THUG Pro/`. if you have it installed elsewhere, you will need to
-[configure cascade](#configuration).
+## `β‘οΈ usage`
-**using cascade is very easy:**
+> [!WARNING]
+> I highly recommend making a manual backup of your saves! Though `cascade` has been rigorously
+> tested against community CAS packs, the possibility of save corruption still exists.
-**one-** click "set trickset" to tell cascade which save's trickset you want to use.
+Upon opening Cascade, you will be greeted with three columns.
-**two-** click "copy trickset to saves" to copy that trickset to all of your saves.
+### `from`
-it will tell you how many tricksets were successfully copied - if cascade can't
-succesfully copy the trickset to a certain save, it won't touch that save file. this is
-typically the case with corrupted saves, or saves from other games (like THUG1 saves).
+The "from" column allows you to select the save to copy from. You may choose to copy trickset,
+scales, or both.
-if you have a save that is failing to be copied, or want to help contribute to cascade,
-you can [help the project](#help-me)!
+### `to`
-# `πΎ backups`
+The "to" column allows you to select which saves to copy to.
-if you want to revert copying a trickset, or the saves were corrupted for some reason,
-cascade stores backups at `THUG Pro/.cascade/` every time it copies tricksets. each
-entry in this folder is marked with a date and time so you know when it was backed
-up. simply pick which backup you want to use and drag its contents into your
-`THUG Pro/Save/` folder.
+Cascade will automatically detect your save folder at `%localappdata%/THUG Pro/Save`.
+If you have it installed elsewhere, or want to copy saves to a different folder, you will need to
+select the desired folder manually.
-# `π frequently asked questions`
+### `queue`
-## why do i get a message saying "SmartScreen prevented an unrecognized app from starting" when trying to run cascade?
+The "queue" column shows which save files are currently queued to be modified, as well as their
+most recent modification status. Upon pressing the start (βΆοΈ) button, all saves in the queue will
+be backed up then modified in-place.
-this message basically means that i don't have
-the certificate required to become a "recognized publisher".
+If modification of a save is successful, it will turn green. If modification of a save results in
+an error, it will turn red. If you encounter an error while modifying saves, please
+[contact me](#contact).
-these certificates
-[cost hundreds or even thousands of dollars](https://signmycode.com/ev-code-signing), so
-i don't plan on ever getting one.
+> [!TIP]
+> Use `ctrl+-` and `ctrl+=` to resize the UI.
-## what does β\[... path\] is not setβ mean?
+> [!TIP]
+> Create an empty file named `cascade.toml` in the same directory as the executable to use a
+> portable install. Otherwise, cascade will store files at `%localappdata%/cascade`.
-this means you are missing at least one of the path entries in the cascade config.
-see [configuration](#configuration).
+## `πΎ backups`
-## how can i copy my trickset to only some saves?
+If you want to revert a cascade run, backups are stored at `%localappdata%/cascade/backup`.
+Simply pick which backup you want to use and drag its contents into your saves folder.
+Each entry in this folder is labeled with the date and time of modification.
-currently, you can make another folder, put the saves you want to copy in that folder,
-then [configure](#configuration) your `thugpro saves` path to that folder instead.
+## `π£ contact`
-## how can i use multiple tricksets and swap between them?
+Please direct any questions or concerns towards `@1borgy` on Discord. If you encounter an error,
+please send the cascade log file, which you may find at `%localappdata%/cascade/cascade.log`.
-i would recommend having a separate folder of tricksets and swapping between them with
-the βset tricksetβ button. you could also [configure](#configuration) your `trickset`
-path to swap between them, if that feels better for some reason.
+## `π frequently asked questions`
-## why do the file modification times on my saves not change?
+### Why do I get a message saying "SmartScreen prevented an unrecognized app from starting" when trying to run cascade?
-during development, i found it annoying that copying tricksets would change the order
-your saves show up in the βload saveβ menu, so by default, cascade rewrites the file
-modification time to what it was before copying.
+To put it simply, this message means that I don't have the certificate required to become a "recognized publisher".
-i realize this probably shouldn't be default behavior, so please
-[yell at me](#feedback) if you want that to be configurable.
+These certificates [cost hundreds of dollars](https://signmycode.com/ev-code-signing), so I don't plan on getting one.
-## why do not all of my saves copy successfully?
+### Do I have to close THUG Pro to copy the trickset to my saves?
-if a save is not copied succesfully, it's likely either a corrupted save or a save from
-a different game (e.g. THUG1).
+Save files are reloaded when entering the "Load Skater" menu, so you do not have to relaunch.
-i tested a CAS pack with nearly 300 saves, and only one of them failed. the one that
-failed to copy crashed THUG Pro when i tried to open it, so it's safe to say that CAS
-is not a valid THUG Pro CAS anyways.
+### Why do the file modification times on my saves not change?
-## do i have to close thug pro to copy the trickset to my saves?
+This was a design decision I made during development, to preserve ordering of save files.
+If you'd like this to be configurable, please [contact me](#contact).
-from my testing, you don't need to close thug pro. the save files are re-loaded when
-loading a new skater.
+## `β¨ shoutouts`
-# `π’ you can help cascade!`
+Cascade would not exist without the following people, so shoutout to them:
-please reach out to me on discord **("borgy" in most THPS servers)** with any
-questions or comments, especially if you have any issues getting cascade to work.
-
-if you run into an issue, please send me the cascade logs
-(you can find these at `THUG Pro/.cascade/cascade.log`) and any CAS that is failing so
-i can help diagnose the issue.
-
-i'd also love to hear general [feedback](#i-hate-cascade-it-sucks)! i already have a
-[few ideas for improvements](#thinking-emoji).
-
-## feedback π€¬
-
-```diff
-! i want a scale copier!
-! i want a save selector for trickset copying!
-! i want a cas randomizer! (so do i!)
-! i want rotating backups they take up too much room!
-! trickset copying is too slow!!! π€¬
-! i want a portable install!
-! i want a rotating log file!
-! i want a configurable source/destination folder for saves!
-
-+ what else do you want to see in cascade?
-+ how can the user interface be improved?
-+ is the UI's performance poor on your system?
-+ etc.
-```
-
-## things i've thought about ππ
-
-see [github issues](https://github.com/1borgy/cascade/issues).
-
-# `βοΈ configuration`
-
-## thugpro saves
-
-this path refers to the folder that cascade reads saves from and copies saves back
-to.
-
-by default, cascade autodetects THUG Pro's install folder if it exists at
-`%localappdata%/THUG Pro/` (if there is another commonly installed location
-[let me know](#help-me)!).
-
-if your THUG Pro is installed elsewhere, you will need to tell cascade where to look
-for saves. click on the `config` tab and select the path for `thugpro saves`. this
-path should point to the `THUG Pro/Save/` folder, not the base `THUG Pro/` folder.
-
-## backups
-
-this path refers to the folder that cascade stores backups in.
-
-by default, cascade will store backups in the `THUG Pro/.cascade/backup/` folder.
-
-## trickset
-
-this path refers to the file cascade uses as the reference trickset when copying a
-trickset to saves.
-
-by default, cascade will store the trickset at `THUG Pro/.cascade/trickset.SKA`.
-
-## portable install?
-
-by default, cascade stores all its required files in `THUG Pro/.cascade/`.
-
-if you prefer a portable install, you can configure these paths to be in the same
-folder as the executable.
-
-currently the cascade config (`cascade.toml`) cannot be moved from this folder, but
-that can change [if there is demand for it](#help-me)!
-
-# `β¨π« shoutouts π«β¨`
-
-cascade would not exist without the following people, so shoutout to them:
-
-- **source** π§ for the great work on [castool](https://castool.xyz) and for giving me
+- **source** π§ for their great work on [castool](https://castool.xyz) and for giving me
pointers regarding CAS format
-- **[@chc](https://github.com/chc)** π§ for the great work on the
+- **[@chc](https://github.com/chc)** π§ for their great work on the
[save editor](http://save-editor.thmods.com/#/manage_save)
and [THPS.API](https://github.com/chc/thps.api/tree/master)
- **[@c4marilla](https://github.com/c4marilla)** π¦ for being the first beta tester,
making the project logo and banner, and being appointed cascade project manager β¨
-- **retro** π¦ for being an early beta tester
+- **retro**, **judy**, and **f1shy** π¦ for being beta testers
+
+- **cdot** πΉ for releasing their CAS pack, which is used for automated integration testing
+
+- **[@wermipls](https://github.com/wermipls)** πͺ± for helping with GitHub actions
+
+- **[@catppuccin](https://github.com/catppuccin/catppuccin)** πββ¬ for providing cascade's color palette
-- **cdot** πΉ for releasing his giant cas pack, which i'm using for automated
- integration testing
+- **[@iced-rs](https://github.com/iced-rs/iced)** π§ for powering cascade's UI
-- **[@wermipls](https://github.com/wermipls)** πͺ± for helping with github actions
+# `π οΈ development`
-- **[@catppuccin](https://github.com/catppuccin/catppuccin)** πββ¬ for
- [1] blessing my eyes while i waste away in vim and [2] being the color palette used as
- a reference for cascade's themes
+Building cascade requires nightly rust.
-- **[@iced-rs](https://github.com/iced-rs/iced)** π§ for powering cascade's ui
- and making me not hate frontend that much
+```bash
+rustup toolchain install nightly
+rustup default nightly
+```
+
+To cross-compile for windows:
+
+```bash
+cargo build --target x86_64-pc-windows-gnu --release
+```
diff --git a/cascade/Cargo.toml b/cascade/Cargo.toml
index a9c3bca..5122f11 100644
--- a/cascade/Cargo.toml
+++ b/cascade/Cargo.toml
@@ -1,20 +1,16 @@
[package]
name = "cascade"
-version = "0.2.0"
+version = "0.3.0"
edition = "2021"
[dependencies]
byteorder = "1.4.3"
count-write = "0.1.0"
crc = "3.0.1"
-directories = "5.0.1"
filetime = "0.2.21"
-lazy_static = "1.4.0"
+indexmap = "2.5.0"
log = "0.4.17"
-rayon = "1.8.0"
+ron = "0.8.1"
serde = { version = "1.0.190", features = ["rc", "derive"] }
-serde_yaml = "0.9.27"
thiserror = "1.0.50"
-time = "0.3.30"
toml = "0.8.6"
-walkdir = "2.3.3"
diff --git a/cascade/src/actions/backup.rs b/cascade/src/actions/backup.rs
deleted file mode 100644
index 304c5bc..0000000
--- a/cascade/src/actions/backup.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use std::{
- fs,
- path::{Path, PathBuf},
-};
-
-use time;
-
-use super::error::ActionError;
-
-pub fn backup>(
- backup_dir: P,
- saves_dir: P,
-) -> Result<(), ActionError> {
- let backup_dir = backup_dir.as_ref();
- let saves_dir = saves_dir.as_ref();
-
- let datetime = time::OffsetDateTime::now_local().unwrap_or({
- log::warn!("could not get local timezone; using utc");
- time::OffsetDateTime::now_utc()
- });
-
- let subdir_name = format!(
- "{:04}-{:02}-{:02}T{:02}-{:02}-{:02}",
- datetime.year(),
- u8::from(datetime.month()),
- datetime.day(),
- datetime.hour(),
- datetime.minute(),
- datetime.second()
- );
-
- let mut subdir = PathBuf::from(backup_dir);
- subdir.push(subdir_name);
-
- fs::create_dir_all(&subdir)?;
-
- // TODO: use SaveFile here since it's lazy now
- for file in fs::read_dir(saves_dir)? {
- // why him so confused ??
- let file = file?;
- let file_path = file.path();
-
- if file.file_type()?.is_file() {
- if let Some(extension) = file.path().extension() {
- if extension == "SKA" {
- // holy shit let it stop im so sorry
- // just use .filter() or something u moron
- if let Some(file_name) = file_path.file_name() {
- let mut backup_file_path = subdir.clone();
- backup_file_path.push(file_name);
-
- log::info!(
- "backing up {:?} to {:?}",
- file_path,
- backup_file_path
- );
-
- fs::copy(file_path, backup_file_path)?;
- }
- }
- }
- }
- }
-
- Ok(())
-}
diff --git a/cascade/src/actions/copy_trickset.rs b/cascade/src/actions/copy_trickset.rs
deleted file mode 100644
index 5f8e9c3..0000000
--- a/cascade/src/actions/copy_trickset.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-use std::{
- fmt::Display,
- path::Path,
- sync::{
- atomic::{AtomicUsize, Ordering},
- Arc,
- },
-};
-
-use rayon::prelude::ParallelIterator;
-
-use crate::{
- actions::{backup::backup, error::ActionError},
- mutations::{self, Mutation},
- save::{SaveCollection, SaveFile, SaveFileExtension},
-};
-
-fn try_result(result: Result) -> Option
-where
- E: Display,
-{
- match result {
- Err(err) => {
- log::info!("error: {}", err);
- None
- }
- Ok(v) => Some(v),
- }
-}
-
-pub fn copy_trickset + Sync>(
- trickset_path: P,
- backup_dir: P,
- saves_dir: P,
-) -> Result<(usize, usize), ActionError> {
- backup(&backup_dir, &saves_dir)?;
-
- let trickset_file = SaveFile::at_path(&trickset_path)?;
-
- log::info!(
- "generating trickset mutation from \"{}\"",
- trickset_file.filename()
- );
- let trickset_mutation =
- mutations::Trickset::from_save(&trickset_file.load_content()?)?;
-
- let num_successful = Arc::new(AtomicUsize::new(0));
-
- let saves = SaveCollection::at_dir(&saves_dir)?
- .filter_extension(SaveFileExtension::SKA);
-
- let num_all_saves = saves.len();
-
- // TODO: move this to functions
- // TODO: please do it quickly
- // TODO: it's so messy
- // TODO:
- // TODO: please
- saves.par_iter().for_each(|save| {
- try_result(save.load_content()).and_then(|content| {
- match trickset_mutation.mutate(content) {
- Ok(new_content) => {
- log::info!(
- "successfully copied trickset to save \"{}\"",
- save.filename()
- );
-
- try_result(
- save.with_dir(&saves_dir).write_content(&new_content),
- )
- .and_then(|_| {
- num_successful.fetch_add(1, Ordering::SeqCst);
- None::<()>
- });
- }
- Err(err) => {
- log::error!(
- "could not copy trickset to save \"{}\": {}",
- save.filename(),
- err
- );
- }
- };
-
- None::<()>
- });
- });
-
- for save in saves.iter() {
- save.overwrite_metadata()?;
- }
-
- Ok((num_successful.load(Ordering::SeqCst), num_all_saves))
-}
diff --git a/cascade/src/actions/error.rs b/cascade/src/actions/error.rs
deleted file mode 100644
index bad1b6b..0000000
--- a/cascade/src/actions/error.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use std::{backtrace::Backtrace, fmt::Debug, io};
-
-use thiserror::Error;
-
-use crate::{mutations::MutationError, save::SaveError};
-
-#[derive(Error, Debug)]
-pub enum ActionError {
- #[error("a mutation failed: {source}")]
- Mutation {
- #[from]
- source: MutationError,
- backtrace: Backtrace,
- },
-
- #[error("a save operation failed: {source}")]
- Save {
- #[from]
- source: SaveError,
- backtrace: Backtrace,
- },
-
- #[error("an io error occurred: {source}")]
- Io {
- #[from]
- source: io::Error,
- backtrace: Backtrace,
- },
-}
diff --git a/cascade/src/actions/mod.rs b/cascade/src/actions/mod.rs
deleted file mode 100644
index ee726cf..0000000
--- a/cascade/src/actions/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-mod backup;
-mod copy_trickset;
-mod error;
-mod set_trickset;
-
-pub use backup::backup;
-pub use copy_trickset::copy_trickset;
-pub use error::ActionError;
-pub use set_trickset::set_trickset;
diff --git a/cascade/src/actions/set_trickset.rs b/cascade/src/actions/set_trickset.rs
deleted file mode 100644
index 54f4468..0000000
--- a/cascade/src/actions/set_trickset.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use std::{fs, path::Path};
-
-use super::error::ActionError;
-
-pub fn set_trickset>(
- trickset_path: P,
- selected_path: P,
-) -> Result<(), ActionError> {
- log::info!(
- "setting trickset at {:?} to {:?}",
- trickset_path.as_ref(),
- selected_path.as_ref()
- );
-
- fs::copy(selected_path, trickset_path)?;
-
- Ok(())
-}
diff --git a/cascade/src/crc32.rs b/cascade/src/crc32.rs
index 538081f..b5aa005 100644
--- a/cascade/src/crc32.rs
+++ b/cascade/src/crc32.rs
@@ -1,8 +1,9 @@
+use std::cell::LazyCell;
+
use crc::{Algorithm, Crc};
-use lazy_static::lazy_static;
-lazy_static! {
- static ref CRC_ALG: Algorithm = Algorithm {
+const CRC: LazyCell> = LazyCell::new(|| {
+ Crc::::new(&Algorithm {
width: 32,
poly: 0x04c11db7,
init: 0xffffffff,
@@ -11,10 +12,9 @@ lazy_static! {
xorout: 0x0000,
check: 0xaee7,
residue: 0x0000,
- };
- static ref CRC: Crc = Crc::::new(&CRC_ALG);
-}
+ })
+});
-pub fn get_checksum_for_bytes(bytes: &Vec) -> u32 {
+pub fn checksum(bytes: &Vec) -> u32 {
CRC.checksum(bytes.as_slice())
}
diff --git a/cascade/src/lib.rs b/cascade/src/lib.rs
index 8485531..fcb2d28 100644
--- a/cascade/src/lib.rs
+++ b/cascade/src/lib.rs
@@ -1,7 +1,5 @@
-#![feature(error_generic_member_access, try_blocks)]
-pub mod actions;
-mod crc32;
-mod lookup;
-pub mod mutations;
+#![feature(error_generic_member_access)]
+pub mod crc32;
+pub mod lut;
+pub mod qb;
pub mod save;
-pub mod structure;
diff --git a/cascade/src/lookup.rs b/cascade/src/lookup.rs
deleted file mode 100644
index 7cc78f6..0000000
--- a/cascade/src/lookup.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::collections::HashMap;
-
-use lazy_static::lazy_static;
-use serde::{Deserialize, Serialize};
-
-// I feel like this whole module is messy but it gets the job done ig
-
-const CHECKSUM_LOOKUP_BYTES: &[u8] =
- include_bytes!("../../resources/checksum_lookup.yaml");
-const COMPRESSED_LOOKUP_BYTES: &[u8] =
- include_bytes!("../../resources/compressed_lookup.yaml");
-
-lazy_static! {
- static ref CHECKSUM_LOOKUP: HashMap = load_checksum_lookup();
- static ref COMPRESSED_LOOKUP: CompressedLookupTable =
- load_compressed_lookup();
- static ref REVERSE_CHECKSUM_LOOKUP: HashMap =
- create_reverse_checksum_lookup(&CHECKSUM_LOOKUP);
- static ref REVERSE_COMPRESSED_LOOKUP: ReverseCompressedLookupTable =
- create_reverse_compressed_lookup(&COMPRESSED_LOOKUP);
-}
-
-#[derive(Serialize, Deserialize)]
-struct CompressedLookupTable {
- byte: Vec,
- word: Vec,
-}
-
-struct ReverseCompressedLookupTable {
- byte: HashMap,
- word: HashMap,
-}
-
-// .expect() is lazy here but we can guarantee it's valid YAML since we provide it
-// (and is critical for execution)
-fn load_checksum_lookup() -> HashMap {
- serde_yaml::from_slice(CHECKSUM_LOOKUP_BYTES)
- .expect("could not load checksum lookup table!")
-}
-
-fn create_reverse_checksum_lookup(
- checksum_lookup: &HashMap,
-) -> HashMap {
- let mut reverse_lookup = HashMap::new();
-
- for (checksum, name) in checksum_lookup.iter() {
- reverse_lookup.insert(name.clone(), *checksum as u32);
- }
-
- reverse_lookup
-}
-
-fn load_compressed_lookup() -> CompressedLookupTable {
- serde_yaml::from_slice(COMPRESSED_LOOKUP_BYTES)
- .expect("could not load compressed lookup table!")
-}
-
-fn create_reverse_compressed_lookup(
- compressed_lookup: &CompressedLookupTable,
-) -> ReverseCompressedLookupTable {
- let mut byte = HashMap::new();
- let mut word = HashMap::new();
-
- for (compressed_index, name) in compressed_lookup.byte.iter().enumerate() {
- byte.insert(name.clone(), compressed_index as u8);
- }
-
- for (compressed_index, name) in compressed_lookup.word.iter().enumerate() {
- word.insert(name.clone(), compressed_index as u16);
- }
-
- ReverseCompressedLookupTable { byte, word }
-}
-
-pub fn checksum(checksum: u32) -> Option {
- CHECKSUM_LOOKUP.get(&(checksum as usize)).cloned()
-}
-
-pub fn compressed8(byte: u8) -> Option {
- COMPRESSED_LOOKUP.byte.get(byte as usize).cloned()
-}
-
-pub fn compressed16(word: u16) -> Option {
- COMPRESSED_LOOKUP.word.get(word as usize).cloned()
-}
-
-pub fn reverse_checksum(name: &String) -> Option {
- REVERSE_CHECKSUM_LOOKUP.get(name).cloned()
-}
-
-pub fn reverse_compressed16(name: &String) -> Option {
- REVERSE_COMPRESSED_LOOKUP.word.get(name).cloned()
-}
-
-pub fn reverse_compressed8(name: &String) -> Option {
- REVERSE_COMPRESSED_LOOKUP.byte.get(name).cloned()
-}
diff --git a/cascade/src/lut.rs b/cascade/src/lut.rs
new file mode 100644
index 0000000..edb7284
--- /dev/null
+++ b/cascade/src/lut.rs
@@ -0,0 +1,166 @@
+use std::{collections::HashMap, result, str::Utf8Error};
+
+use ron::de::SpannedError;
+use serde::{Deserialize, Serialize};
+
+use crate::qb;
+
+const LUT_THUGPRO_BYTES: &[u8] =
+ include_bytes!("../../resources/lut/thug_pro.ron");
+
+#[derive(thiserror::Error, Debug, Clone)]
+pub enum Error {
+ #[error("ron deserialization error: {0}")]
+ Spanned(#[from] SpannedError),
+
+ #[error("utf8 decoding error: {0}")]
+ Utf8(#[from] Utf8Error),
+
+ #[error("name not found in LUT: {0}")]
+ NameNotFound(String),
+
+ #[error("id not found in LUT: {0:?}")]
+ IdNotFound(qb::Id),
+}
+
+pub type Result = result::Result;
+
+#[derive(Serialize, Deserialize)]
+struct LutFile {
+ checksum: HashMap,
+ compressed_8: Vec,
+ compressed_16: Vec,
+}
+
+impl LutFile {
+ pub fn from_bytes(bytes: &[u8]) -> Result {
+ let as_str = std::str::from_utf8(bytes)?;
+ Ok(Self::from_str(as_str)?)
+ }
+
+ pub fn from_str(s: &str) -> Result {
+ Ok(ron::from_str(s)?)
+ }
+
+ pub fn thug_pro() -> Result {
+ LutFile::from_bytes(LUT_THUGPRO_BYTES)
+ }
+}
+
+#[derive(Debug, Clone)]
+struct NameLut {
+ checksum: HashMap,
+ compressed_8: HashMap,
+ compressed_16: HashMap,
+}
+
+impl From<&LutFile> for NameLut {
+ fn from(file: &LutFile) -> Self {
+ Self {
+ checksum: file.checksum.clone(),
+ compressed_8: file
+ .compressed_8
+ .iter()
+ .enumerate()
+ .map(|(index, name)| (name.clone(), index as u8))
+ .collect(),
+ compressed_16: file
+ .compressed_16
+ .iter()
+ .enumerate()
+ .map(|(index, name)| (name.clone(), index as u16))
+ .collect(),
+ }
+ }
+}
+
+impl NameLut {
+ pub fn lookup(&self, name: &impl ToString) -> Option {
+ let name = name.to_string();
+
+ if let Some(checksum) = self.checksum.get(&name) {
+ Some(qb::Id::Checksum(*checksum))
+ } else if let Some(compressed8) = self.compressed_8.get(&name) {
+ Some(qb::Id::Compressed8(*compressed8))
+ } else if let Some(compressed16) = self.compressed_16.get(&name) {
+ Some(qb::Id::Compressed16(*compressed16))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct IdLut {
+ checksum: HashMap,
+ compressed_8: HashMap,
+ compressed_16: HashMap,
+}
+
+impl From<&LutFile> for IdLut {
+ fn from(file: &LutFile) -> Self {
+ Self {
+ checksum: file
+ .checksum
+ .iter()
+ .map(|(name, checksum)| (*checksum, name.clone()))
+ .collect(),
+ compressed_8: file
+ .compressed_8
+ .iter()
+ .cloned()
+ .enumerate()
+ .map(|(index, name)| (index as u8, name))
+ .collect(),
+ compressed_16: file
+ .compressed_16
+ .iter()
+ .cloned()
+ .enumerate()
+ .map(|(index, name)| (index as u16, name))
+ .collect(),
+ }
+ }
+}
+
+impl IdLut {
+ pub fn lookup(&self, id: qb::Id) -> Option<&String> {
+ match id {
+ qb::Id::Checksum(checksum) => self.checksum.get(&checksum),
+ qb::Id::Compressed8(index) => self.compressed_8.get(&index),
+ qb::Id::Compressed16(index) => self.compressed_16.get(&index),
+ qb::Id::None => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Lut {
+ name: NameLut,
+ id: IdLut,
+}
+
+impl From for Lut {
+ fn from(file: LutFile) -> Self {
+ Self {
+ name: NameLut::from(&file),
+ id: IdLut::from(&file),
+ }
+ }
+}
+
+impl Lut {
+ pub fn by_name(&self, name: &impl ToString) -> Result {
+ self.name
+ .lookup(name)
+ .ok_or(Error::NameNotFound(name.to_string()))
+ }
+
+ pub fn by_id(&self, id: qb::Id) -> Result<&String> {
+ self.id.lookup(id).ok_or(Error::IdNotFound(id))
+ }
+
+ pub fn thug_pro() -> Result {
+ Ok(Lut::from(LutFile::thug_pro()?))
+ }
+}
diff --git a/cascade/src/mutations/error.rs b/cascade/src/mutations/error.rs
deleted file mode 100644
index 050837e..0000000
--- a/cascade/src/mutations/error.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use std::{backtrace::Backtrace, fmt::Debug};
-
-use thiserror::Error;
-
-use crate::structure::{self, StructureError};
-
-#[derive(Error, Debug)]
-pub enum MutationError {
- #[error("a structure operation failed: {source}")]
- Structure {
- #[from]
- source: StructureError,
- backtrace: Backtrace,
- },
-
- #[error("symbol with checksum {0} could not be found")]
- SymbolNotFound(structure::NameChecksum),
-
- #[error("mutation spec is invalid; cannot start with leaf")]
- SpecRootIsLeaf,
-
- #[error("mutation spec is invalid; root must be at top")]
- SpecNodeIsRoot,
-}
diff --git a/cascade/src/mutations/mod.rs b/cascade/src/mutations/mod.rs
deleted file mode 100644
index febfa62..0000000
--- a/cascade/src/mutations/mod.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use crate::{mutations::spec::StructureMutation, save::SaveContent};
-
-mod error;
-mod spec;
-mod trickset;
-
-pub use error::MutationError;
-pub use trickset::Trickset;
-
-pub trait Mutation {
- fn data_mutation(&self) -> Option<&StructureMutation>;
- fn summary_mutation(&self) -> Option<&StructureMutation>;
-
- fn mutate(
- &self,
- content: SaveContent,
- ) -> Result {
- let data = match self.data_mutation() {
- Some(mutation) => Some(mutation.mutate_structure(content.data())?),
- None => None,
- };
-
- let summary = match self.summary_mutation() {
- Some(mutation) => {
- Some(mutation.mutate_structure(content.summary())?)
- }
- None => None,
- };
-
- let content =
- data.map(|data| content.with_data(data)).unwrap_or(content);
-
- let content = summary
- .map(|summary| content.with_summary(summary))
- .unwrap_or(content);
-
- Ok(content)
- }
-}
diff --git a/cascade/src/mutations/name.rs b/cascade/src/mutations/name.rs
deleted file mode 100644
index e69de29..0000000
diff --git a/cascade/src/mutations/spec.rs b/cascade/src/mutations/spec.rs
deleted file mode 100644
index 1fd670c..0000000
--- a/cascade/src/mutations/spec.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-use std::{collections::HashMap, sync::Arc};
-
-use super::error::MutationError;
-use crate::structure::{
- NameChecksum, Structure, StructureError, Symbol, Value,
-};
-
-// We define two types of datatypes for structure mutation here: `spec` and `mutation`.
-//
-// A spec simply defines the format of which keys should be mutated, and which actions
-// should be performed on those keys. It does not specify what values to copy.
-//
-// A mutation contains the same information, but also contains information about the
-// values to set.
-//
-// A spec can be populated from a structure to create a mutation. The spec actions will
-// be converted to mutation actions (or errors) using the values in the structure. For
-// example, a `CopyOrDelete` spec action will either result in a `Set` or `Delete`
-// mutation action depending on whether the given key is present. A `CopyRequired` will
-// either result in a `Set` or `MutationError` depending on whether the given key is
-// present.
-
-#[derive(Copy, Clone, Debug)]
-pub enum SpecAction {
- #[allow(dead_code)]
- // TODO: i promise this will be used in the cas randomizer i swear!!!!
- CopyOrRemove,
- CopyRequired,
-}
-
-impl SpecAction {
- pub fn populate(
- &self,
- name_checksum: NameChecksum,
- symbol: Option<&Symbol>,
- ) -> Result {
- match self {
- SpecAction::CopyOrRemove => match symbol {
- Some(symbol) => Ok(MutationAction::Set(symbol.clone())),
- None => Ok(MutationAction::Remove),
- },
- SpecAction::CopyRequired => match symbol {
- Some(symbol) => Ok(MutationAction::Set(symbol.clone())),
- None => Err(MutationError::SymbolNotFound(name_checksum)),
- },
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct SpecChildren {
- children: HashMap,
-}
-
-impl SpecChildren {
- fn build(children: I) -> Result
- where
- I: IntoIterator,
- {
- Ok(SpecChildren {
- children: children
- .into_iter()
- .map(|child| (child.name(), child))
- .collect(),
- })
- }
-
- pub fn populate_from_structure(
- &self,
- structure: Arc,
- ) -> Result {
- Ok(MutationChildren {
- children: self
- .children
- .iter()
- .map(|(name, node)| try {
- (*name, node.populate_from_symbol(structure.get(*name))?)
- })
- .collect::>()?,
- })
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum SpecNode {
- Node {
- name_checksum: NameChecksum,
- children: SpecChildren,
- },
- Leaf {
- name_checksum: NameChecksum,
- action: SpecAction,
- },
-}
-
-// Root
-pub struct Spec {
- children: SpecChildren,
-}
-
-impl Spec {
- pub fn build(children: I) -> Result
- where
- I: IntoIterator,
- {
- Ok(Self {
- children: SpecChildren::build(children)?,
- })
- }
-
- pub fn populate_from_structure(
- &self,
- structure: Arc,
- ) -> Result {
- Ok(StructureMutation {
- children: self.children.populate_from_structure(structure)?,
- })
- }
-}
-
-impl SpecNode {
- pub fn node(name: N, children: I) -> Result
- where
- I: IntoIterator,
- N: TryInto,
- {
- Ok(SpecNode::Node {
- name_checksum: name.try_into()?,
- children: SpecChildren::build(children)?,
- })
- }
-
- pub fn leaf(name: N, action: SpecAction) -> Result
- where
- N: TryInto,
- {
- Ok(SpecNode::Leaf {
- name_checksum: name.try_into()?,
- action,
- })
- }
-
- pub fn name(&self) -> NameChecksum {
- match self {
- SpecNode::Node {
- name_checksum,
- children: _,
- } => *name_checksum,
- SpecNode::Leaf {
- name_checksum,
- action: _,
- } => *name_checksum,
- }
- }
-
- pub fn populate_from_symbol(
- &self,
- symbol: Option<&Symbol>,
- ) -> Result {
- match self {
- SpecNode::Node {
- name_checksum,
- children,
- } => SpecNode::populate_node(symbol, *name_checksum, children),
- SpecNode::Leaf {
- name_checksum,
- action,
- } => SpecNode::populate_leaf(symbol, *name_checksum, action),
- }
- }
-
- fn populate_node(
- symbol: Option<&Symbol>,
- name_checksum: NameChecksum,
- children: &SpecChildren,
- ) -> Result {
- match symbol {
- Some(symbol) => Ok(MutationNode::Node {
- name_checksum,
- children: children
- .populate_from_structure(symbol.try_as_struct()?)?,
- }),
- None => Err(MutationError::SymbolNotFound(name_checksum)),
- }
- }
-
- fn populate_leaf(
- symbol: Option<&Symbol>,
- name_checksum: NameChecksum,
- action: &SpecAction,
- ) -> Result {
- Ok(MutationNode::Leaf {
- name_checksum,
- action: action.populate(name_checksum, symbol)?,
- })
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum MutationAction {
- Set(Symbol),
- Remove,
-}
-
-#[derive(Clone, Debug)]
-pub struct MutationChildren {
- children: HashMap,
-}
-
-impl MutationChildren {
- pub fn mutate_structure(
- &self,
- structure: Arc,
- ) -> Result, MutationError> {
- let mut children = self.children.clone();
-
- let existing_children = structure
- .iter()
- .map(|symbol| {
- let name_checksum = symbol.name_checksum();
-
- match children.remove(&name_checksum) {
- Some(node) => {
- Ok(node.mutate_symbol(structure.get(name_checksum))?)
- }
- None => Ok(Some(symbol.clone())),
- }
- })
- .collect::>, MutationError>>()?;
-
- let missing_children = children
- .drain()
- .map(|(_, node)| node.mutate_symbol(None))
- .collect::>, MutationError>>()?;
-
- Ok(existing_children
- .into_iter()
- .chain(missing_children)
- .filter_map(|symbol| symbol)
- .into_iter()
- .collect())
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum MutationNode {
- Node {
- name_checksum: NameChecksum,
- children: MutationChildren,
- },
- Leaf {
- name_checksum: NameChecksum,
- action: MutationAction,
- },
-}
-
-impl MutationNode {
- pub fn mutate_symbol(
- &self,
- symbol: Option<&Symbol>,
- ) -> Result