Files
benilla-octo/crates/benilla-app/src/no_install.rs
T
Sam SungandClaude Opus 5 694ef5832d A boot with no readable WoW install is fatal, with an error dialog (#1630)
With no WoW install found, or a `Data` folder whose archives will not open, benilla booted into a window of textless buttons, since fonts, art and the stock interface all come from the install.

It now ends before any window exists. A native error dialog (`rfd`: AppKit, Win32, `zenity` on Linux) names every folder searched, or the folder and the archive error, and says where to put the install; then it exits 1. An unattended run prints the same text to stderr and exits without a dialog.

1.12.1 ends at a fatal dialog for a present but corrupt base archive (`0x403b6f`) and only logs a missing one (`0x403b00`); ending at no install at all is recorded as a `Deviation:`.

Tests cover both messages and the failing exit; both cases were run live unattended. Built on macOS, Linux and Windows.

Fixes #836

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6f6531078333fcd0c0c1bcbbd8e61ce531cdf607)

# Conflicts:
#	docs/DEVIATIONS.md
#	docs/MAP.md

Absorbed into the octo fork. Generated docs reconciled: MAP.md stays ours, DEVIATIONS.md stays absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-25 23:03:53 -06:00

170 lines
6.4 KiB
Rust

//! The boot with no WoW install: fatal. Fonts, art and the stock interface all come from the
//! install, so a client without one is a window of textless buttons. The reference ships inside its
//! install; its archive mount (`0x403740`, `0x403b00`) logs a missing base archive and goes on, and
//! ends the process with a fatal error dialog (`SErrDisplayAppFatal2`, `0x403b6f`) only for one
//! that is present and fails its checks.
//!
//! Deviation: a boot with no install at all ends at a fatal error dialog, where the reference logs
//! each missing archive and goes on (`0x403b00`), because nothing past this point can draw a word.
//!
//! [`require`] is the decision, pure; [`fail`] is the exit, which shows a native dialog only to a
//! person: an [unattended](crate::run_mode::unattended) run prints the same message and exits.
use std::path::PathBuf;
use bevy::app::AppExit;
/// Why the client cannot start: the message a person is shown and a log carries.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct NoInstall {
title: &'static str,
message: String,
}
impl NoInstall {
pub(crate) fn message(&self) -> &str {
&self.message
}
/// A `Data` folder whose archives will not open, a corrupt one or none the chain knows: as fatal
/// as no folder, the case the reference's `0x403b6f` covers for a base archive.
pub(crate) fn unreadable(data_dir: &std::path::Path, err: &anyhow::Error) -> Self {
Self {
title: "benilla: the WoW install cannot be read",
message: format!(
"benilla found a World of Warcraft install at\n {}\nbut cannot read its archives:\n \
{err:#}\n\nCheck that this is an unmodified 1.12.1 Data folder.",
data_dir.display()
),
}
}
}
/// The install the boot goes on with, or the fatal error: `found` is [`benilla_formats::wow_data`]
/// and `looked_in` its [`benilla_formats::candidates`], named in order in the message.
pub(crate) fn require(found: Option<PathBuf>, looked_in: &[PathBuf]) -> Result<PathBuf, NoInstall> {
found.ok_or_else(|| NoInstall {
title: "benilla: no WoW install found",
message: message(looked_in),
})
}
/// What to tell the player: the folders searched and the two ways to supply an install.
fn message(looked_in: &[PathBuf]) -> String {
let mut out =
String::from("benilla needs a World of Warcraft 1.12.1 install and found none.\n");
if looked_in.is_empty() {
// `$WOW_DATA` set and empty ends the search with nothing in it.
out.push_str("\nWOW_DATA is set and empty, which means no install.\n");
} else {
out.push_str("\nIt looked in:\n");
for dir in looked_in {
out.push_str(&format!(" {}\n", dir.display()));
}
}
out.push_str(
"\nPut the install's Data folder at Data/ or WoW/Data/ beside the benilla binary, \
or point WOW_DATA at it.",
);
out
}
/// Report `err` and end the boot with a failing exit: stderr always, the dialog too unless the run
/// is `unattended`, so an instrumented run never waits on a click.
pub(crate) fn fail(err: &NoInstall, unattended: bool) -> AppExit {
eprintln!("benilla: {}", err.message());
if !unattended {
// Linux shows it through `zenity` and logs nothing when that is missing, which stderr covers.
rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title(err.title)
.set_description(err.message())
.set_buttons(rfd::MessageButtons::Ok)
.show();
}
AppExit::error()
}
/// [`require`] on the real resolver.
pub(crate) fn resolve() -> Result<PathBuf, NoInstall> {
require(benilla_formats::wow_data(), &benilla_formats::candidates())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn dirs() -> Vec<PathBuf> {
[
"/opt/wow/Data",
"/games/benilla/Data",
"/games/benilla/WoW/Data",
]
.map(PathBuf::from)
.to_vec()
}
/// With no install the boot is fatal, and the message names every folder searched, in order,
/// and both ways to supply one.
#[test]
fn no_install_is_fatal_and_names_every_folder_searched() {
let looked_in = dirs();
let err = require(None, &looked_in).expect_err("no install must be fatal");
let msg = err.message();
let mut from = 0;
for dir in &looked_in {
let at = msg[from..]
.find(&dir.display().to_string())
.unwrap_or_else(|| panic!("{dir:?} not named in order in:\n{msg}"));
from += at;
}
assert!(
msg.contains("Data/ or WoW/Data/ beside the benilla binary"),
"{msg}"
);
assert!(msg.contains("WOW_DATA"), "{msg}");
}
/// `WOW_DATA=` (set, empty) searches nowhere, and the message says why instead of listing none.
#[test]
fn an_empty_wow_data_is_named_as_the_reason() {
let err = require(None, &[]).expect_err("no install must be fatal");
assert!(
err.message().contains("WOW_DATA is set and empty"),
"{}",
err.message()
);
assert!(!err.message().contains("looked in"));
}
/// An install found goes on, whatever else was searched.
#[test]
fn a_found_install_boots() {
let found = Path::new("/games/benilla/Data").to_path_buf();
assert_eq!(require(Some(found.clone()), &dirs()), Ok(found));
}
/// The unattended exit is a failing one: `AppExit::error()` is what the launcher returns as the
/// process status. (The attended branch shows a dialog, so no test takes it.)
#[test]
fn the_unattended_exit_fails() {
let err = require(None, &dirs()).unwrap_err();
assert_ne!(fail(&err, true), AppExit::Success);
}
/// A folder whose archives will not open names the folder and the error, under its own title.
#[test]
fn an_unreadable_install_names_the_folder_and_the_error() {
let err = NoInstall::unreadable(
Path::new("/games/benilla/Data"),
&anyhow::anyhow!("no known vanilla MPQs found in /games/benilla/Data"),
);
assert!(err
.message()
.contains("/games/benilla/Data\nbut cannot read"));
assert!(err.message().contains("no known vanilla MPQs"));
assert_ne!(err.title, "benilla: no WoW install found");
}
}