From ea954a9ac59952ff11b1b3f07436f2660543e573 Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 08:54:52 -0500 Subject: [PATCH 1/7] adding db hashing and steam build and pathc version parsing. more verbose patch/hotfix version string searching --- Cargo.lock | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + README.md | 123 +++++++++++++++++++++++++++++++++++++++++----- src/colorize.rs | 86 +++++++++++++++++++++++++++++++- src/db.rs | 90 ++++++++++++++++++++++++++++++++++ src/main.rs | 80 ++++++++++++++++++++++++++++++ 6 files changed, 496 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be6a964..bc528e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,32 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "blake3" +version = "1.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "cc" version = "1.4.0" @@ -62,6 +88,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.6.4" @@ -108,6 +140,27 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "equivalent" version = "1.0.2" @@ -124,8 +177,10 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" name = "gdse" version = "0.1.0" dependencies = [ + "blake3", "clap", "lib_gddb", + "time", ] [[package]] @@ -190,12 +245,33 @@ dependencies = [ "libc", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -214,6 +290,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "2.0.1" @@ -237,6 +333,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 55325e0..b612cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +blake3 = "1.8" clap = { version = "4.5", features = ["derive"] } lib_gddb = { git = "https://github.com/gregates/lib-gddb.git" } +time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } diff --git a/README.md b/README.md index 78c8ee9..304065f 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,114 @@ This can be a virtue if you don't strongly disagree with its opinions. gdse has been tested with game version 1.3.0 on linux only, and only for English text files, but in theory it should work on windows and with localizations. -There are not currently any precompiled binaries available. You must build from source. To use gdse, -you can: - -1. [Install rust](https://www.rust-lang.org/tools/install) -2. `git clone git@github.com:gregates/gdse.git` -3. `cd gdse` -4. `cargo run --release` - -You must set GRIM_DAWN_INSTALL_PATH environment to the path to your game installation for the -program to work. By default, it will write the modified text resources to -`$GRIM_DAWN_INSTALL_PATH/settings/`. Re-run any time Grim Dawn gets patched, if you see any missing -tag errors or incorrect colorings. +There are not currently any precompiled binaries available. You must build from source. + +### Quick Start (Windows, PowerShell) + +1. Install Rust (choose one option): + + Option A (recommended, PowerShell or cmd with winget): + + ```powershell + winget install --id Rustlang.Rustup -e --accept-source-agreements --accept-package-agreements + ``` + + Option B (manual installer from rust-lang.org): + - Go to https://www.rust-lang.org/tools/install + - Download and run `rustup-init.exe` + - Keep the default installation settings unless you have a specific reason to change them + +2. Open a new terminal, then verify tools are available wtih these commands: + + ```powershell + rustc --version + cargo --version + ``` + +3. Find your Grim Dawn install path in Steam: + - Open Steam library. + - Right-click Grim Dawn -> Manage -> Browse local files. + - Copy the folder path from the file explorer address bar. + +4. Set the required environment variable (replace the example path with your own): + + ```powershell + setx GRIM_DAWN_INSTALL_PATH "C:\Program Files (x86)\Steam\steamapps\common\Grim Dawn" + ``` + +5. Clone and run from the repo root: + + When you clone any repo (including gdse), you choose a local folder on your PC where it will be saved. + You must `cd` into that exact local folder before running cargo commands. + `C:\repos` below is only an example location: + + ```powershell + cd C:\repos + git clone git@github.com:gregates/gdse.git + cd C:\repos\gdse + cargo run --release + ``` + + If you cloned somewhere else, replace `C:\repos\gdse` with your own full path. + Example: if you cloned into Downloads, use `cd C:\Users\\Downloads\gdse`. + +Important: the Grim Dawn install path above is only an example. Use the actual path from your own installation. + +### Quick Start (Linux) + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +rustc --version +cargo --version +export GRIM_DAWN_INSTALL_PATH="/path/to/Grim Dawn" +git clone git@github.com:gregates/gdse.git +cd /path/where/you/cloned/gdse +cargo run --release +``` + +### Environment Variable Requirement + +`GRIM_DAWN_INSTALL_PATH` is required. gdse writes modified text resources to: + +`$GRIM_DAWN_INSTALL_PATH/settings/` + +Re-run gdse after game patches if you see missing tags or incorrect colors. + +To make this easier to track, gdse also writes `gdse-db-hash.txt` in the output folder. +Each run of `cargo run --release` appends one line in this format (local computer time, minute precision): + +` hash= steam_build_id= patch_versions=` + +`patch_versions` is inferred from release-marker comments found in game text (for example, lines starting with `#Patch`, `#Hotfix`, or `#Update`). + +### Common Windows Issues + +- `rustc` or `cargo` not found: + - Close and reopen your terminal after installing Rust. + - Confirm `%USERPROFILE%\\.cargo\\bin` is in your PATH. +- linker error about `link.exe` not found: + - Install Visual Studio Build Tools with the "Desktop development with C++" workload. +- `GRIM_DAWN_INSTALL_PATH` missing or wrong: + - Print it with `echo $env:GRIM_DAWN_INSTALL_PATH` in PowerShell. + - Update it with `setx GRIM_DAWN_INSTALL_PATH ""`. + +### Notes for AI Coding Agents + +If you are using Copilot, Claude Code, Codex, or another coding agent: + +1. Run commands from the repository root so Cargo can find `Cargo.toml`. +2. Check `GRIM_DAWN_INSTALL_PATH` before running: + + ```powershell + echo $env:GRIM_DAWN_INSTALL_PATH + ``` + +3. Prefer this command order when validating changes: + + ```powershell + cargo build --release + cargo run --release + ``` + +4. Do not run destructive git commands on a user's repo (for example, `git reset --hard`). diff --git a/src/colorize.rs b/src/colorize.rs index 4c107e6..3f502a8 100644 --- a/src/colorize.rs +++ b/src/colorize.rs @@ -7,7 +7,7 @@ //! WanezGD's placement cascade), and any file that changed is written whole — //! comments, Desc lines and untouched tags preserved — to the output directory. -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::io::{BufRead, Seek}; use std::path::Path; @@ -33,6 +33,90 @@ fn text_arcs(lang: &str) -> [String; 4] { ] } +/// Best-effort patch-version detection from decoded tag text. +/// +/// Looks for comment markers like `#Patch v1.3.1` inside `tags*.txt` records +/// from the language text archives and returns distinct discovered versions. +pub fn detect_patch_versions(lang: &str) -> Vec { + let base = install_path(); + let mut versions = BTreeSet::new(); + + for rel in text_arcs(lang) { + let Ok(mut arc) = Archive::open(base.join(&rel)) else { + continue; + }; + let Ok(records) = arc.iter_records() else { + continue; + }; + for record in records.flatten() { + if !record.id.contains("tag") || !record.id.ends_with(".txt") { + continue; + } + let text = String::from_utf8_lossy(&record.data); + for line in text.lines() { + if let Some(v) = parse_patch_version(line) { + versions.insert(v.to_string()); + } + } + } + } + + versions.into_iter().collect() +} + +fn parse_patch_version(line: &str) -> Option<&str> { + // Most releases use comment markers like `#Patch v1.3.1`, but some builds + // may use other words such as `Hotfix` or `Update`. + let lower = line.to_ascii_lowercase(); + let marker_idx = ["#patch", "#hotfix", "#update"] + .iter() + .filter_map(|m| lower.find(m)) + .min()?; + + let tail = line[marker_idx..].trim(); + extract_version_token(tail) +} + +fn extract_version_token(s: &str) -> Option<&str> { + for raw in s.split_whitespace() { + let trimmed = raw.trim_matches(|c: char| ",;:()[]{}\"'".contains(c)); + let core = trimmed + .strip_prefix('v') + .or_else(|| trimmed.strip_prefix('V')) + .unwrap_or(trimmed); + if is_versionish(core) { + return Some(core); + } + } + None +} + +fn is_versionish(s: &str) -> bool { + let mut chars = s.chars().peekable(); + let mut saw_dot = false; + let mut saw_digit = false; + let mut starts_with_digit = false; + + if let Some(c) = chars.peek().copied() { + starts_with_digit = c.is_ascii_digit(); + } + + for c in chars { + if c.is_ascii_digit() { + saw_digit = true; + } else if c == '.' { + saw_dot = true; + } else if c == '-' || c == '_' || c.is_ascii_alphabetic() { + // Allow suffixes like 1.3.1a or 1.3.1-hotfix. + continue; + } else { + return false; + } + } + + starts_with_digit && saw_digit && saw_dot +} + /// Infers tag colors, rewrites `lang`'s text bundles, and writes the changed /// `.txt` files under `out_dir`, printing one line per file written. pub fn run( diff --git a/src/db.rs b/src/db.rs index a1f2bc3..cbc9a8b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -6,6 +6,7 @@ use std::fs::{File, canonicalize}; use std::io::{BufRead, BufReader, Seek}; use std::path::PathBuf; +use blake3::Hasher; use lib_gddb::arz::{Database, RawRecord, Record}; /// Relative paths to the base game + expansion databases, in load order. @@ -42,6 +43,95 @@ pub fn open_all() -> Vec>> { dbs } +/// Computes a combined content hash across all available game database files. +/// +/// The hash is based on bytes from the same ARZ files used for data inference, +/// in load order, and includes each relative path as a separator/input. +pub fn database_hash() -> String { + let base = install_path(); + let mut hasher = Hasher::new(); + let mut saw_any = false; + + for rel in DBS { + let path = base.join(rel); + if !path.exists() { + continue; + } + saw_any = true; + hasher.update(rel.as_bytes()); + hasher.update(&[0]); + + let mut f = File::open(&path).unwrap_or_else(|e| { + eprintln!("Could not open database file {}: {e}", path.display()); + std::process::exit(1); + }); + std::io::copy(&mut f, &mut hasher).unwrap_or_else(|e| { + eprintln!("Could not read database file {}: {e}", path.display()); + std::process::exit(1); + }); + hasher.update(&[0xFF]); + } + + if !saw_any { + eprintln!("Could not read any database files under {}", base.display()); + std::process::exit(1); + } + + hasher.finalize().to_hex().to_string() +} + +/// Best-effort Steam build id lookup for Grim Dawn (App ID 219990). +/// +/// Returns None when the game was not installed via Steam, the manifest file +/// is not reachable from the install path, or the build id key is missing. +pub fn steam_build_id() -> Option { + let install = install_path(); + let mut candidates = Vec::new(); + + // Typical layout: /steamapps/common/Grim Dawn + if let Some(common_dir) = install.parent() { + if common_dir + .file_name() + .is_some_and(|name| name.eq_ignore_ascii_case("common")) + { + if let Some(steamapps_dir) = common_dir.parent() { + candidates.push(steamapps_dir.join("appmanifest_219990.acf")); + } + } + } + + // Also try any ancestor that is itself a steamapps directory. + for anc in install.ancestors() { + if anc + .file_name() + .is_some_and(|name| name.eq_ignore_ascii_case("steamapps")) + { + candidates.push(anc.join("appmanifest_219990.acf")); + } + } + + for manifest in candidates { + let Ok(text) = std::fs::read_to_string(&manifest) else { + continue; + }; + if let Some(v) = parse_acf_value(&text, "buildid") { + return Some(v.to_string()); + } + } + + None +} + +fn parse_acf_value<'a>(acf: &'a str, key: &str) -> Option<&'a str> { + for line in acf.lines() { + let parts: Vec<_> = line.trim().split('"').collect(); + if parts.len() >= 4 && parts[1] == key { + return Some(parts[3]); + } + } + None +} + /// Resolves every record whose id satisfies `keep`, across all databases. /// /// Filtering on the (cheap) record id before resolving avoids decompressing diff --git a/src/main.rs b/src/main.rs index 27e6956..b9259a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; +use std::{fs::OpenOptions, io::Write}; use clap::Parser; +use time::macros::format_description; +use time::{OffsetDateTime, UtcOffset}; mod color; mod colorize; @@ -32,6 +35,9 @@ struct Args { fn main() { let args = Args::parse(); let lang = args.language.to_lowercase(); + let db_hash = db::database_hash(); + let steam_build_id = db::steam_build_id().unwrap_or_else(|| "unknown".to_string()); + let patch_versions = colorize::detect_patch_versions(&lang); let mut dbs = db::open_all(); let out = args.out.unwrap_or_else(|| { db::install_path() @@ -43,5 +49,79 @@ fn main() { } else { DamageColors::Default }; + + let hash_file = out.join("gdse-db-hash.txt"); + match std::fs::read_to_string(&hash_file) { + Ok(previous_log) => { + if latest_logged_hash(&previous_log).is_some_and(|h| h == db_hash) { + println!("Database hash unchanged since last run; output should be up to date."); + } else { + println!("Database hash changed since last run; rewriting output."); + } + } + Err(_) => { + println!("No previous database hash found; generating output."); + } + } + + if patch_versions.is_empty() { + println!("No patch version label detected in text archives."); + } else { + println!( + "Detected patch version label(s): {}", + patch_versions.join(", ") + ); + } + colorize::run(&mut dbs, &out, &lang, damage_colors); + + let now = local_timestamp_minute(); + let patch_versions_field = if patch_versions.is_empty() { + "unknown".to_string() + } else { + patch_versions.join(",") + }; + let mut f = OpenOptions::new() + .create(true) + .append(true) + .open(&hash_file) + .unwrap_or_else(|e| { + eprintln!("Could not open {}: {e}", hash_file.display()); + std::process::exit(1); + }); + if let Err(e) = writeln!( + f, + "{now} hash={db_hash} steam_build_id={steam_build_id} patch_versions={patch_versions_field}" + ) { + eprintln!("Could not write {}: {e}", hash_file.display()); + std::process::exit(1); + } +} + +fn latest_logged_hash(log: &str) -> Option<&str> { + for line in log.lines().rev() { + if let Some(token) = line + .split_whitespace() + .find(|token| token.starts_with("hash=")) + { + return token.strip_prefix("hash="); + } + + // Backward compatibility with old lines: "YYYY-MM-DD HH:MM " + let tokens: Vec<_> = line.split_whitespace().collect(); + if tokens.len() >= 3 { + return Some(tokens[2]); + } + } + None +} + +fn local_timestamp_minute() -> String { + // Prefer local machine time for user-facing history output. + let now = UtcOffset::current_local_offset() + .map(|offset| OffsetDateTime::now_utc().to_offset(offset)) + .unwrap_or_else(|_| OffsetDateTime::now_utc()); + let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]"); + now.format(&fmt) + .unwrap_or_else(|_| "unknown-time".to_string()) } From f29cc3cce6829f8933d678594fa8e8f948b082f5 Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 09:10:18 -0500 Subject: [PATCH 2/7] adding user prompt and clear version diff display --- README.md | 6 +++ src/main.rs | 115 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 304065f..4e24d23 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,9 @@ There are not currently any precompiled binaries available. You must build from cargo run --release ``` + gdse will show whether it detected changes since your last run, then prompt: + `Proceed with recoloring run? [Y/N]`. + If you cloned somewhere else, replace `C:\repos\gdse` with your own full path. Example: if you cloned into Downloads, use `cd C:\Users\\Downloads\gdse`. @@ -116,6 +119,9 @@ cd /path/where/you/cloned/gdse cargo run --release ``` +gdse will show whether it detected changes since your last run, then prompt: +`Proceed with recoloring run? [Y/N]`. + ### Environment Variable Requirement `GRIM_DAWN_INSTALL_PATH` is required. gdse writes modified text resources to: diff --git a/src/main.rs b/src/main.rs index b9259a0..77b6e4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,26 +51,13 @@ fn main() { }; let hash_file = out.join("gdse-db-hash.txt"); - match std::fs::read_to_string(&hash_file) { - Ok(previous_log) => { - if latest_logged_hash(&previous_log).is_some_and(|h| h == db_hash) { - println!("Database hash unchanged since last run; output should be up to date."); - } else { - println!("Database hash changed since last run; rewriting output."); - } - } - Err(_) => { - println!("No previous database hash found; generating output."); - } - } - - if patch_versions.is_empty() { - println!("No patch version label detected in text archives."); - } else { - println!( - "Detected patch version label(s): {}", - patch_versions.join(", ") - ); + let previous = std::fs::read_to_string(&hash_file) + .ok() + .and_then(|log| latest_run_info(&log)); + print_change_summary(previous.as_ref(), &db_hash, &steam_build_id, &patch_versions); + if !confirm_run() { + println!("Canceled by user; no output files were changed."); + return; } colorize::run(&mut dbs, &out, &lang, damage_colors); @@ -98,24 +85,98 @@ fn main() { } } -fn latest_logged_hash(log: &str) -> Option<&str> { +#[derive(Debug)] +struct RunInfo { + hash: String, + steam_build_id: Option, + patch_versions: Option, +} + +fn latest_run_info(log: &str) -> Option { for line in log.lines().rev() { - if let Some(token) = line - .split_whitespace() - .find(|token| token.starts_with("hash=")) - { - return token.strip_prefix("hash="); + let mut hash = None; + let mut steam_build_id = None; + let mut patch_versions = None; + + for token in line.split_whitespace() { + if let Some(v) = token.strip_prefix("hash=") { + hash = Some(v.to_string()); + } else if let Some(v) = token.strip_prefix("steam_build_id=") { + steam_build_id = Some(v.to_string()); + } else if let Some(v) = token.strip_prefix("patch_versions=") { + patch_versions = Some(v.to_string()); + } + } + + if let Some(hash) = hash { + return Some(RunInfo { + hash, + steam_build_id, + patch_versions, + }); } // Backward compatibility with old lines: "YYYY-MM-DD HH:MM " let tokens: Vec<_> = line.split_whitespace().collect(); if tokens.len() >= 3 { - return Some(tokens[2]); + return Some(RunInfo { + hash: tokens[2].to_string(), + steam_build_id: None, + patch_versions: None, + }); } } None } +fn print_change_summary( + previous: Option<&RunInfo>, + current_hash: &str, + current_build_id: &str, + current_patch_versions: &[String], +) { + let current_patch_field = if current_patch_versions.is_empty() { + "unknown".to_string() + } else { + current_patch_versions.join(",") + }; + + if let Some(prev) = previous { + let hash_changed = prev.hash != current_hash; + let prev_build = prev.steam_build_id.as_deref().unwrap_or("unknown"); + let prev_patch = prev.patch_versions.as_deref().unwrap_or("unknown"); + let build_changed = prev_build != current_build_id; + let patch_changed = prev_patch != current_patch_field; + + if hash_changed || build_changed || patch_changed { + println!("Changes detected since last run."); + } else { + println!("No changes detected since last run."); + } + println!("Steam build id: last={prev_build} current={current_build_id}"); + println!("Patch version label(s): last={prev_patch} current={current_patch_field}"); + } else { + println!("No previous run marker found."); + println!("Steam build id: last=unknown current={current_build_id}"); + println!("Patch version label(s): last=unknown current={current_patch_field}"); + } +} + +fn confirm_run() -> bool { + print!("Proceed with recoloring run? [Y/N]: "); + if let Err(e) = std::io::stdout().flush() { + eprintln!("Could not flush prompt to stdout: {e}"); + std::process::exit(1); + } + + let mut input = String::new(); + if let Err(e) = std::io::stdin().read_line(&mut input) { + eprintln!("Could not read user input: {e}"); + std::process::exit(1); + } + matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes") +} + fn local_timestamp_minute() -> String { // Prefer local machine time for user-facing history output. let now = UtcOffset::current_local_offset() From ab385a0479beb6fd19676d55a6ae85488b9ccdea Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 09:19:29 -0500 Subject: [PATCH 3/7] adding elmodor color palette and non-damage labels plus classes by skills --- src/colorize.rs | 34 +++++++++++++++++++++++ src/palette.rs | 2 ++ src/property.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/colorize.rs b/src/colorize.rs index 3f502a8..48aff52 100644 --- a/src/colorize.rs +++ b/src/colorize.rs @@ -188,6 +188,7 @@ fn recolor_file( ) -> (String, usize) { let mut out = String::with_capacity(text.len()); let mut colored = 0usize; + let mut class_values: Vec<(String, String)> = Vec::new(); for segment in text.split_inclusive('\n') { let (line, eol) = split_eol(segment); if let Some((tag, value)) = line.split_once('=') { @@ -212,6 +213,39 @@ fn recolor_file( out.push_str(eol); continue; } + if let Some(color) = property::color_other_for(tag) { + let new_value = apply_color(value, color); + if new_value != value { + colored += 1; + } + out.push_str(tag); + out.push('='); + out.push_str(&new_value); + out.push_str(eol); + continue; + } + + if let Some((name, class_value)) = property::text_class(tag, value) { + // Capture localized class names and append them to class skill names later. + class_values.push((name, class_value)); + out.push_str(tag); + out.push('='); + out.push_str(value); + out.push_str(eol); + continue; + } + + if let Some(suffix) = property::text_for(tag, &class_values) { + let new_value = format!("{value} {suffix}"); + if new_value != value { + colored += 1; + } + out.push_str(tag); + out.push('='); + out.push_str(&new_value); + out.push_str(eol); + continue; + } } out.push_str(segment); } diff --git a/src/palette.rs b/src/palette.rs index d7ce01a..8521ca5 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -19,6 +19,8 @@ pub const WHITE: char = 'w'; pub const YELLOW: char = 'y'; /// `10EB5D` pub const GREEN: char = 'g'; +/// `00A819` +pub const DARK_GREEN: char = 'x'; /// `F1E78C` pub const KHAKI: char = 'k'; /// `FF69B5` diff --git a/src/property.rs b/src/property.rs index 99d158e..f1aec47 100644 --- a/src/property.rs +++ b/src/property.rs @@ -12,7 +12,8 @@ //! no element token and so are left untouched. use crate::palette::{ - AQUA, COBALT, CYAN, FUSHIA, KHAKI, MAROON, OLIVE, ORANGE, PURPLE, RED, YELLOW, + AQUA, COBALT, CYAN, DARK_GREEN, FUSHIA, KHAKI, MAROON, OLIVE, ORANGE, PURPLE, + RED, WHITE, YELLOW, }; /// Structural prefixes that mark a damage / resistance / retaliation / @@ -104,3 +105,73 @@ pub fn color_for(tag: &str, damage_colors: DamageColors) -> Option { } Some(color) } + +// Additional non-damage stat labels that benefit from consistent coloring. +const PREFIXES_OTHER: [&str; 4] = [ + "tagChar", + "tagDamageModifier", + "ItemMasteryIncrement", + "ItemAllSkillIncrement", +]; + +const TOKENS_OTHER: [(&str, char); 13] = [ + ("tagCharAttribute0", WHITE), + ("ItemMasteryIncrement", DARK_GREEN), + ("ItemAllSkillIncrement", DARK_GREEN), + ("tagCharRunSpeed", DARK_GREEN), + ("tagCharSpellCastSpeed", DARK_GREEN), + ("tagCharAttackSpeed", DARK_GREEN), + ("tagCharTotalSpeedModifier", DARK_GREEN), + ("tagCharRunSpeedModifier", DARK_GREEN), + ("tagCharOffensiveAbility", DARK_GREEN), + ("tagCharDefensiveAbility", DARK_GREEN), + ("tagDamageModifierCritDamage", DARK_GREEN), + ("tagDamageModifierDamageMult", DARK_GREEN), + ("tagDamageModifierTotalDamage", DARK_GREEN), +]; + +pub fn color_other_for(tag: &str) -> Option { + if !PREFIXES_OTHER.iter().any(|p| tag.starts_with(p)) { + return None; + } + TOKENS_OTHER + .iter() + .find(|(tok, _)| tag.contains(tok)) + .map(|(_, color)| *color) +} + +const CLASSES: [&str; 4] = ["tagClass", "tagGDX1Class", "tagGDX2Class", "tagGDX3Class"]; + +pub fn text_class(tag: &str, value: &str) -> Option<(String, String)> { + match tag { + "tagSkillClassName01" => Some(("Class01Skill".to_string(), value.to_string())), + "tagSkillClassName02" => Some(("Class02Skill".to_string(), value.to_string())), + "tagSkillClassName03" => Some(("Class03Skill".to_string(), value.to_string())), + "tagSkillClassName04" => Some(("Class04Skill".to_string(), value.to_string())), + "tagSkillClassName05" => Some(("Class05Skill".to_string(), value.to_string())), + "tagSkillClassName06" => Some(("Class06Skill".to_string(), value.to_string())), + "tagSkillClassName07" => Some(("Class07Skill".to_string(), value.to_string())), + "tagSkillClassName08" => Some(("Class08Skill".to_string(), value.to_string())), + "tagSkillClassName09" => Some(("Class09Skill".to_string(), value.to_string())), + "tagSkillClassName10" => Some(("Class10Skill".to_string(), value.to_string())), + _ => None, + } +} + +pub fn text_for(tag: &str, class_values: &[(String, String)]) -> Option { + if !CLASSES.iter().any(|p| tag.starts_with(p)) { + return None; + } + if !tag.contains("Name") { + return None; + } + // This contains the class name itself, not a skill. + if tag.contains("SkillName00A") { + return None; + } + let class = class_values + .iter() + .find(|(tok, _)| tag.contains(tok)) + .map(|(_, class)| class)?; + Some(format!("({class})")) +} From ff0729f9a535a2e8cad9c803cb0d8dde2aaa5752 Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 10:21:22 -0500 Subject: [PATCH 4/7] color palette concept and extending user flow. Testing defaults --- README.md | 102 ++++++++++++++++++++++++- src/color.rs | 9 ++- src/colorize.rs | 18 +++-- src/main.rs | 34 ++++++++- src/palette.rs | 6 +- src/property.rs | 55 ++++++++------ src/user_palette.rs | 177 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 37 deletions(-) create mode 100644 src/user_palette.rs diff --git a/README.md b/README.md index 4e24d23..fb07ad5 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ There are not currently any precompiled binaries available. You must build from cargo run --release ``` - gdse will show whether it detected changes since your last run, then prompt: + gdse will first ask whether to use default colors or a custom palette file, + then show whether it detected changes since your last run, then prompt: `Proceed with recoloring run? [Y/N]`. If you cloned somewhere else, replace `C:\repos\gdse` with your own full path. @@ -119,7 +120,8 @@ cd /path/where/you/cloned/gdse cargo run --release ``` -gdse will show whether it detected changes since your last run, then prompt: +gdse will first ask whether to use default colors or a custom palette file, +then show whether it detected changes since your last run, then prompt: `Proceed with recoloring run? [Y/N]`. ### Environment Variable Requirement @@ -137,6 +139,102 @@ Each run of `cargo run --release` appends one line in this format (local compute `patch_versions` is inferred from release-marker comments found in game text (for example, lines starting with `#Patch`, `#Hotfix`, or `#Update`). +### Current Color Palette + +gdse uses Grim Dawn's built-in color-code letters from `gd-colorcodes.json`. +The tables below show the current mappings in plain English plus the game code +letter and hex value. + +Note: item/tooltip text is rendered on dark/black backgrounds in Grim Dawn, so +high-contrast bright colors are strongly recommended for readability. + +#### Rarity Colors + +| Category | Color Name | Game Code | Hex | +|---|---|---|---| +| Common item/affix | White | `w` | `#FFFFFF` | +| Magical item/affix | Yellow | `y` | `#FFF62C` | +| Rare item/affix | Green | `g` | `#10EB5D` | + +#### Damage-Type Property Colors + +| Category | Color Name | Game Code | Hex | +|---|---|---|---| +| Physical | Khaki | `k` | `#F1E78C` | +| Pierce (gdse default) | Fushia/Pink | `f` | `#FF69B5` | +| Bleeding | Red | `r` | `#FF4200` | +| Fire | Orange | `o` | `#F3A44D` | +| Cold | Cyan | `c` | `#00FFFF` | +| Lightning | Cobalt | `z` | `#6A91E0` | +| Poison/Acid | Olive | `l` | `#92CC00` | +| Vitality/Life | Maroon | `m` | `#800000` | +| Aether | Aqua | `a` | `#80FFD5` | +| Chaos | Purple | `p` | `#BD94C6` | +| Elemental | Yellow | `y` | `#FFF62C` | + +#### Non-Damage Property Colors + +| Category | Color Name | Game Code | Hex | +|---|---|---|---| +| Cunning/Spirit/Physique aggregate (`tagCharAttribute0`) | Grayish Orange (highlight) | `h` | (engine highlight color) | +| Mastery/All Skills increments | Teal | `t` | `#00FFD2` | +| OA/DA/Speed/Crit/Total damage modifier group | Uncolored by default | (none) | (inherits game default text color) | + +### Custom Palette File + +If you want your own colors, create `gdse-palette.txt` in the same folder where +you run `cargo run --release`, or pass a custom file path with `--palette-file`. + +In the interactive run flow, if you answer `N` to `Use default gdse palette? [Y/N]`, +gdse exits immediately and asks you to create `gdse-palette.txt`, then run again. + +Format is one `key=letter` per line. Blank lines and `# comments` are allowed. + +Example: + +```text +# Rarity +rarity.common=w +rarity.magical=y +rarity.rare=g + +# Damage +damage.physical=k +damage.pierce=f +damage.bleeding=r +damage.fire=o +damage.cold=c +damage.lightning=z +damage.poison=l +damage.vitality=m +damage.life=m +damage.aether=a +damage.chaos=p +damage.elemental=y + +# Non-damage +nondamage.attribute0=h +nondamage.mastery_increment=t +nondamage.all_skill_increment=t + +# Optional overrides for categories that are uncolored by default: +# nondamage.run_speed=g +# nondamage.cast_speed=g +# nondamage.attack_speed=g +# nondamage.total_speed=g +# nondamage.run_speed_modifier=g +# nondamage.offensive_ability=g +# nondamage.defensive_ability=g +# nondamage.crit_damage=g +# nondamage.damage_mult=g +# nondamage.total_damage=g +``` + +If a key is omitted, gdse keeps the built-in default for that category. + +You can override just one category if you want. Example: a file containing only +`damage.chaos=f` changes Chaos to hot pink and leaves every other color on gdse defaults. + ### Common Windows Issues - `rustc` or `cargo` not found: diff --git a/src/color.rs b/src/color.rs index e6ec337..7594d4d 100644 --- a/src/color.rs +++ b/src/color.rs @@ -12,19 +12,20 @@ use crate::infer::TagInfo; use crate::keywords::{Kind, Rarity}; use crate::palette::{GREEN, WHITE, YELLOW}; +use crate::user_palette::UserPalette; /// The color letter to bake into the tag's value, or `None` if it's left /// untouched. Base item names are only colored when they can take a name- /// altering affix (otherwise the engine's native rarity color is fine and there /// is no bleed to guard against); affix tags are always colored by their rarity. -pub fn color_for(info: &TagInfo) -> Option { +pub fn color_for(info: &TagInfo, user_palette: &UserPalette) -> Option { if info.kind == Kind::Item && !info.affixable { return None; } match info.rarity { - Rarity::Common => Some(WHITE), - Rarity::Magical => Some(YELLOW), - Rarity::Rare => Some(GREEN), + Rarity::Common => Some(user_palette.rarity(Rarity::Common).unwrap_or(WHITE)), + Rarity::Magical => Some(user_palette.rarity(Rarity::Magical).unwrap_or(YELLOW)), + Rarity::Rare => Some(user_palette.rarity(Rarity::Rare).unwrap_or(GREEN)), Rarity::Epic | Rarity::Legendary => None, } } diff --git a/src/colorize.rs b/src/colorize.rs index 48aff52..cd5beed 100644 --- a/src/colorize.rs +++ b/src/colorize.rs @@ -18,6 +18,7 @@ use crate::color; use crate::db::install_path; use crate::infer; use crate::property::{self, DamageColors}; +use crate::user_palette::UserPalette; /// The text bundles for a language, base game + expansions, in load order. EN /// ships one arc per part; other languages bundle everything into the base arc @@ -124,8 +125,9 @@ pub fn run( out_dir: &Path, lang: &str, damage_colors: DamageColors, + user_palette: &UserPalette, ) { - let colors = color_map(dbs); + let colors = color_map(dbs, user_palette); if let Err(e) = std::fs::create_dir_all(out_dir) { eprintln!("Could not create {}: {e}", out_dir.display()); @@ -147,7 +149,7 @@ pub fn run( continue; } let text = String::from_utf8_lossy(&record.data); - let (rewritten, colored) = recolor_file(&text, &colors, damage_colors); + let (rewritten, colored) = recolor_file(&text, &colors, damage_colors, user_palette); if colored == 0 { continue; } @@ -171,10 +173,13 @@ pub fn run( /// Builds the tag -> color-letter map for the DB-inferred item / affix tags. /// Damage-type Property tags aren't listed here: they're recognized by name on /// the fly in `recolor_file` (see `property::color_for`). -fn color_map(dbs: &mut [Database]) -> HashMap { +fn color_map( + dbs: &mut [Database], + user_palette: &UserPalette, +) -> HashMap { infer::infer(dbs) .into_iter() - .filter_map(|(tag, info)| color::color_for(&info).map(|c| (tag, c))) + .filter_map(|(tag, info)| color::color_for(&info, user_palette).map(|c| (tag, c))) .collect() } @@ -185,6 +190,7 @@ fn recolor_file( text: &str, colors: &HashMap, damage_colors: DamageColors, + user_palette: &UserPalette, ) -> (String, usize) { let mut out = String::with_capacity(text.len()); let mut colored = 0usize; @@ -196,7 +202,7 @@ fn recolor_file( if let Some(color) = colors .get(tag) .copied() - .or_else(|| property::color_for(tag, damage_colors)) + .or_else(|| property::color_for(tag, damage_colors, user_palette)) { let mut new_value = apply_color(value, color); // Conversion labels carry no placeholder, so the color would @@ -213,7 +219,7 @@ fn recolor_file( out.push_str(eol); continue; } - if let Some(color) = property::color_other_for(tag) { + if let Some(color) = property::color_other_for(tag, user_palette) { let new_value = apply_color(value, color); if new_value != value { colored += 1; diff --git a/src/main.rs b/src/main.rs index 77b6e4b..cc52686 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod infer; mod keywords; mod palette; mod property; +mod user_palette; use property::DamageColors; @@ -30,6 +31,9 @@ struct Args { /// Paints Pierce red, like rainbow filter, not pink. #[arg(long)] rainbow_filter_damage_colors: bool, + /// Optional palette config file (key=value lines). Defaults to ./gdse-palette.txt if present. + #[arg(long)] + palette_file: Option, } fn main() { @@ -38,6 +42,8 @@ fn main() { let db_hash = db::database_hash(); let steam_build_id = db::steam_build_id().unwrap_or_else(|| "unknown".to_string()); let patch_versions = colorize::detect_patch_versions(&lang); + let palette_file = choose_palette_file(args.palette_file.as_deref()); + let palette_load = user_palette::load(palette_file.as_deref()); let mut dbs = db::open_all(); let out = args.out.unwrap_or_else(|| { db::install_path() @@ -49,6 +55,11 @@ fn main() { } else { DamageColors::Default }; + if let Some(source) = &palette_load.source { + println!("Loaded custom palette from {}", source.display()); + } else { + println!("Using default gdse palette."); + } let hash_file = out.join("gdse-db-hash.txt"); let previous = std::fs::read_to_string(&hash_file) @@ -60,7 +71,7 @@ fn main() { return; } - colorize::run(&mut dbs, &out, &lang, damage_colors); + colorize::run(&mut dbs, &out, &lang, damage_colors, &palette_load.palette); let now = local_timestamp_minute(); let patch_versions_field = if patch_versions.is_empty() { @@ -163,7 +174,26 @@ fn print_change_summary( } fn confirm_run() -> bool { - print!("Proceed with recoloring run? [Y/N]: "); + confirm_yes_no("Proceed with recoloring run? [Y/N]: ") +} + +fn choose_palette_file(cli_palette_file: Option<&std::path::Path>) -> Option { + if let Some(path) = cli_palette_file { + return Some(path.to_path_buf()); + } + + if confirm_yes_no("Use default gdse palette? [Y/N]: ") { + return None; + } + + println!( + "Custom palette selected. Create gdse-palette.txt in this folder, then run gdse again." + ); + std::process::exit(0); +} + +fn confirm_yes_no(prompt: &str) -> bool { + print!("{prompt}"); if let Err(e) = std::io::stdout().flush() { eprintln!("Could not flush prompt to stdout: {e}"); std::process::exit(1); diff --git a/src/palette.rs b/src/palette.rs index 8521ca5..c2891e0 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -19,8 +19,6 @@ pub const WHITE: char = 'w'; pub const YELLOW: char = 'y'; /// `10EB5D` pub const GREEN: char = 'g'; -/// `00A819` -pub const DARK_GREEN: char = 'x'; /// `F1E78C` pub const KHAKI: char = 'k'; /// `FF69B5` @@ -41,3 +39,7 @@ pub const MAROON: char = 'm'; pub const AQUA: char = 'a'; /// `BD94C6` pub const PURPLE: char = 'p'; +/// Grayish Orange (engine highlight color) +pub const HIGHLIGHT_ORANGE: char = 'h'; +/// `00FFD2` +pub const TEAL: char = 't'; diff --git a/src/property.rs b/src/property.rs index f1aec47..acdcc34 100644 --- a/src/property.rs +++ b/src/property.rs @@ -12,9 +12,10 @@ //! no element token and so are left untouched. use crate::palette::{ - AQUA, COBALT, CYAN, DARK_GREEN, FUSHIA, KHAKI, MAROON, OLIVE, ORANGE, PURPLE, - RED, WHITE, YELLOW, + AQUA, COBALT, CYAN, FUSHIA, HIGHLIGHT_ORANGE, KHAKI, MAROON, OLIVE, ORANGE, + PURPLE, RED, TEAL, YELLOW, }; +use crate::user_palette::UserPalette; /// Structural prefixes that mark a damage / resistance / retaliation / /// conversion stat label. A property tag always starts with one of these. @@ -62,7 +63,7 @@ const TOKENS: [(&str, char); 12] = [ ]; /// The color letter for a damage-type Property tag, or `None` if `tag` isn't one. -pub fn color_for(tag: &str, damage_colors: DamageColors) -> Option { +pub fn color_for(tag: &str, damage_colors: DamageColors, user_palette: &UserPalette) -> Option { if !PREFIXES.iter().any(|p| tag.starts_with(p)) { return None; } @@ -97,7 +98,7 @@ pub fn color_for(tag: &str, damage_colors: DamageColors) -> Option { let color = TOKENS .iter() .find(|(tok, _)| tag.contains(tok)) - .map(|(_, color)| *color)?; + .map(|(tok, color)| user_palette.damage(tok).unwrap_or(*color))?; // Opt back in to Full Rainbow's Red Pierce. Fushia is the only entry gdse // departs on, so folding it back to Red is the whole of the flag. if damage_colors == DamageColors::RainbowFilter && color == FUSHIA { @@ -114,30 +115,42 @@ const PREFIXES_OTHER: [&str; 4] = [ "ItemAllSkillIncrement", ]; -const TOKENS_OTHER: [(&str, char); 13] = [ - ("tagCharAttribute0", WHITE), - ("ItemMasteryIncrement", DARK_GREEN), - ("ItemAllSkillIncrement", DARK_GREEN), - ("tagCharRunSpeed", DARK_GREEN), - ("tagCharSpellCastSpeed", DARK_GREEN), - ("tagCharAttackSpeed", DARK_GREEN), - ("tagCharTotalSpeedModifier", DARK_GREEN), - ("tagCharRunSpeedModifier", DARK_GREEN), - ("tagCharOffensiveAbility", DARK_GREEN), - ("tagCharDefensiveAbility", DARK_GREEN), - ("tagDamageModifierCritDamage", DARK_GREEN), - ("tagDamageModifierDamageMult", DARK_GREEN), - ("tagDamageModifierTotalDamage", DARK_GREEN), +const TOKENS_OTHER: [(&str, &str, Option); 13] = [ + ("tagCharAttribute0", "nondamage.attribute0", Some(HIGHLIGHT_ORANGE)), + ( + "ItemMasteryIncrement", + "nondamage.mastery_increment", + Some(TEAL), + ), + ( + "ItemAllSkillIncrement", + "nondamage.all_skill_increment", + Some(TEAL), + ), + ("tagCharRunSpeed", "nondamage.run_speed", None), + ("tagCharSpellCastSpeed", "nondamage.cast_speed", None), + ("tagCharAttackSpeed", "nondamage.attack_speed", None), + ("tagCharTotalSpeedModifier", "nondamage.total_speed", None), + ( + "tagCharRunSpeedModifier", + "nondamage.run_speed_modifier", + None, + ), + ("tagCharOffensiveAbility", "nondamage.offensive_ability", None), + ("tagCharDefensiveAbility", "nondamage.defensive_ability", None), + ("tagDamageModifierCritDamage", "nondamage.crit_damage", None), + ("tagDamageModifierDamageMult", "nondamage.damage_mult", None), + ("tagDamageModifierTotalDamage", "nondamage.total_damage", None), ]; -pub fn color_other_for(tag: &str) -> Option { +pub fn color_other_for(tag: &str, user_palette: &UserPalette) -> Option { if !PREFIXES_OTHER.iter().any(|p| tag.starts_with(p)) { return None; } TOKENS_OTHER .iter() - .find(|(tok, _)| tag.contains(tok)) - .map(|(_, color)| *color) + .find(|(tok, _, _)| tag.contains(tok)) + .and_then(|(_, key, default)| user_palette.non_damage(key).or(*default)) } const CLASSES: [&str; 4] = ["tagClass", "tagGDX1Class", "tagGDX2Class", "tagGDX3Class"]; diff --git a/src/user_palette.rs b/src/user_palette.rs new file mode 100644 index 0000000..d31ab2e --- /dev/null +++ b/src/user_palette.rs @@ -0,0 +1,177 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use crate::keywords::Rarity; + +#[derive(Debug, Clone)] +pub struct UserPalette { + values: HashMap, +} + +impl UserPalette { + pub fn empty() -> Self { + Self { + values: HashMap::new(), + } + } + + pub fn get(&self, key: &str) -> Option { + self.values.get(key).copied() + } + + pub fn rarity(&self, rarity: Rarity) -> Option { + match rarity { + Rarity::Common => self.get("rarity.common"), + Rarity::Magical => self.get("rarity.magical"), + Rarity::Rare => self.get("rarity.rare"), + Rarity::Epic | Rarity::Legendary => None, + } + } + + pub fn damage(&self, token: &str) -> Option { + match token { + "Physical" => self.get("damage.physical"), + "Pierce" => self.get("damage.pierce"), + "Bleeding" => self.get("damage.bleeding"), + "Fire" => self.get("damage.fire"), + "Cold" => self.get("damage.cold"), + "Lightning" => self.get("damage.lightning"), + "Poison" => self.get("damage.poison"), + "Vitality" => self.get("damage.vitality"), + "Life" => self.get("damage.life"), + "Aether" => self.get("damage.aether"), + "Chaos" => self.get("damage.chaos"), + "Elemental" => self.get("damage.elemental"), + _ => None, + } + } + + pub fn non_damage(&self, key: &str) -> Option { + self.get(key) + } +} + +pub struct PaletteLoad { + pub palette: UserPalette, + pub source: Option, +} + +pub fn load(path_arg: Option<&Path>) -> PaletteLoad { + if let Some(path) = path_arg { + let palette = read_palette_file(path); + return PaletteLoad { + palette, + source: Some(path.to_path_buf()), + }; + } + + let default = Path::new("gdse-palette.txt"); + if default.exists() { + let palette = read_palette_file(default); + PaletteLoad { + palette, + source: Some(default.to_path_buf()), + } + } else { + PaletteLoad { + palette: UserPalette::empty(), + source: None, + } + } +} + +fn read_palette_file(path: &Path) -> UserPalette { + let text = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln!("Could not read palette file {}: {e}", path.display()); + std::process::exit(1); + }); + + let mut values = HashMap::new(); + for (line_no, raw) in text.lines().enumerate() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key_raw, val_raw)) = line.split_once('=') else { + eprintln!( + "Invalid palette line {} in {}: expected key=value", + line_no + 1, + path.display() + ); + std::process::exit(1); + }; + + let key = key_raw.trim().to_ascii_lowercase(); + if !is_known_key(&key) { + eprintln!( + "Unknown palette key '{}' on line {} in {}", + key, + line_no + 1, + path.display() + ); + std::process::exit(1); + } + + let value = val_raw.trim(); + let code = parse_color_code(value).unwrap_or_else(|| { + eprintln!( + "Invalid color code '{}' on line {} in {}. Use one letter like w, y, g, f, r.", + value, + line_no + 1, + path.display() + ); + std::process::exit(1); + }); + + values.insert(key, code); + } + + UserPalette { values } +} + +fn parse_color_code(s: &str) -> Option { + let mut chars = s.chars(); + let c = chars.next()?; + if chars.next().is_some() { + return None; + } + if c.is_ascii_alphabetic() { + Some(c.to_ascii_lowercase()) + } else { + None + } +} + +fn is_known_key(key: &str) -> bool { + matches!( + key, + "rarity.common" + | "rarity.magical" + | "rarity.rare" + | "damage.physical" + | "damage.pierce" + | "damage.bleeding" + | "damage.fire" + | "damage.cold" + | "damage.lightning" + | "damage.poison" + | "damage.vitality" + | "damage.life" + | "damage.aether" + | "damage.chaos" + | "damage.elemental" + | "nondamage.attribute0" + | "nondamage.mastery_increment" + | "nondamage.all_skill_increment" + | "nondamage.run_speed" + | "nondamage.cast_speed" + | "nondamage.attack_speed" + | "nondamage.total_speed" + | "nondamage.run_speed_modifier" + | "nondamage.offensive_ability" + | "nondamage.defensive_ability" + | "nondamage.crit_damage" + | "nondamage.damage_mult" + | "nondamage.total_damage" + ) +} From 22443c15a8c89129748cb8752f33b89b006cb7b8 Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 10:40:45 -0500 Subject: [PATCH 5/7] readme clarifications --- README.md | 74 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index fb07ad5..c9a51c3 100644 --- a/README.md +++ b/README.md @@ -190,51 +190,53 @@ gdse exits immediately and asks you to create `gdse-palette.txt`, then run again Format is one `key=letter` per line. Blank lines and `# comments` are allowed. -Example: +Example (single override): ```text -# Rarity -rarity.common=w -rarity.magical=y -rarity.rare=g - -# Damage -damage.physical=k -damage.pierce=f -damage.bleeding=r -damage.fire=o -damage.cold=c -damage.lightning=z -damage.poison=l -damage.vitality=m -damage.life=m -damage.aether=a damage.chaos=p -damage.elemental=y - -# Non-damage -nondamage.attribute0=h -nondamage.mastery_increment=t -nondamage.all_skill_increment=t - -# Optional overrides for categories that are uncolored by default: -# nondamage.run_speed=g -# nondamage.cast_speed=g -# nondamage.attack_speed=g -# nondamage.total_speed=g -# nondamage.run_speed_modifier=g -# nondamage.offensive_ability=g -# nondamage.defensive_ability=g -# nondamage.crit_damage=g -# nondamage.damage_mult=g -# nondamage.total_damage=g ``` -If a key is omitted, gdse keeps the built-in default for that category. +This is totally valid: one line is enough. Any key you do not include keeps the +built-in gdse default for that category. You can override just one category if you want. Example: a file containing only `damage.chaos=f` changes Chaos to hot pink and leaves every other color on gdse defaults. +#### Palette Keys Reference + +Use these keys in `gdse-palette.txt`: + +| Key | Controls | +|---|---| +| `rarity.common` | Common item/affix rarity color | +| `rarity.magical` | Magical item/affix rarity color | +| `rarity.rare` | Rare item/affix rarity color | +| `damage.physical` | Physical damage property color | +| `damage.pierce` | Pierce damage property color | +| `damage.bleeding` | Bleeding damage property color | +| `damage.fire` | Fire damage property color | +| `damage.cold` | Cold damage property color | +| `damage.lightning` | Lightning damage property color | +| `damage.poison` | Poison/Acid damage property color | +| `damage.vitality` | Vitality damage property color | +| `damage.life` | Life-based property color | +| `damage.aether` | Aether damage property color | +| `damage.chaos` | Chaos damage property color | +| `damage.elemental` | Elemental damage property color | +| `nondamage.attribute0` | Cunning/Spirit/Physique aggregate color | +| `nondamage.mastery_increment` | +Mastery level line color | +| `nondamage.all_skill_increment` | +All Skills line color | +| `nondamage.run_speed` | Run speed line color | +| `nondamage.cast_speed` | Cast speed line color | +| `nondamage.attack_speed` | Attack speed line color | +| `nondamage.total_speed` | Total speed modifier line color | +| `nondamage.run_speed_modifier` | Run speed modifier line color | +| `nondamage.offensive_ability` | Offensive Ability line color | +| `nondamage.defensive_ability` | Defensive Ability line color | +| `nondamage.crit_damage` | Crit damage modifier line color | +| `nondamage.damage_mult` | Damage multiplier line color | +| `nondamage.total_damage` | Total damage modifier line color | + ### Common Windows Issues - `rustc` or `cargo` not found: From d4080c520c6e6a83c8c28b9f3ccae025fa5b7e8d Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 11:56:25 -0500 Subject: [PATCH 6/7] pallete tweaks and readme --- README.md | 234 +++++++++++++++++++++++--------------------- src/color.rs | 9 +- src/user_palette.rs | 5 +- 3 files changed, 132 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index c9a51c3..5a7bf95 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,57 @@ +# Contents + +- [Contents](#contents) + - [Overview](#overview) + - [Why not just use Rainbow Filter?](#why-not-just-use-rainbow-filter) + - [Installation \& Usage](#installation--usage) + - [Setup - Windows OS](#setup---windows-os) + - [Quick Start (Linux)](#quick-start-linux) + - [Environment Variable Requirement](#environment-variable-requirement) + - [Current Color Palette](#current-color-palette) + - [Rarity Colors](#rarity-colors) + - [Damage-Type Property Colors](#damage-type-property-colors) + - [Non-Damage Property Colors](#non-damage-property-colors) + - [Grim Dawn Color Code Reference](#grim-dawn-color-code-reference) + - [Custom Palette File](#custom-palette-file) + - [Common Windows Issues](#common-windows-issues) + - [Notes for AI Coding Agents](#notes-for-ai-coding-agents) +--- ## Overview - -gdse is a simple, fully-automatic alternative to the popular [Rainbow -Filter](https://forums.crateentertainment.com/t/tool-rainbow-filter-item-highlighting/42765/917) mod -for the action RPG Grim Dawn, created by [Crate Entertainment](https://www.crateentertainment.com/). +gdse is a simple, fully-automatic alternative to the popular [Rainbow Filter](https://forums.crateentertainment.com/t/tool-rainbow-filter-item-highlighting/42765/917) mod for the action RPG Grim Dawn, created by [Crate Entertainment](https://www.crateentertainment.com/). It recolors game text for items in two ways: -1. Separate coloring for affix rarity and base item rarity, for Common, Magic, and Rare items that - roll affixes. +1. Separate coloring for affix rarity and base item rarity, for Common, Magic, and Rare items that roll affixes. 2. Colored text in item tooltips for different damage types. image -These are the two features of rainbow filter that the author of gdse finds essential and can't play -Grim Dawn without. All other features of Rainbow Filter are omitted. One other small difference from -Rainbow Filter is that gdse colors Pierce damage pink, not red, to distinguish it from Bleed (you can -opt-in to Rainbow Filter's exact damage color scheme if you prefer it with the -`--rainbow-filter-damage-colors` flag). +These are the two features of Rainbow Filter that the author of gdse finds essential and can't play Grim Dawn without. All other features of Rainbow Filter are omitted. One other small difference from Rainbow Filter is that gdse colors Pierce damage pink, not red, to distinguish it from Bleed (you can opt in to Rainbow Filter-like Pierce coloring by setting `damage.pierce=r` in your custom palette file (see [Custom Palette File](#custom-palette-file)). + +gdse works by writing modified copies of some game resources to the `/settings/` directory of your Grim Dawn installation, just like Rainbow Filter. It's not compatible with Rainbow Filter or other mods that change game text. If new text is added to the game by a patch, you may see missing tag warnings until you re-run gdse. -gdse works by writing modified copies of some game resources to the `/settings/` directory of your -Grim Dawn installation, just like Rainbow Filter. It's not compatible with Rainbow Filter or other -mods that change game text. If new text is added to the game by a patch, you may see missing tag -warnings until you re-run gdse. +--- ## Why not just use Rainbow Filter? -Great question! I used Rainbow Filter for years. It's a great mod. You should continue to use it if -it works for you. +Great question! I used Rainbow Filter for years. It's a great mod. You should continue to use it if it works for you. + +The main motivation for gdse was that Rainbow Filter depends on hand-crafted configuration that needs to be updated after every game patch. That gives Rainbow Filter a lot of power and flexibility, but it also causes delays — it takes time to produce a new Rainbow Filter mod after each patch. -The main motivation for gdse was that Rainbow Filter depends on hand-crafted configuration that -needs to be updated after every game patch. That gives Rainbow Filter a lot of power and -flexibility, but it also causes delays — it takes time to produce a new Rainbow Filter mod after -each patch. +gdse depends on zero manually configured colors. Instead, it infers correct coloring based on game database files and a few simple heuristics. Therefore, when a new patch lands, all you need to do to get updated, recolored text files is rerun gdse. No waiting for me to release a new version in the typical case (major changes to the game may require updates to handle new game features, but it should mostly just work). -gdse depends on zero manually configured colors. Instead, it infers correct coloring based on game -database files and a few simple heuristics. Therefore, when a new patch lands, all you need to do to -get updated, recolored text files is rerun gdse. No waiting for me to release a new version in the -typical case (major changes to the game may require updates to handle new game features, but it -should mostly just work). +Additionally, I find many of Rainbow Filter's choices to be a little too much. I don't need an (S) to get inserted on every set item, or MIs to have a special color, or "Empowered" and "Mythical" to be a different color than the base item name, or for "Physique" to be colored pink. Your preferences may not match mine, and that's OK! One of Rainbow Filter's strengths is that it's highly customizable (it just takes a lot of effort to customize). gdse is simpler and more opinionated. This can be a virtue if you don't strongly disagree with its opinions. -Additionally, I find many of Rainbow Filter's choices to be a little too much. I don't need an (S) -to get inserted on every set item, or MIs to have a special color, or "Empowered" and "Mythical" to -be a different color than the base item name, or for "Physique" to be colored pink. Your preferences -may not match mine, and that's OK! One of Rainbow Filter's strengths is that it's highly -customizable (it just takes a lot of effort to customize). gdse is simpler and more opinionated. -This can be a virtue if you don't strongly disagree with its opinions. +--- ## Installation & Usage -gdse has been tested with game version 1.3.0 on linux only, and only for -English text files, but in theory it should work on windows and with localizations. +gdse was originally developed and tested on Linux by the original author (game version 1.3.0, English text files). Additional features in this fork, including recent palette and workflow additions, were developed and tested on Windows. In theory it should also work with localizations. There are not currently any precompiled binaries available. You must build from source. -### Quick Start (Windows, PowerShell) +--- + +### Setup - Windows OS 1. Install Rust (choose one option): @@ -67,7 +66,7 @@ There are not currently any precompiled binaries available. You must build from - Download and run `rustup-init.exe` - Keep the default installation settings unless you have a specific reason to change them -2. Open a new terminal, then verify tools are available wtih these commands: +2. Open a new terminal, then verify tools are available with these commands: ```powershell rustc --version @@ -98,8 +97,7 @@ There are not currently any precompiled binaries available. You must build from cargo run --release ``` - gdse will first ask whether to use default colors or a custom palette file, - then show whether it detected changes since your last run, then prompt: + gdse will first ask whether to use default colors or a custom palette file, then show whether it detected changes since your last run, then prompt: `Proceed with recoloring run? [Y/N]`. If you cloned somewhere else, replace `C:\repos\gdse` with your own full path. @@ -107,6 +105,8 @@ There are not currently any precompiled binaries available. You must build from Important: the Grim Dawn install path above is only an example. Use the actual path from your own installation. +--- + ### Quick Start (Linux) ```bash @@ -120,8 +120,7 @@ cd /path/where/you/cloned/gdse cargo run --release ``` -gdse will first ask whether to use default colors or a custom palette file, -then show whether it detected changes since your last run, then prompt: +gdse will first ask whether to use default colors or a custom palette file, then show whether it detected changes since your last run, then prompt: `Proceed with recoloring run? [Y/N]`. ### Environment Variable Requirement @@ -132,110 +131,119 @@ then show whether it detected changes since your last run, then prompt: Re-run gdse after game patches if you see missing tags or incorrect colors. -To make this easier to track, gdse also writes `gdse-db-hash.txt` in the output folder. -Each run of `cargo run --release` appends one line in this format (local computer time, minute precision): +To make this easier to track, gdse also writes `gdse-db-hash.txt` in the output folder. Each run of `cargo run --release` appends one line in this format (local computer time, minute precision): ` hash= steam_build_id= patch_versions=` `patch_versions` is inferred from release-marker comments found in game text (for example, lines starting with `#Patch`, `#Hotfix`, or `#Update`). +--- + ### Current Color Palette -gdse uses Grim Dawn's built-in color-code letters from `gd-colorcodes.json`. -The tables below show the current mappings in plain English plus the game code -letter and hex value. +gdse uses Grim Dawn's built-in color-code letters from `gd-colorcodes.json`. The tables below show the current mappings in plain English plus the game code letter. -Note: item/tooltip text is rendered on dark/black backgrounds in Grim Dawn, so -high-contrast bright colors are strongly recommended for readability. +Note: item/tooltip text is rendered on dark/black backgrounds in Grim Dawn, so high-contrast bright colors are strongly recommended for readability. #### Rarity Colors -| Category | Color Name | Game Code | Hex | +| Category | Palette Key | Color Name | Game Code | |---|---|---|---| -| Common item/affix | White | `w` | `#FFFFFF` | -| Magical item/affix | Yellow | `y` | `#FFF62C` | -| Rare item/affix | Green | `g` | `#10EB5D` | +| Common item/affix | `rarity.common` | White | `w` | +| Magical item/affix | `rarity.magical` | Yellow | `y` | +| Rare item/affix | `rarity.rare` | Green | `g` | +| Epic item/affix (optional override) | `rarity.epic` | Uncolored by default | `b` (engine default blue) | +| Legendary item/affix (optional override) | `rarity.legendary` | Uncolored by default | `i` (engine default indigo) | #### Damage-Type Property Colors -| Category | Color Name | Game Code | Hex | +| Category | Palette Key | Color Name | Game Code | |---|---|---|---| -| Physical | Khaki | `k` | `#F1E78C` | -| Pierce (gdse default) | Fushia/Pink | `f` | `#FF69B5` | -| Bleeding | Red | `r` | `#FF4200` | -| Fire | Orange | `o` | `#F3A44D` | -| Cold | Cyan | `c` | `#00FFFF` | -| Lightning | Cobalt | `z` | `#6A91E0` | -| Poison/Acid | Olive | `l` | `#92CC00` | -| Vitality/Life | Maroon | `m` | `#800000` | -| Aether | Aqua | `a` | `#80FFD5` | -| Chaos | Purple | `p` | `#BD94C6` | -| Elemental | Yellow | `y` | `#FFF62C` | +| Physical | `damage.physical` | Khaki | `k` | +| Pierce (gdse default) | `damage.pierce` | Fuchsia/Pink | `f` | +| Bleeding | `damage.bleeding` | Red | `r` | +| Fire | `damage.fire` | Orange | `o` | +| Cold | `damage.cold` | Cyan | `c` | +| Lightning | `damage.lightning` | Cobalt | `z` | +| Poison/Acid | `damage.poison` | Olive | `l` | +| Vitality/Life | `damage.vitality`, `damage.life` | Maroon | `m` | +| Aether | `damage.aether` | Aqua | `a` | +| Chaos | `damage.chaos` | Purple | `p` | +| Elemental | `damage.elemental` | Yellow | `y` | #### Non-Damage Property Colors -| Category | Color Name | Game Code | Hex | +| Category | Palette Key | Color Name | Game Code | |---|---|---|---| -| Cunning/Spirit/Physique aggregate (`tagCharAttribute0`) | Grayish Orange (highlight) | `h` | (engine highlight color) | -| Mastery/All Skills increments | Teal | `t` | `#00FFD2` | -| OA/DA/Speed/Crit/Total damage modifier group | Uncolored by default | (none) | (inherits game default text color) | +| Cunning/Spirit/Physique aggregate (`tagCharAttribute0`) | `nondamage.attribute0` | Grayish Orange (highlight) | `h` | +| Mastery increment | `nondamage.mastery_increment` | Teal | `t` | +| All Skills increment | `nondamage.all_skill_increment` | Teal | `t` | +| Run speed | `nondamage.run_speed` | Uncolored by default | (none) | +| Cast speed | `nondamage.cast_speed` | Uncolored by default | (none) | +| Attack speed | `nondamage.attack_speed` | Uncolored by default | (none) | +| Total speed modifier | `nondamage.total_speed` | Uncolored by default | (none) | +| Run speed modifier | `nondamage.run_speed_modifier` | Uncolored by default | (none) | +| Offensive Ability | `nondamage.offensive_ability` | Uncolored by default | (none) | +| Defensive Ability | `nondamage.defensive_ability` | Uncolored by default | (none) | +| Crit damage modifier | `nondamage.crit_damage` | Uncolored by default | (none) | +| Damage multiplier | `nondamage.damage_mult` | Uncolored by default | (none) | +| Total damage modifier | `nondamage.total_damage` | Uncolored by default | (none) | + +#### Grim Dawn Color Code Reference + +The game uses single-letter color codes in tag text (for example `{^f}`). + +| Code | Name | Notes | +|---|---|---| +| `a` | Aqua | Bright, readable | +| `b` | Blue | Engine default Epic item color | +| `c` | Cyan | Bright, readable | +| `d` | Dark Gray | Usually too dark on tooltip background | +| `e` | Brown | Engine body text color | +| `f` | Fuchsia/Pink | Bright, readable | +| `g` | Green | Bright, readable | +| `h` | Grayish Orange | Engine highlight color | +| `i` | Indigo | Engine default Legendary item color | +| `j` | (disabled) | Disabled in game color table | +| `k` | Khaki | Bright, readable | +| `l` | Olive | Bright enough for many uses | +| `m` | Maroon | Darker than most bright colors | +| `n` | (disabled) | Disabled in game color table | +| `o` | Orange | Bright, readable | +| `p` | Purple | Bright, readable | +| `q` | Grayish Magenta | Engine lore-item color | +| `r` | Red | Bright, readable | +| `s` | Silver | Engine dynamic "inactive bonus" cue | +| `t` | Teal | Unused by engine defaults, good override candidate | +| `u` | (disabled) | Disabled in game color table | +| `v` | (disabled) | Disabled in game color table | +| `w` | White | Bright, readable | +| `x` | Dark Green | Usually too dark on tooltip background | +| `y` | Yellow | Bright, readable | +| `z` | Cobalt | Bright, readable | + +--- ### Custom Palette File -If you want your own colors, create `gdse-palette.txt` in the same folder where -you run `cargo run --release`, or pass a custom file path with `--palette-file`. +If you want your own colors, create `gdse-palette.txt` in the same folder where you run `cargo run --release`, or pass a custom file path with `--palette-file`. -In the interactive run flow, if you answer `N` to `Use default gdse palette? [Y/N]`, -gdse exits immediately and asks you to create `gdse-palette.txt`, then run again. +In the interactive run flow, if you answer `N` to `Use default gdse palette? [Y/N]`, gdse exits immediately and asks you to create `gdse-palette.txt`, then run again. +If you prefer another location or filename, use `--palette-file `. Format is one `key=letter` per line. Blank lines and `# comments` are allowed. Example (single override): ```text -damage.chaos=p +damage.chaos=f ``` -This is totally valid: one line is enough. Any key you do not include keeps the -built-in gdse default for that category. - -You can override just one category if you want. Example: a file containing only -`damage.chaos=f` changes Chaos to hot pink and leaves every other color on gdse defaults. - -#### Palette Keys Reference - -Use these keys in `gdse-palette.txt`: - -| Key | Controls | -|---|---| -| `rarity.common` | Common item/affix rarity color | -| `rarity.magical` | Magical item/affix rarity color | -| `rarity.rare` | Rare item/affix rarity color | -| `damage.physical` | Physical damage property color | -| `damage.pierce` | Pierce damage property color | -| `damage.bleeding` | Bleeding damage property color | -| `damage.fire` | Fire damage property color | -| `damage.cold` | Cold damage property color | -| `damage.lightning` | Lightning damage property color | -| `damage.poison` | Poison/Acid damage property color | -| `damage.vitality` | Vitality damage property color | -| `damage.life` | Life-based property color | -| `damage.aether` | Aether damage property color | -| `damage.chaos` | Chaos damage property color | -| `damage.elemental` | Elemental damage property color | -| `nondamage.attribute0` | Cunning/Spirit/Physique aggregate color | -| `nondamage.mastery_increment` | +Mastery level line color | -| `nondamage.all_skill_increment` | +All Skills line color | -| `nondamage.run_speed` | Run speed line color | -| `nondamage.cast_speed` | Cast speed line color | -| `nondamage.attack_speed` | Attack speed line color | -| `nondamage.total_speed` | Total speed modifier line color | -| `nondamage.run_speed_modifier` | Run speed modifier line color | -| `nondamage.offensive_ability` | Offensive Ability line color | -| `nondamage.defensive_ability` | Defensive Ability line color | -| `nondamage.crit_damage` | Crit damage modifier line color | -| `nondamage.damage_mult` | Damage multiplier line color | -| `nondamage.total_damage` | Total damage modifier line color | +This is totally valid: one line is enough. Any key you do not include keeps the built-in gdse default for that category. + +You can override just one category if you want. Example: a file containing only `damage.chaos=f` changes Chaos to hot pink and leaves every other color on gdse defaults. + +--- ### Common Windows Issues @@ -248,6 +256,8 @@ Use these keys in `gdse-palette.txt`: - Print it with `echo $env:GRIM_DAWN_INSTALL_PATH` in PowerShell. - Update it with `setx GRIM_DAWN_INSTALL_PATH ""`. +--- + ### Notes for AI Coding Agents If you are using Copilot, Claude Code, Codex, or another coding agent: diff --git a/src/color.rs b/src/color.rs index 7594d4d..4a2ce13 100644 --- a/src/color.rs +++ b/src/color.rs @@ -3,8 +3,8 @@ //! scope-decision memory). //! //! Only item & affix rarity is colored, and only the three rarities that can -//! carry a name-altering affix: Common=White, Magical=Yellow, Rare=Green. Epic/Legendary -//! names never take a prefix/suffix, so they're left to the engine's native +//! carry a name-altering affix: Common=White, Magical=Yellow, Rare=Green. +//! Higher rarities keep native game colors by default, but can be overridden. //! rarity color. Faction / Set / Skill / Quality / Style get no special cue //! either, matching Full Rainbow. Damage-type Property colors live separately in //! `property.rs`. Colors are the single-letter codes from gd-colorcodes.json. @@ -26,6 +26,9 @@ pub fn color_for(info: &TagInfo, user_palette: &UserPalette) -> Option { Rarity::Common => Some(user_palette.rarity(Rarity::Common).unwrap_or(WHITE)), Rarity::Magical => Some(user_palette.rarity(Rarity::Magical).unwrap_or(YELLOW)), Rarity::Rare => Some(user_palette.rarity(Rarity::Rare).unwrap_or(GREEN)), - Rarity::Epic | Rarity::Legendary => None, + // Optional override key: rarity.epic + Rarity::Epic => user_palette.rarity(Rarity::Epic), + // Optional override key: rarity.legendary + Rarity::Legendary => user_palette.rarity(Rarity::Legendary), } } diff --git a/src/user_palette.rs b/src/user_palette.rs index d31ab2e..87f974e 100644 --- a/src/user_palette.rs +++ b/src/user_palette.rs @@ -24,7 +24,8 @@ impl UserPalette { Rarity::Common => self.get("rarity.common"), Rarity::Magical => self.get("rarity.magical"), Rarity::Rare => self.get("rarity.rare"), - Rarity::Epic | Rarity::Legendary => None, + Rarity::Epic => self.get("rarity.epic"), + Rarity::Legendary => self.get("rarity.legendary"), } } @@ -148,6 +149,8 @@ fn is_known_key(key: &str) -> bool { "rarity.common" | "rarity.magical" | "rarity.rare" + | "rarity.epic" + | "rarity.legendary" | "damage.physical" | "damage.pierce" | "damage.bleeding" From 2fb04e9013afbf0582044603de1804d86c0b5f7d Mon Sep 17 00:00:00 2001 From: Chris Maclean Date: Thu, 13 Aug 2026 12:01:48 -0500 Subject: [PATCH 7/7] last tweaks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a7bf95..3b71d5c 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Additionally, I find many of Rainbow Filter's choices to be a little too much. I ## Installation & Usage -gdse was originally developed and tested on Linux by the original author (game version 1.3.0, English text files). Additional features in this fork, including recent palette and workflow additions, were developed and tested on Windows. In theory it should also work with localizations. +gdse was originally developed and tested on Linux by the original author (game version 1.3.0, English text files). Additional features , including recent palette and workflow additions, were developed by a contributor and tested on Windows only. In theory it should also work with localizations. There are not currently any precompiled binaries available. You must build from source.