Skip to content

Commit 1f082a4

Browse files
committed
refactor: split the GUI, CLI and tasks into smaller modules
1 parent dcc3940 commit 1f082a4

67 files changed

Lines changed: 20863 additions & 21073 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/core/src/addons/browse.rs‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -186,22 +186,12 @@ pub async fn search(
186186
}
187187

188188
/// The most-downloaded addons for a flavor, for a search box nobody has typed
189-
/// in yet.
189+
/// in yet: ranked by popularity alone, from WoWInterface's catalogue already
190+
/// on disk, so Browse has rows before the first keystroke.
190191
///
191-
/// A different question from [`search`], not a search with no words: there is
192-
/// nothing to match, so nothing to rank by match, and the whole order is
193-
/// popularity. WoWInterface answers it out of the catalogue already on disk,
194-
/// so the Browse tab has rows before the first keystroke and at no cost.
195-
///
196-
/// Tukui is deliberately absent. It publishes no download counts at all, so
197-
/// its two addons would either sit at the bottom of a popularity list as
198-
/// zeroes or have to be given a rank they did not earn, and ElvUI belongs
199-
/// nowhere near the bottom. It is one search away.
200-
///
201-
/// A provider that cannot answer is dropped rather than reported, unlike in
202-
/// [`search`]: this list is the default view of a tab, not the answer to a
203-
/// question somebody asked, and a standing warning over a page nobody asked
204-
/// to load is noise. A search says so properly.
192+
/// Tukui is left out: it publishes no download counts, and ElvUI does not
193+
/// belong at the bottom. A provider that cannot answer is dropped silently,
194+
/// unlike in [`search`]: this is a tab's default view, not a question asked.
205195
pub async fn popular(
206196
http: &Http,
207197
catalog: Option<&Catalog>,

‎crates/core/src/addons/fingerprint.rs‎

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,18 @@ pub fn of_file(path: &Path) -> Option<u32> {
9595

9696
/// The fingerprint of an addon folder, as CurseForge computes it.
9797
///
98-
/// Reverse-engineered against 34 folders from two unrelated addons whose
99-
/// published fingerprints the catalog states, and it reproduces all 34. The
100-
/// parts that are not guessable from the outside, in the order they bite:
98+
/// Reverse-engineered against 34 folders from two addons; reproduces all 34.
99+
/// The parts that cannot be guessed:
101100
///
102-
/// 1. **Not every file counts.** The set is the folder's `.toc` files, the
103-
/// paths those tocs *load*, and the files the client loads implicitly.
104-
/// Only unconditionally, though: a line carrying a `[...]` clause,
105-
/// `BossPrototype.lua [ExcludeLoadGameType vanilla]`: is out, and so is
106-
/// a file nothing references. BigWigs has 338 files on disk and a
107-
/// fingerprint over 19.
108-
/// 2. **Each file is hashed, then the hashes are hashed.** Per file it is
109-
/// [`of_bytes`]; the folder is those numbers written out as decimal,
110-
/// concatenated, and hashed again.
111-
/// 3. **Sorted by value, not by name.** Ascending numeric order of the file
112-
/// hashes themselves, which is why no filename ordering ever matched.
101+
/// 1. **Not every file counts.** Only the `.toc` files, the paths they load
102+
/// unconditionally (a line with a `[...]` clause is out) and the files the
103+
/// client loads implicitly. BigWigs: 338 files on disk, 19 hashed.
104+
/// 2. **Hash of hashes.** Each file is [`of_bytes`]; the folder hashes those
105+
/// numbers written as decimal and concatenated.
106+
/// 3. **Sorted by value**, not by name.
113107
///
114-
/// `None` when the folder declares nothing, no toc, or a toc that loads
115-
/// nothing that exists. An addon with no fingerprint cannot be adopted,
116-
/// which is the honest outcome rather than hashing an arbitrary subset.
108+
/// `None` when the folder loads nothing that exists; such an addon cannot be
109+
/// adopted, rather than hashing an arbitrary subset.
117110
pub fn of_folder(dir: &Path) -> Option<u32> {
118111
let mut hashes: Vec<u32> = declared(dir)
119112
.iter()

‎crates/core/src/addons/install.rs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -320,17 +320,11 @@ pub struct Extracted {
320320

321321
/// Extract an addon package into `addons_dir`.
322322
///
323-
/// Replaces the folders it contains: an update must not leave a removed file
324-
/// from the previous version behind, since a stale `.lua` can break an addon
325-
/// far more confusingly than a missing one. `retire` names more folders to
326-
/// take away in the same step, the ones a repair replaces that this build no
327-
/// longer ships.
328-
///
329-
/// Nothing installed is touched until the whole package is on disk. It is
330-
/// written to a staging folder beside `AddOns`, on the same filesystem, and
331-
/// only then swapped in folder by folder with renames. A full disk or an I/O
332-
/// error while writing leaves the old addon exactly as it was, and a failed
333-
/// rename puts back every folder already swapped.
323+
/// Replaces whole folders, since a stale `.lua` left from the last version
324+
/// breaks an addon more confusingly than a missing one; `retire` names extra
325+
/// folders to remove in the same step. The package is written to a staging
326+
/// folder beside `AddOns` first, then swapped in by rename, so a full disk
327+
/// leaves the old addon intact and a failed rename puts back what it swapped.
334328
pub fn extract(package: &[u8], addons_dir: &Path, retire: &[String]) -> Result<Extracted, InstallError> {
335329
extract_within(package, addons_dir, retire, MAX_UNCOMPRESSED)
336330
}

‎crates/core/src/addons/manage.rs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -610,17 +610,11 @@ pub struct Adopted {
610610

611611
/// Recognise hand-installed addons and take them into the ledger.
612612
///
613-
/// An addon Lightforge did not install has no provider and no id, so it can
614-
/// be listed and nothing else, no update, no channel, no removal. Hashing
615-
/// each unmanaged folder the way CurseForge does (see
616-
/// [`crate::addons::fingerprint`]) and asking the catalog whose it is turns
617-
/// it into an ordinary managed addon.
618-
///
619-
/// Conservative in both directions. A folder already owned by a record is
620-
/// left alone, so this cannot take an addon away from the entry that
621-
/// installed it. And a match is only written for folders that are *actually
622-
/// present*: the catalog answers with the whole package, and recording a
623-
/// folder that is not there would have removal promise to delete it.
613+
/// An unmanaged folder can only be listed; fingerprinting it the way
614+
/// CurseForge does (see [`crate::addons::fingerprint`]) and asking the catalog
615+
/// makes it an ordinary managed addon. A folder another record owns is left
616+
/// alone, and only folders actually present are recorded, so a later removal
617+
/// never promises to delete one that is not there.
624618
pub async fn adopt(
625619
http: &Http,
626620
catalog: Option<&Catalog>,

‎crates/core/src/assets.rs‎

Lines changed: 21 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,13 @@ use sha2::{Digest, Sha256};
2828
use crate::net::{ETagCache, Http, NetError};
2929
use crate::product::{Branch, Catalog, CatalogEntry, ProductCode};
3030

31-
/// Manifest schema this build understands.
31+
/// Manifest schema this build understands. A newer one is refused: the last
32+
/// good cached art beats half-understood theming.
3233
///
33-
/// A manifest declaring a newer schema is refused rather than parsed
34-
/// optimistically: the cached art from the last good fetch is a better answer
35-
/// than half-understood theming.
36-
///
37-
/// **2** made `logo` and `background` optional, so a product can be themed
38-
/// with nothing but its square mark. Battle.net hand-offs, Diablo,
39-
/// Hearthstone, StarCraft, are never drawn with a logo or a background by
40-
/// this launcher, but they do want a mark in the product bar and the browse
41-
/// list, and under schema 1 that was unreachable without inventing a wordmark
42-
/// and a background for each of them.
43-
///
44-
/// The bump is deliberate and not merely additive: a schema-1 client reading
45-
/// a manifest whose entries omit `logo` fails to parse the *whole* document,
46-
/// so it must be told to keep its cached copy instead.
34+
/// **2** made `logo` and `background` optional, so a Battle.net hand-off can
35+
/// be themed with just its square mark. Not merely additive: a schema-1 client
36+
/// fails to parse the whole document when an entry omits `logo`, so it must
37+
/// be told to keep its cache.
4738
pub const SCHEMA_VERSION: u32 = 2;
4839

4940
/// File name of the manifest within its base URL or fixture directory.
@@ -176,18 +167,11 @@ impl Manifest {
176167

177168
/// The theme of this product's *line*: Classic's for Classic PTR.
178169
///
179-
/// The step between a product's own entry and `default`. A PTR is the
180-
/// same game as the branch it tracks, so when the manifest does not name
181-
/// it, the game it belongs to is a far better answer than whatever
182-
/// `default` happens to be: without this, Era PTR and Classic PTR both
183-
/// wore retail's logo and backdrop, because retail is what `default`
184-
/// points at.
185-
///
186-
/// For the head itself, or a head the manifest does not name, the first
187-
/// other branch that has a theme. Forever's head is `wowf`, and a
188-
/// manifest naming `wowf` would make 1.0.0-beta.3 and older install it
189-
/// into a literal `wowf/` (they do not know the code), so its art stays
190-
/// on `wow_classic_beta` and the live product borrows it from there.
170+
/// Between a product's own entry and `default`; without it both PTRs wore
171+
/// retail's art. For a head the manifest does not name, the first other
172+
/// branch with a theme: Forever's head is `wowf`, and naming `wowf` in the
173+
/// manifest would make 1.0.0-beta.3 and older install into a literal
174+
/// `wowf/`, so its art stays on `wow_classic_beta`.
191175
fn head_theme(&self, code: &ProductCode) -> Option<&Theme> {
192176
let line = code.line();
193177
let head = line.head();
@@ -355,22 +339,12 @@ impl Store {
355339

356340
/// Delete cached art that `keep` does not name.
357341
///
358-
/// Published art is content-addressed upstream, the filename carries the
359-
/// first bytes of its sha256, so every image that changes arrives under
360-
/// a *new* URL, and the cache file for the old one is never written to
361-
/// again. Without this, a few megabytes are stranded on disk per art
362-
/// revision, forever.
363-
///
364-
/// Deliberately narrow about what it will remove: only direct children,
365-
/// only names that look like something [`Store::cache_name`] produced,
366-
/// and never the manifest. A cache directory is still someone's disk, and
367-
/// a prune that mis-fires there is far worse than the leak it fixes.
368-
///
369-
/// **Each kept file keeps its `.etag` sidecar too.** Missing that was a
370-
/// real bug: every pass deleted every validator, so the next revalidation
371-
/// had nothing to send, re-downloaded art it already had, and the UI
372-
/// swapped pictures that had not changed, which looks like the page
373-
/// flickering a second after each product switch.
342+
/// Art is content-addressed upstream, so every revision arrives under a new
343+
/// URL and the old cache file would be stranded forever. Narrow on
344+
/// purpose: direct children only, names [`Store::cache_name`] could have
345+
/// produced, never the manifest. **Each kept file keeps its `.etag`
346+
/// sidecar**: deleting those once made every revalidation re-download and
347+
/// swap unchanged pictures, a flicker after each product switch.
374348
pub fn prune(&self, keep: &[String]) -> usize {
375349
let mut wanted: std::collections::HashSet<String> =
376350
std::collections::HashSet::with_capacity(keep.len() * 2);
@@ -544,18 +518,10 @@ impl Pipeline {
544518

545519
/// Resolve branding from what is already on disk, touching no network.
546520
///
547-
/// A warm cache holds the exact bytes the UI is about to draw, so making
548-
/// it wait on a revalidation round trip before painting buys nothing but
549-
/// a few hundred milliseconds of unthemed dashboard. This hands back the
550-
/// last good art at once; [`Pipeline::resolve_with`] follows behind and
551-
/// swaps in whatever actually changed.
552-
///
553-
/// `None` whenever the cache cannot fully answer, a missing manifest, an
554-
/// unthemed product, an image not fetched yet, or one that no longer
555-
/// matches its checksum. Partial branding is not a thing the UI can draw,
556-
/// so a half-warm cache simply waits for the network like a cold one.
557-
///
558-
/// Blocking file IO: call it off the thread that is drawing.
521+
/// The warm cache paints at once; [`Pipeline::resolve_with`] follows and
522+
/// swaps in whatever changed. `None` unless the cache can answer fully
523+
/// (manifest, theme, every image present and matching its checksum):
524+
/// partial branding cannot be drawn. Blocking IO: call it off the UI thread.
559525
pub fn cached(&self, code: &ProductCode) -> Option<Branding> {
560526
let manifest = self.cached_manifest()?;
561527
let theme = manifest.theme(code)?;

‎crates/core/src/battlenet.rs‎

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -538,24 +538,17 @@ pub const AUTOSTART_MINIMIZED_KEY: &str = "User.Client.AutoStartMinimized";
538538

539539
/// Start the client minimized, or normally.
540540
///
541-
/// Two keys, because the client decides this for itself and neither one is
542-
/// enough alone. It logs the whole decision on every start:
541+
/// The client logs its decision on every start:
543542
///
544543
/// ```text
545544
/// [Frontend] {Main} Attempting to show main window. isMinimizedOnStartup=false
546545
/// shouldMainWindowShowMinimizedAtAutoStartup=false varAutoStarted=false …
547-
/// needToMinimize=false …
548546
/// ```
549547
///
550-
/// `isMinimizedOnStartup` is the Qt window state below, what the client wrote
551-
/// out of its own window when it last exited. `shouldMainWindowShow…` is the
552-
/// preference above, and it is only consulted when `varAutoStarted` is set,
553-
/// which is what [`Launch::Autostart`] is for. Setting the preference without
554-
/// that flag changes nothing, which is why the window kept appearing.
555-
///
556-
/// Both have to be re-applied before each start, because the client writes the
557-
/// window state back from its live window every time it exits, the same reason
558-
/// the game's CVars are reconciled per launch rather than set once.
548+
/// `isMinimizedOnStartup` is the Qt window state below; the preference above
549+
/// is only read when `varAutoStarted` is set, which is what
550+
/// [`Launch::Autostart`] is for. Both are re-applied before each start because
551+
/// the client writes its window state back on every exit.
559552
pub fn window_state_tweaks(minimized: bool) -> Vec<Tweak> {
560553
vec![
561554
Tweak::new(
@@ -637,20 +630,15 @@ const READY_POLL: Duration = Duration::from_millis(250);
637630

638631
/// Wait until the client says it is signed in.
639632
///
640-
/// Watches the newest log in [`Layout::battle_net_logs`] that the client
641-
/// wrote *after* `since`, so a previous session's log cannot answer for this
642-
/// one. Reading a log file is the whole mechanism. Nothing is injected,
643-
/// nothing is scraped out of the client's memory, and the marker is a line
644-
/// the client prints for its own benefit:
633+
/// Reads the newest log in [`Layout::battle_net_logs`] written after `since`
634+
/// (so an old session cannot answer) for the client's own marker. Nothing is
635+
/// injected or read from its memory.
645636
///
646637
/// ```text
647-
/// I [GSStateTracker] State changed to: Ready
648-
/// I [AccountManager] Account manager state has changed to: Ready
649638
/// I [BNLogin] Logged into Battle.net successfully. |bnet=…
650639
/// ```
651640
///
652-
/// Measured on a cold start under Proton: about five and a half seconds from
653-
/// the process appearing to the sign-in line.
641+
/// About 5.5 s from process start on a cold Proton start.
654642
pub fn wait_until_ready(
655643
layout: &Layout,
656644
user: &str,
@@ -1016,19 +1004,10 @@ pub enum Launch {
10161004
/// `repair` and `modify` in its binary against the descriptions "Start a
10171005
/// package installation" and "Game registration/installation uid".
10181006
InstallFlow { uid: String },
1019-
/// A `battlenet://` link, handed to the client the way the system would.
1020-
///
1021-
/// The form Wine registered for the protocol at install time:
1022-
///
1023-
/// ```text
1024-
/// [Software\Classes\battlenet\shell\open\command]
1025-
/// @="\"C:\Program Files (x86)\Battle.net\Battle.net.exe\" --uri=\"%1\""
1026-
/// ```
1027-
///
1028-
/// This is how Blizzard's own web buttons drive the client, which makes
1029-
/// it the one documented way to say "show me this game", and unlike a
1030-
/// tile selection it is a command the client acts on rather than view
1031-
/// state its own startup can overwrite.
1007+
/// A `battlenet://` link, handed over the way Wine registered the protocol
1008+
/// (`Battle.net.exe --uri="%1"`). It is how Blizzard's own web buttons
1009+
/// drive the client, and a command the client acts on rather than view
1010+
/// state its startup can overwrite.
10321011
Uri(String),
10331012
}
10341013

@@ -1094,13 +1073,10 @@ pub fn game_uri(family: &str) -> String {
10941073
format!("battlenet://{family}")
10951074
}
10961075

1097-
/// Battle.net's own identifier for a game, as used by `--exec="launch <uid>"`.
1076+
/// Battle.net's id for a game's tile, as `--exec="launch <uid>"` wants it.
10981077
///
1099-
/// **This is not the TACT product code.** The client works in *game family*
1100-
/// ids, one per tile in its library, and the flavor within a tile comes from
1101-
/// its version dropdown. Read out of the client's own log, where each selection
1102-
/// is recorded (`Selecting game family by id: …` followed by
1103-
/// `Active product set to: …`):
1078+
/// **Not the TACT product code.** The client works in game families, one per
1079+
/// tile, read out of its log (`Selecting game family by id: …`):
11041080
///
11051081
/// ```text
11061082
/// WoW -> WoWX12_retail
@@ -1109,18 +1085,10 @@ pub fn game_uri(family: &str) -> String {
11091085
/// W3 -> W3_retail
11101086
/// ```
11111087
///
1112-
/// Passing a TACT code here is not a no-op. It is worse. The client fails to
1113-
/// resolve it and **falls back to the first family it knows, which is
1114-
/// Warcraft III**, so the user lands on a game they may not even own. That was
1115-
/// a real bug: `launch wow_classic_era` put Gabriel on the W3 page every time.
1116-
/// The family for a product, preferring what is compiled in and falling back
1117-
/// to the client's own catalogue.
1118-
///
1119-
/// That order, not the other way round: the WoW families were read out of a
1120-
/// real log and carry decisions the catalogue cannot express, a PTR and a
1121-
/// beta live in the retail tile's version list, and Forever is a tile of its
1122-
/// own. Everything else comes from [`crate::families`], which knows every
1123-
/// title Blizzard publishes rather than the two that were observed.
1088+
/// A TACT code here makes the client fall back to its first family, Warcraft
1089+
/// III, which is where `launch wow_classic_era` used to land. The compiled-in
1090+
/// WoW families come first (a PTR and a beta live in the retail tile, Forever
1091+
/// has its own); everything else comes from [`crate::families`].
11241092
pub fn resolve_game_uid(code: &ProductCode, families: &crate::families::Families) -> Option<String> {
11251093
game_uid(code).or_else(|| families.family_of(code.as_str()).map(str::to_owned))
11261094
}

‎crates/core/src/config.rs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,11 @@ pub struct Config {
108108
pub battle_net_minimized: bool,
109109
/// How Battle.net and every game it launches are started.
110110
///
111-
/// **Shared, not per-game, and that is the mechanism rather than a
112-
/// simplification.** Lightforge only ever sets the environment of a
113-
/// Battle.net client it starts *cold*; a client that is already up takes
114-
/// the launch over its own IPC queue, where there is no new process to
115-
/// give an environment to. Two games started from one client session
116-
/// therefore cannot have different environments, so offering a per-game
117-
/// control would be a lie; see `SPEC.md`, "Per-product pinning".
118-
///
119-
/// Only the host and runtime half of [`crate::tweaks::Tweaks`] is read
120-
/// here. A hand-off game has no `Config.wtf`, so
121-
/// [`crate::tweaks::Tweaks::cvars`] is never called on this value.
111+
/// Shared, not per game: Lightforge only sets the environment of a client
112+
/// it starts cold, and a running client takes launches over IPC, so two
113+
/// games from one session cannot differ (`SPEC.md`, "Per-product
114+
/// pinning"). A hand-off game has no `Config.wtf`, so only the host and
115+
/// runtime half of [`crate::tweaks::Tweaks`] is read.
122116
#[serde(default)]
123117
pub battle_net_tweaks: crate::tweaks::Tweaks,
124118
/// Which of Blizzard's regions products are downloaded from.

0 commit comments

Comments
 (0)