|
38 | 38 | TEXT_DIM, TEXT_MAIN, |
39 | 39 | FONT_NORMAL, FONT_BOLD, FONT_SMALL, |
40 | 40 | ) |
| 41 | +from wizards._install_as_mod import derive_mod_name, register_as_mod |
41 | 42 |
|
42 | 43 | _ARCHIVE_EXTS = {".zip", ".7z", ".rar", ".tar", ".tar.gz", ".tar.bz2", ".tar.xz"} |
43 | 44 |
|
@@ -207,7 +208,9 @@ def __init__( |
207 | 208 | self._archive_path: Path | None = None |
208 | 209 | self._resolved_download_url: str | None = None |
209 | 210 | self._game_root: Path | None = game.get_game_path() |
210 | | - self._install_to_root_folder = ctk.BooleanVar(value=False) |
| 211 | + # "game" = extract to game folder (default), "root" = Root_Folder staging, |
| 212 | + # "mod" = install as a managed mod (staging dir + modlist entry + rootFolder flag) |
| 213 | + self._install_mode = ctk.StringVar(value="game") |
211 | 214 |
|
212 | 215 | title_bar = ctk.CTkFrame(self, fg_color=BG_HEADER, corner_radius=0, height=40) |
213 | 216 | title_bar.pack(fill="x") |
@@ -237,6 +240,22 @@ def _clear_body(self): |
237 | 240 | for w in self._body.winfo_children(): |
238 | 241 | w.destroy() |
239 | 242 |
|
| 243 | + def _build_install_mode_chooser(self, parent) -> ctk.CTkFrame: |
| 244 | + """Three-way radio: install to game folder, Root_Folder staging, or as a mod.""" |
| 245 | + frame = ctk.CTkFrame(parent, fg_color="transparent") |
| 246 | + for label, value in ( |
| 247 | + ("Install to game folder", "game"), |
| 248 | + ("Install to Root_Folder (staging)", "root"), |
| 249 | + ("Install as mod (managed in modlist)", "mod"), |
| 250 | + ): |
| 251 | + ctk.CTkRadioButton( |
| 252 | + frame, text=label, |
| 253 | + variable=self._install_mode, value=value, |
| 254 | + font=FONT_SMALL, text_color=TEXT_DIM, |
| 255 | + fg_color=ACCENT, hover_color=ACCENT_HOV, |
| 256 | + ).pack(anchor="w", pady=(0, 2)) |
| 257 | + return frame |
| 258 | + |
240 | 259 | # ------------------------------------------------------------------ |
241 | 260 | # Step 1 — Download (GitHub auto-download or manual page fallback) |
242 | 261 | # ------------------------------------------------------------------ |
@@ -322,14 +341,7 @@ def _show_step_download_github(self): |
322 | 341 | self._dl_progress.pack(pady=(0, 16)) |
323 | 342 | self._dl_progress.start() |
324 | 343 |
|
325 | | - ctk.CTkCheckBox( |
326 | | - self._body, |
327 | | - text="Install to Root_Folder (staging) instead of game folder", |
328 | | - variable=self._install_to_root_folder, |
329 | | - font=FONT_SMALL, text_color=TEXT_DIM, |
330 | | - fg_color=ACCENT, hover_color=ACCENT_HOV, |
331 | | - checkmark_color="white", |
332 | | - ).pack(pady=(0, 8)) |
| 344 | + self._build_install_mode_chooser(self._body).pack(pady=(0, 8)) |
333 | 345 |
|
334 | 346 | btn_frame = ctk.CTkFrame(self._body, fg_color="transparent") |
335 | 347 | btn_frame.pack(side="bottom", pady=(8, 0)) |
@@ -448,14 +460,7 @@ def _show_step_download_manual(self): |
448 | 460 | font=FONT_SMALL, text_color="#e06c6c", |
449 | 461 | ).pack(pady=(0, 8)) |
450 | 462 |
|
451 | | - ctk.CTkCheckBox( |
452 | | - self._body, |
453 | | - text="Install to Root_Folder (staging) instead of game folder", |
454 | | - variable=self._install_to_root_folder, |
455 | | - font=FONT_SMALL, text_color=TEXT_DIM, |
456 | | - fg_color=ACCENT, hover_color=ACCENT_HOV, |
457 | | - checkmark_color="white", |
458 | | - ).pack(pady=(0, 12)) |
| 463 | + self._build_install_mode_chooser(self._body).pack(pady=(0, 12)) |
459 | 464 |
|
460 | 465 | ctk.CTkButton( |
461 | 466 | self._body, text="Next \u2192", width=120, height=36, |
@@ -564,38 +569,57 @@ def _show_step_extract(self): |
564 | 569 |
|
565 | 570 | def _do_extract(self): |
566 | 571 | try: |
567 | | - if self._install_to_root_folder.get(): |
568 | | - game_root = self._game.get_effective_root_folder_path() |
569 | | - game_root.mkdir(parents=True, exist_ok=True) |
570 | | - else: |
571 | | - game_root = self._game_root |
572 | | - if game_root is None: |
573 | | - raise RuntimeError("Game path is not configured.") |
574 | | - |
| 572 | + mode = self._install_mode.get() |
575 | 573 | archive = self._archive_path |
576 | 574 | if archive is None or not archive.is_file(): |
577 | 575 | raise RuntimeError("Archive not found.") |
578 | 576 |
|
579 | | - self._set_status("Restoring game to vanilla state\u2026") |
580 | | - try: |
581 | | - self._game.restore(log_fn=self._log) |
582 | | - except Exception as exc: |
583 | | - self._log(f"Wizard: restore skipped or failed: {exc}") |
584 | | - |
585 | | - self._set_status("Extracting archive to game folder\u2026") |
586 | | - self._log(f"Wizard: extracting {archive.name} \u2192 {game_root}") |
587 | | - |
588 | | - paths = _extract_archive(archive, game_root) |
| 577 | + install_as_mod = mode == "mod" |
| 578 | + mod_name: str | None = None |
| 579 | + if install_as_mod: |
| 580 | + staging = self._game.get_effective_mod_staging_path() |
| 581 | + if staging is None: |
| 582 | + raise RuntimeError("Mod staging path is not configured.") |
| 583 | + mod_name = derive_mod_name(archive, fallback="Script Extender") |
| 584 | + dest = staging / mod_name |
| 585 | + if dest.exists(): |
| 586 | + shutil.rmtree(dest, ignore_errors=True) |
| 587 | + dest.mkdir(parents=True, exist_ok=True) |
| 588 | + elif mode == "root": |
| 589 | + dest = self._game.get_effective_root_folder_path() |
| 590 | + dest.mkdir(parents=True, exist_ok=True) |
| 591 | + else: |
| 592 | + dest = self._game_root |
| 593 | + if dest is None: |
| 594 | + raise RuntimeError("Game path is not configured.") |
| 595 | + self._set_status("Restoring game to vanilla state\u2026") |
| 596 | + try: |
| 597 | + self._game.restore(log_fn=self._log) |
| 598 | + except Exception as exc: |
| 599 | + self._log(f"Wizard: restore skipped or failed: {exc}") |
| 600 | + |
| 601 | + dest_label = { |
| 602 | + "mod": f"mod folder ({mod_name})", |
| 603 | + "root": "Root_Folder (staging)", |
| 604 | + "game": "game folder", |
| 605 | + }[mode] |
| 606 | + self._set_status(f"Extracting archive to {dest_label}\u2026") |
| 607 | + self._log(f"Wizard: extracting {archive.name} \u2192 {dest}") |
| 608 | + |
| 609 | + paths = _extract_archive(archive, dest) |
589 | 610 | file_count = len([p for p in paths if p.is_file()]) |
590 | 611 | self._log(f"Wizard: extracted {file_count} file(s).") |
591 | 612 |
|
| 613 | + if install_as_mod: |
| 614 | + register_as_mod(self._game, mod_name, archive, |
| 615 | + parent_widget=self, log_fn=self._log) |
| 616 | + |
592 | 617 | try: |
593 | 618 | archive.unlink() |
594 | 619 | self._log(f"Wizard: deleted {archive.name} from Downloads.") |
595 | 620 | except OSError as exc: |
596 | 621 | self._log(f"Wizard: could not delete archive: {exc}") |
597 | 622 |
|
598 | | - dest_label = "Root_Folder (staging)" if self._install_to_root_folder.get() else "game folder" |
599 | 623 | self._set_status( |
600 | 624 | f"Script extender installed successfully!\n" |
601 | 625 | f"{file_count} file(s) extracted to the {dest_label}.\n\n" |
|
0 commit comments