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:
arch_agent
2026-07-19 16:20:28 +02:00
parent 4d86ac9fc2
commit 4b616cd580
+271
View File
@@ -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