diff --git a/CHANGELOG.md b/CHANGELOG.md index 223bc692..69ad6117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ User-facing changes only. The release workflow publishes the matching ### Fixed +- Setups page: Crosshair now names the crosshair the setup uses, with your own designs under + the names you saved them with, instead of reading "Execs crosshairs" for every setup. - Viewmodels: hiding a weapon no longer hides another weapon you left shown when the two share animations. Keep melee visible no longer hid Soldier's Shovel (through the Concheror) or melee swings for Engineer, Demoman and Medic. A weapon that stays partly visible because of this is diff --git a/apps/desktop/src-tauri/core/src/setup_overview.rs b/apps/desktop/src-tauri/core/src/setup_overview.rs index e33f6534..b68d16bf 100644 --- a/apps/desktop/src-tauri/core/src/setup_overview.rs +++ b/apps/desktop/src-tauri/core/src/setup_overview.rs @@ -7,7 +7,9 @@ use std::path::Path; use serde::Serialize; use crate::apply::profile_file_bytes_from; -use crate::profile::{load_library_from, load_manifest, ProfileError, ProfileManifest}; +use crate::profile::{ + load_library_from, load_manifest, CrosshairRecord, ProfileError, ProfileManifest, +}; use crate::profile_compare::{casual_names, managed_text, managed_values, sound_names}; const CONFIG_CFG: &str = "tf/cfg/config.cfg"; @@ -17,8 +19,12 @@ const CONFIG_CFG: &str = "tf/cfg/config.cfg"; pub struct SetupOverview { pub id: String, pub hud: Option, - /// The custom crosshair pack; `None` when it is off. + /// The crosshair every weapon gets from the custom pack (its `shape`; + /// per-weapon assignments aside); `None` when the pack is off. pub crosshair: Option, + /// Saved designer parameters when `crosshair` is one of the player's + /// designs, so it can be shown under its saved name. + pub crosshair_design: Option, pub fov: Option, pub sensitivity: Option, pub hit_sound: Option, @@ -51,6 +57,18 @@ pub fn setup_overviews( .collect()) } +/// The stored design behind `record.shape`. `design` holds either a single +/// design (for the `designed` entry) or a map of library name to design. +fn shape_design(record: &CrosshairRecord) -> Option { + let raw = record.design.as_deref()?; + let value: serde_json::Value = serde_json::from_str(raw).ok()?; + let entries = value.as_object()?; + if entries.contains_key("style") { + return (record.shape == "designed").then(|| raw.to_string()); + } + entries.get(&record.shape)?.as_str().map(str::to_string) +} + /// Values in the profile's saved `config.cfg`; empty when it has none. fn config_values(profiles_dir: &Path, manifest: &ProfileManifest) -> BTreeMap { if !manifest.files.iter().any(|file| file.path == CONFIG_CFG) { @@ -80,14 +98,17 @@ fn overview(profiles_dir: &Path, manifest: &ProfileManifest) -> SetupOverview { let fov = value("fov_desired"); let sensitivity = value("sensitivity"); let (hit_sound, kill_sound) = sound_names(manifest.hitsound.as_ref()); + // `id` names the pack itself; `shape` is the crosshair it gives every + // weapon. Very old records may have no shape to name. + let pack = manifest + .crosshair + .as_ref() + .filter(|record| !record.inactive && !record.shape.is_empty()); SetupOverview { id: manifest.id.clone(), hud: manifest.hud.as_ref().map(|hud| hud.id.clone()), - crosshair: manifest - .crosshair - .as_ref() - .filter(|record| !record.inactive) - .map(|record| record.id.clone()), + crosshair: pack.map(|record| record.shape.clone()), + crosshair_design: pack.and_then(shape_design), fov, sensitivity, hit_sound, @@ -209,8 +230,7 @@ mod tests { fn crosshair_hud_sounds_and_mods_come_from_the_manifest() { let dir = crate::test_temp_dir(); let (profiles, root, id) = saved(&dir, &[], false); - let edit = |active: bool| { - let crosshair = serde_json::json!({ "id": "dot", "inactive": !active }); + let edit = |crosshair: serde_json::Value| { let hitsound = serde_json::json!({ "hit": { "name": "ding", "source": "file" }, "kill": { "name": "bell", "source": "community" }, @@ -253,10 +273,22 @@ mod tests { .unwrap(); }; - edit(true); + let pack = |shape: &str, design: Option<&str>, active: bool| { + serde_json::json!({ + "id": "execs-crosshairs", + "inactive": !active, + "shape": shape, + "assignments": { "tf_weapon_sniperrifle": "circle" }, + "design": design, + }) + }; + + edit(pack("dot", None, true)); let on = only(&profiles, &root); assert_eq!(on.hud.as_deref(), Some("flawhud")); + // The shape every weapon gets, not the pack's id or an assignment. assert_eq!(on.crosshair.as_deref(), Some("dot")); + assert_eq!(on.crosshair_design, None); assert_eq!(on.hit_sound.as_deref(), Some("ding")); assert_eq!(on.kill_sound.as_deref(), Some("bell")); // Two mods, plus two Casual addons and one particle mod (named "skin" @@ -264,8 +296,27 @@ mod tests { assert_eq!(on.mods, 2 + 3); assert_eq!(on.performance, None); - edit(false); - assert_eq!(only(&profiles, &root).crosshair, None); + // A design is passed along with its saved parameters, whether the + // record holds one design or a library of them. + let single = r#"{"style":"dot","label":"Mine"}"#; + edit(pack("designed", Some(single), true)); + let designed = only(&profiles, &root); + assert_eq!(designed.crosshair.as_deref(), Some("designed")); + assert_eq!(designed.crosshair_design.as_deref(), Some(single)); + let ring = r#"{"style":"ring","label":"Ring"}"#; + let library = serde_json::json!({ "design-ring": ring, "design-x": "{}" }).to_string(); + edit(pack("design-ring", Some(&library), true)); + assert_eq!( + only(&profiles, &root).crosshair_design.as_deref(), + Some(ring) + ); + edit(pack("dot", Some(single), true)); + assert_eq!(only(&profiles, &root).crosshair_design, None); + + edit(pack("dot", None, false)); + let off = only(&profiles, &root); + assert_eq!(off.crosshair, None); + assert_eq!(off.crosshair_design, None); std::fs::remove_dir_all(dir).unwrap(); } diff --git a/apps/desktop/src/SetupsHome.test.tsx b/apps/desktop/src/SetupsHome.test.tsx index b324468e..5c83d894 100644 --- a/apps/desktop/src/SetupsHome.test.tsx +++ b/apps/desktop/src/SetupsHome.test.tsx @@ -42,6 +42,7 @@ const OVERVIEWS: SetupOverview[] = [ id: "main", hud: "rayshud", crosshair: "dot", + crosshairDesign: null, fov: "90", sensitivity: "2.5", hitSound: "Quake hit", @@ -53,6 +54,7 @@ const OVERVIEWS: SetupOverview[] = [ id: "casual", hud: "flawhud", crosshair: "dot", + crosshairDesign: null, fov: "90", sensitivity: "1.8", hitSound: "Quake hit", @@ -64,6 +66,7 @@ const OVERVIEWS: SetupOverview[] = [ id: "plain", hud: null, crosshair: null, + crosshairDesign: null, fov: "110", sensitivity: "2.5", hitSound: null, diff --git a/apps/desktop/src/lib/bridge.ts b/apps/desktop/src/lib/bridge.ts index 385e4db7..7d27b9ea 100644 --- a/apps/desktop/src/lib/bridge.ts +++ b/apps/desktop/src/lib/bridge.ts @@ -422,8 +422,13 @@ export type SetupOverview = { id: string; /** HUD catalog id or folder name. */ hud: string | null; - /** Custom crosshair id; `null` when the crosshair pack is off. */ + /** + * The crosshair the pack gives every weapon (per-weapon picks aside); + * `null` when the crosshair pack is off. + */ crosshair: string | null; + /** Saved designer parameters when `crosshair` is one of the player's designs. */ + crosshairDesign: string | null; /** `fov_desired`, from the Gameplay cfg or else the profile's config.cfg. */ fov: string | null; sensitivity: string | null; diff --git a/apps/desktop/src/lib/preview-bridge.ts b/apps/desktop/src/lib/preview-bridge.ts index 79b12203..d22fee05 100644 --- a/apps/desktop/src/lib/preview-bridge.ts +++ b/apps/desktop/src/lib/preview-bridge.ts @@ -167,6 +167,7 @@ const PREVIEW_SETUP_FACTS: Omit[] = [ { hud: "rayshud", crosshair: "dot", + crosshairDesign: null, fov: "90", sensitivity: "2.5", hitSound: "Quake hit", @@ -177,6 +178,7 @@ const PREVIEW_SETUP_FACTS: Omit[] = [ { hud: "budhud", crosshair: "dot", + crosshairDesign: null, fov: "90", sensitivity: "1.8", hitSound: "Quake hit", @@ -187,6 +189,7 @@ const PREVIEW_SETUP_FACTS: Omit[] = [ { hud: null, crosshair: null, + crosshairDesign: null, fov: "110", sensitivity: "2.5", hitSound: null, @@ -197,6 +200,7 @@ const PREVIEW_SETUP_FACTS: Omit[] = [ { hud: "m0rehud", crosshair: "ring", + crosshairDesign: null, fov: "100", sensitivity: "2.5", hitSound: "Bonk", diff --git a/apps/desktop/src/lib/setups-ui.test.ts b/apps/desktop/src/lib/setups-ui.test.ts index 8d307b5e..11e2000c 100644 --- a/apps/desktop/src/lib/setups-ui.test.ts +++ b/apps/desktop/src/lib/setups-ui.test.ts @@ -21,6 +21,7 @@ function overview(patch: Partial = {}): SetupOverview { id: "main", hud: "rayshud", crosshair: "dot", + crosshairDesign: null, fov: "90", sensitivity: "2.5", hitSound: "Quake hit", @@ -126,6 +127,14 @@ describe("fact values", () => { expect(value("hud")).toBe("Ray's HUD"); expect(value("crosshair")).toBe("Small dot"); expect(value("crosshair", { crosshair: "design-my-ring" })).toBe("My ring"); + const named = JSON.stringify({ style: "ring", label: "Tournament ring" }); + expect(value("crosshair", { crosshair: "design-my-ring", crosshairDesign: named })).toBe( + "Tournament ring", + ); + expect(value("crosshair", { crosshair: "designed", crosshairDesign: named })).toBe( + "Tournament ring", + ); + expect(value("crosshair", { crosshair: "designed" })).toBe("My crosshair"); expect(value("fov")).toBe("90°"); expect(value("fov", { fov: "90.0" })).toBe("90°"); expect(value("sensitivity", { sensitivity: "1.800000" })).toBe("1.8"); @@ -138,6 +147,7 @@ describe("fact values", () => { const bare = { hud: null, crosshair: null, + crosshairDesign: null, fov: null, sensitivity: null, hitSound: null, @@ -189,6 +199,13 @@ describe("differences from the setup in the game", () => { expect(differingFacts(overview({ fov: "wide" }), overview({ fov: "90" }))).toEqual(["fov"]); }); + it("tells two different designs saved under the same name apart", () => { + const design = (label: string) => + overview({ crosshair: "designed", crosshairDesign: JSON.stringify({ style: "dot", label }) }); + expect(differingFacts(design("A"), design("B"))).toEqual(["crosshair"]); + expect(differingFacts(design("A"), design("A"))).toEqual([]); + }); + it("summarises the count against the active setup's name", () => { expect(changeSummary(3, "Main")).toBe("3 changes from Main"); expect(changeSummary(1, "Main")).toBe("1 change from Main"); diff --git a/apps/desktop/src/lib/setups-ui.ts b/apps/desktop/src/lib/setups-ui.ts index 6ecf18ca..547b8ba3 100644 --- a/apps/desktop/src/lib/setups-ui.ts +++ b/apps/desktop/src/lib/setups-ui.ts @@ -92,6 +92,15 @@ function numberLabel(value: string | null, unit = ""): string { return parsed === null ? value : `${parsed}${unit}`; } +/** The pack's crosshair, under its saved name when it is one of the player's designs. */ +function crosshairFact({ crosshair, crosshairDesign }: SetupOverview): string { + if (crosshair === null) return TF2_OWN; + return crosshairLabel( + crosshair, + crosshairDesign === null ? {} : { [crosshair]: crosshairDesign }, + ); +} + export function factValue( key: SetupFactKey, overview: SetupOverview, @@ -101,7 +110,7 @@ export function factValue( case "hud": return hudLabel(overview.hud, catalog); case "crosshair": - return overview.crosshair === null ? TF2_OWN : crosshairLabel(overview.crosshair); + return crosshairFact(overview); case "fov": return numberLabel(overview.fov, "°"); case "sensitivity": @@ -135,7 +144,7 @@ export function sameFact(key: SetupFactKey, a: SetupOverview, b: SetupOverview): case "hud": return sameText(a.hud, b.hud, true); case "crosshair": - return sameText(a.crosshair, b.crosshair); + return sameText(a.crosshair, b.crosshair) && a.crosshairDesign === b.crosshairDesign; case "fov": return sameNumber(a.fov, b.fov); case "sensitivity":