v0.4: Install from Folder + redscript Compiler via Wine
- Install from Folder: install all mods from a directory to the game - redscript Compiler: compile r6/scripts via scc.exe + Wine - Codeware fix: .reds under red4ext/plugins/*/Scripts/ also copied to r6/scripts/ - Buttons: 'Install from Folder' + 'Compile redscript' in Script Mods tab - No Fluorine/MO2 needed — direct install + compile
This commit is contained in:
+271
@@ -205,6 +205,203 @@ TWEAK_EXTS = {".yaml", ".yml"}
|
|||||||
DLL_EXTS = {".dll"}
|
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/<modname>/ (preserving structure)
|
||||||
|
- .yaml → r6/tweaks/<modname>/ (preserving structure)
|
||||||
|
- .lua → bin/x64/plugins/cyber_engine_tweaks/mods/<modname>/
|
||||||
|
- .dll → red4ext/plugins/<modname>/
|
||||||
|
- .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/<modname>/ 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):
|
def extract_archive(filepath, extract_to):
|
||||||
"""Extract a .7z/.zip/.rar file to a temporary directory.
|
"""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)
|
toggle_script_btn.clicked.connect(self._toggle_script_mod)
|
||||||
btn_layout.addWidget(toggle_script_btn)
|
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()
|
btn_layout.addStretch()
|
||||||
layout.addLayout(btn_layout)
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
@@ -1278,6 +1483,72 @@ class ModManagerWindow(QMainWindow):
|
|||||||
|
|
||||||
menu.exec(self.script_tree.viewport().mapToGlobal(pos))
|
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):
|
def _on_mod_checkbox_changed(self, item, column):
|
||||||
if column != 0:
|
if column != 0:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user