Skip to main content

pacenotes_installer\wizard\scene/
game_folder_routes.rs

1//! Scene 3's three routes, Download/Update, Configure/Install and
2//! Restore/Uninstall, as buttons in the page: alternatives, not a primary and an
3//! afterthought, one click each.
4
5use windows_sys::Win32::Foundation::{HWND, LPARAM};
6use windows_sys::Win32::UI::Controls as ctl;
7use windows_sys::Win32::UI::WindowsAndMessaging as wm;
8
9// No route means anything until a game is settled.
10pub unsafe fn show_route_buttons(dialog: HWND) {
11    let ready = crate::wizard::page::state(dialog)
12        .and_then(|state| state.game)
13        .is_some();
14    crate::ui::enable(dialog, crate::wizard::control_ids::IDC_S3_DOWNLOAD, ready);
15    crate::ui::enable(dialog, crate::wizard::control_ids::IDC_S3_CONFIGURE, ready);
16    crate::ui::enable(dialog, crate::wizard::control_ids::IDC_S3_RESTORE, ready);
17}
18
19/// The main route is this scene's way forward, so it is painted like Next;
20/// the other two keep the theme's look.
21pub unsafe fn paint_main_route(dialog: HWND, lparam: LPARAM) -> isize {
22    let draw = &*(lparam as *const ctl::NMCUSTOMDRAW);
23    if draw.hdr.idFrom != usize::from(crate::wizard::control_ids::IDC_S3_DOWNLOAD) {
24        return 0;
25    }
26    crate::wizard::page::reply(dialog, crate::ui::paint_forward_button(draw))
27}
28
29/// Starts the log and opens the cache in the game folder, then jumps to the
30/// route's first page by page id, since at most one of them is the next page.
31pub unsafe fn take_route(dialog: HWND, route: crate::wizard::state::Route) {
32    let Some(state) = crate::wizard::page::state(dialog) else {
33        return;
34    };
35    if state.game.is_none() {
36        super::game_folder::warn_no_game(dialog);
37        return;
38    }
39    // Every route writes into the folder, and this is where the user commits to it.
40    let folder = std::path::PathBuf::from(&state.folder);
41    if let Err(reason) = pacenotes_installer::engine::check_writable(&folder) {
42        refuse_unwritable(dialog, state, &reason.to_string());
43        return;
44    }
45    state.route = route;
46    // Both belong to the folder, and this is the moment it is settled.
47    attach_log(dialog, state);
48    open_cache(state);
49
50    let target = match route {
51        crate::wizard::state::Route::Download => crate::wizard::control_ids::IDD_S4_DOWNLOAD,
52        crate::wizard::state::Route::Configure => {
53            // Scene 5 reads the tree, here the cache plus the ledger, ticking what is already here.
54            super::download::load_tree(state, Vec::new());
55            // That replaced the catalogue scene 4 shows, so its next visit fetches it again.
56            state.catalogue_loaded = false;
57            if !super::configuration::has_something_to_pick(dialog, state) {
58                return;
59            }
60            state.chosen = state.tree.checked_keys();
61            let line = format!(
62                "configuring from what is already here: {} package(s)",
63                state.chosen.len()
64            );
65            super::progress_page::log_line(dialog, state, &line);
66            crate::wizard::control_ids::IDD_S5_CONFIG
67        }
68        // Reads the game folder, not the server, so the way back works when nothing else does.
69        crate::wizard::state::Route::Restore => crate::wizard::control_ids::IDD_RESTORE,
70    };
71    wm::SendMessageW(
72        crate::wizard::page::sheet(dialog),
73        ctl::PSM_SETCURSELID,
74        0,
75        target as LPARAM,
76    );
77}
78
79/// Says why, and offers to restart as administrator with what this run already
80/// knows (P11); already elevated, there is nothing to offer, since asking again
81/// would loop. The user stays on scene 3 unless the restart starts.
82unsafe fn refuse_unwritable(
83    dialog: HWND,
84    state: &mut crate::wizard::state::WizardState,
85    reason: &str,
86) {
87    let line = format!("the game folder cannot be written to: {reason}");
88    super::progress_page::log_line(dialog, state, &line);
89    if crate::elevation::is_elevated() {
90        let text =
91            crate::i18n::load_string_arg(crate::i18n::IDS_S3_NOT_WRITABLE_ADMIN, &state.folder);
92        crate::ui::message_box_warning(dialog, &text);
93        return;
94    }
95    let question = crate::i18n::load_string_arg(crate::i18n::IDS_S3_NOT_WRITABLE, &state.folder);
96    if !crate::ui::confirm(dialog, &question) {
97        return;
98    }
99    let restart = crate::elevation::Restart {
100        language: state.language,
101        folder: state.folder.clone(),
102        secret: state.secret.clone(),
103    };
104    if crate::elevation::restart_elevated(crate::wizard::page::sheet(dialog), &restart) {
105        // The elevated instance takes over, and waits for this one to be gone.
106        std::process::exit(0);
107    }
108    super::progress_page::log_line(dialog, state, "restarting as administrator was cancelled");
109}
110
111/// Opened per route taken, so a user who corrects the folder and starts again
112/// does not read the previous folder's downloads.
113fn open_cache(state: &mut crate::wizard::state::WizardState) {
114    let root = state.cache_override.clone().unwrap_or_else(|| {
115        pacenotes_installer::cache::dir_for(std::path::Path::new(&state.folder))
116    });
117    state.cache = pacenotes_installer::cache::Cache::open(root);
118    // An override may serve other folders, whose needs this one cannot see.
119    if state.cache_override.is_none() {
120        sweep_cache(state);
121    }
122}
123
124/// Removes the downloads nothing in this folder needs any more (I9), which
125/// only the folder itself can tell: the restore points need older versions
126/// the cache index has already forgotten.
127fn sweep_cache(state: &mut crate::wizard::state::WizardState) {
128    let in_use = pacenotes_installer::restore::payloads_in_use(std::path::Path::new(&state.folder));
129    if in_use.is_none() {
130        state
131            .log
132            .line("cache: a record here could not be read, so no download was removed");
133    }
134    let swept = state.cache.sweep(in_use.as_deref());
135    if swept.files > 0 {
136        state.log.line(format!(
137            "cache: removed {} file(s) nothing needs any more, {}",
138            swept.files,
139            pacenotes_installer::size::human(swept.bytes)
140        ));
141    }
142}
143
144/// The log belongs with the install it describes, so it moves into the game
145/// folder now, replaying what was said before.
146unsafe fn attach_log(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
147    let folder = std::path::PathBuf::from(&state.folder);
148    // One file per run, not per route button pressed within it.
149    let dir = pacenotes_installer::runlog::log_dir(&folder);
150    if state.log.path().and_then(|path| path.parent()) == Some(dir.as_path()) {
151        return;
152    }
153    let stamp =
154        pacenotes_installer::utc_time::file_stamp(pacenotes_installer::utc_time::unix_now());
155    state
156        .log
157        .attach(pacenotes_installer::runlog::log_path(&folder, &stamp));
158    if let Some(path) = state.log.path() {
159        crate::crash::log_to(path.to_path_buf());
160    }
161    if let Some(game) = state.game {
162        let line = format!("game: {} in {}", game.title(), state.folder);
163        super::progress_page::log_line(dialog, state, &line);
164    }
165}
166
167const VK_RETURN: usize = 0x0D;
168const DLGC_WANTALLKEYS: isize = 0x0004;
169const PATH_SUBCLASS: usize = 1;
170
171/// Enter in the path field takes the main route, Download/Update, as the page
172/// draws it as the default: the sheet sends Enter to its own Next, which this
173/// scene disables, so the field has to claim the key itself.
174pub unsafe fn enter_takes_main_route(dialog: HWND) {
175    let path = wm::GetDlgItem(dialog, i32::from(crate::wizard::control_ids::IDC_S3_PATH));
176    windows_sys::Win32::UI::Shell::SetWindowSubclass(path, Some(path_keys), PATH_SUBCLASS, 0);
177}
178
179unsafe extern "system" fn path_keys(
180    edit: HWND,
181    message: u32,
182    wparam: windows_sys::Win32::Foundation::WPARAM,
183    lparam: LPARAM,
184    _subclass: usize,
185    _reference: usize,
186) -> windows_sys::Win32::Foundation::LRESULT {
187    let enter = wparam == VK_RETURN;
188    match message {
189        wm::WM_GETDLGCODE if enter => {
190            windows_sys::Win32::UI::Shell::DefSubclassProc(edit, message, wparam, lparam)
191                | DLGC_WANTALLKEYS
192        }
193        wm::WM_KEYDOWN if enter => {
194            take_route(wm::GetParent(edit), crate::wizard::state::Route::Download);
195            0
196        }
197        // A single-line edit beeps at a carriage return it does not expect.
198        wm::WM_CHAR if enter => 0,
199        _ => windows_sys::Win32::UI::Shell::DefSubclassProc(edit, message, wparam, lparam),
200    }
201}