diff --git a/mod_manager.py b/mod_manager.py index 464d054..cf174e7 100644 --- a/mod_manager.py +++ b/mod_manager.py @@ -205,6 +205,203 @@ TWEAK_EXTS = {".yaml", ".yml"} DLL_EXTS = {".dll"} +# =========================================================================== +# REDSCRIPT COMPILER — Compile r6/scripts/ via Wine +# =========================================================================== + +def find_scc_exe(): + """Find the redscript compiler (scc.exe).""" + # Check Fluorine mods first + fluorine_scc = Path.home() / ".local/share/fluorine/Cyberpunk 2077/mods/redscript/engine/tools/scc.exe" + if fluorine_scc.exists(): + return fluorine_scc + + # Check game directory + game_scc = Path("/mnt/Spiele/Heroic/Cyberpunk 2077/engine/tools/scc.exe") + if game_scc.exists(): + return game_scc + + return None + + +def find_wine_prefix(): + """Find a working Wine prefix.""" + prefixes = [ + Path.home() / ".local/share/fluorine/Prefix/pfx", + Path.home() / ".wine", + ] + for p in prefixes: + if p.exists(): + return p + return None + + +def compile_redscript(game_path, progress_callback=None): + """Run the redscript compiler (scc.exe) via Wine. + + Compiles all .reds files in r6/scripts/ into r6/cache/final.redscripts. + Returns (success, output_text). + """ + game_path = Path(game_path) + scc_exe = find_scc_exe() + + if not scc_exe: + return False, "redscript compiler (scc.exe) not found. Install the 'redscript' mod first." + + wine_prefix = find_wine_prefix() + if not wine_prefix: + return False, "No Wine prefix found. Install Wine/Proton first." + + # Ensure r6/scripts exists + scripts_dir = game_path / "r6/scripts" + if not scripts_dir.exists(): + scripts_dir.mkdir(parents=True, exist_ok=True) + + # Ensure r6/cache exists + cache_dir = game_path / "r6/cache" + cache_dir.mkdir(parents=True, exist_ok=True) + + if progress_callback: + progress_callback("Compiling redscript...") + + import subprocess + import os + + env = os.environ.copy() + env["WINEPREFIX"] = str(wine_prefix) + env["WINEDEBUG"] = "-all" + env["WINEARCH"] = "win64" + + try: + result = subprocess.run( + ["wine", str(scc_exe), "-compile", "r6/scripts"], + cwd=str(game_path), + env=env, + capture_output=True, + text=True, + timeout=120 + ) + + output = result.stdout + result.stderr + success = "Output successfully saved" in output or "Compilation complete" in output + + if success: + if progress_callback: + progress_callback("✅ redscript compiled successfully!") + return True, output + else: + if progress_callback: + progress_callback("❌ redscript compilation failed!") + return False, output + + except subprocess.TimeoutExpired: + return False, "Compilation timed out (120s)" + except Exception as e: + return False, f"Error: {e}" + + +def install_mod_from_folder(mod_folder, game_path, progress_callback=None): + """Install a mod from a folder to the game directory. + + Handles all file types: + - .archive → archive/pc/mod/ + - .xl → archive/pc/mod/ + - .reds → r6/scripts// (preserving structure) + - .yaml → r6/tweaks// (preserving structure) + - .lua → bin/x64/plugins/cyber_engine_tweaks/mods// + - .dll → red4ext/plugins// + - .ini → engine/config/ + - Scripts under red4ext/plugins/*/Scripts/ → also symlinked to r6/scripts/ + """ + mod_folder = Path(mod_folder) + game_path = Path(game_path) + mod_name = mod_folder.name + + installed = {"archive": 0, "xl": 0, "reds": 0, "yaml": 0, "lua": 0, "dll": 0, "ini": 0, "other": 0} + errors = [] + + # Ensure dirs exist + for d in ["archive/pc/mod", "r6/scripts", "r6/tweaks", "engine/config", + "bin/x64/plugins/cyber_engine_tweaks/mods", "red4ext/plugins"]: + (game_path / d).mkdir(parents=True, exist_ok=True) + + for root, dirs, files in os.walk(mod_folder): + for f in files: + src = Path(root) / f + ext = src.suffix.lower() + rel_path = src.relative_to(mod_folder) + + try: + if ext == ".archive": + dst = game_path / "archive/pc/mod" / f + shutil.copy2(src, dst) + installed["archive"] += 1 + + elif ext == ".xl": + dst = game_path / "archive/pc/mod" / f + shutil.copy2(src, dst) + installed["xl"] += 1 + + elif ext == ".reds": + # Check if under red4ext/plugins/*/Scripts/ — copy to BOTH locations + parts = rel_path.parts + if "red4ext" in parts and "Scripts" in parts: + # Copy to original red4ext location + dst = game_path / rel_path + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + # ALSO copy to r6/scripts// for compiler + script_dst = game_path / "r6/scripts" / mod_name / f + script_dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, script_dst) + else: + # Normal redscript — preserve structure + dst = game_path / "r6/scripts" / mod_name / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["reds"] += 1 + + elif ext in [".yaml", ".yml"]: + dst = game_path / "r6/tweaks" / mod_name / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["yaml"] += 1 + + elif ext == ".lua": + dst = game_path / "bin/x64/plugins/cyber_engine_tweaks/mods" / mod_name / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["lua"] += 1 + + elif ext == ".dll": + # red4ext plugins — preserve structure + dst = game_path / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["dll"] += 1 + + elif ext == ".ini": + if "config" in str(root).lower() or "engine" in str(root).lower(): + dst = game_path / "engine/config" / f + else: + dst = game_path / "bin/x64/plugins/cyber_engine_tweaks/mods" / mod_name / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["ini"] += 1 + + elif ext in [".json", ".toml", ".txt", ".mp3"]: + # Preserve structure for data files + dst = game_path / str(rel_path) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + installed["other"] += 1 + + except Exception as e: + errors.append(f"{rel_path}: {e}") + + return installed, errors + + def extract_archive(filepath, extract_to): """Extract a .7z/.zip/.rar file to a temporary directory. @@ -887,6 +1084,14 @@ class ModManagerWindow(QMainWindow): toggle_script_btn.clicked.connect(self._toggle_script_mod) btn_layout.addWidget(toggle_script_btn) + install_folder_btn = QPushButton("📂 Install from Folder") + install_folder_btn.clicked.connect(self._install_from_folder) + btn_layout.addWidget(install_folder_btn) + + compile_btn = QPushButton("🔨 Compile redscript") + compile_btn.clicked.connect(self._compile_redscript) + btn_layout.addWidget(compile_btn) + btn_layout.addStretch() layout.addLayout(btn_layout) @@ -1278,6 +1483,72 @@ class ModManagerWindow(QMainWindow): menu.exec(self.script_tree.viewport().mapToGlobal(pos)) + def _install_from_folder(self): + """Install mods from a folder.""" + folder = QFileDialog.getExistingDirectory(self, "Select mod folder", str(Path.home() / "Dokumente")) + if not folder: + return + + game_path = self.config["game_path"] + mod_folder = Path(folder) + + # Check if it's a parent folder with multiple mods + subdirs = [d for d in mod_folder.iterdir() if d.is_dir() and not d.name.startswith(".")] + + reply = QMessageBox.question(self, "Install Mods", + f"Install {len(subdirs)} mods from:\n{mod_folder}\n\nTo: {game_path}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + if reply != QMessageBox.StandardButton.Yes: + return + + total_installed = {} + total_errors = [] + + for subdir in subdirs: + installed, errors = install_mod_from_folder(subdir, game_path) + for k, v in installed.items(): + total_installed[k] = total_installed.get(k, 0) + v + total_errors.extend(errors) + + summary = "\n".join(f" {k}: {v}" for k, v in total_installed.items() if v > 0) + summary += f"\n Total: {sum(total_installed.values())} files" + + if total_errors: + summary += f"\n\nErrors ({len(total_errors)}):" + for e in total_errors[:5]: + summary += f"\n {e}" + if len(total_errors) > 5: + summary += f"\n ... and {len(total_errors) - 5} more" + + QMessageBox.information(self, "Installation Complete", summary) + self.status.showMessage(f"Installed {sum(total_installed.values())} files") + self.refresh_script_mods() + + def _compile_redscript(self): + """Compile redscript via Wine.""" + game_path = self.config["game_path"] + + scc = find_scc_exe() + if not scc: + QMessageBox.warning(self, "redscript not found", + "redscript compiler (scc.exe) not found.\n" + "Install the 'redscript' mod first.\n" + "Check: Fluorine mods or game directory.") + return + + self.status.showMessage("Compiling redscript... please wait") + QApplication.processEvents() + + success, output = compile_redscript(game_path) + + if success: + self.status.showMessage("✅ redscript compiled successfully!") + QMessageBox.information(self, "redscript", "✅ Compilation successful!\n\n" + output[-500:]) + else: + self.status.showMessage("❌ redscript compilation failed") + QMessageBox.warning(self, "redscript", "❌ Compilation failed:\n\n" + output[-1000:]) + def _on_mod_checkbox_changed(self, item, column): if column != 0: return