commit ff94d6c4e5c2700f7741ad292b0e2b7d99bbc78e Author: x3 <1+x3@noreply.git.sysdaemon.xyz> Date: Mon Sep 21 11:09:15 2026 +0000 Upload files to "/" diff --git a/Bildschirmfoto vom 2026-09-21 13-08-17.png b/Bildschirmfoto vom 2026-09-21 13-08-17.png new file mode 100644 index 0000000..8698bc0 Binary files /dev/null and b/Bildschirmfoto vom 2026-09-21 13-08-17.png differ diff --git a/beammp-gui.py b/beammp-gui.py new file mode 100644 index 0000000..9cd683e --- /dev/null +++ b/beammp-gui.py @@ -0,0 +1,856 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +BeamMP GUI Setup für BeamNG.drive (Proton) +- lädt BeamMP-Launcher.exe von GitHub herunter +- installiert ihn in das BeamNG-Protonprefix (compatdata/284160) +- trägt BeamMP als Non-Steam-Game in Steam ein (inkl. Proton-CompatToolMapping) + +Getestet auf Gentoo, Steam-Layout mit mehreren Bibliotheken (/mnt/Games + ~/.local/share/Steam). +Benötigt: python3 + PyGObject (Gtk 3). Kein tkinter nötig. +Start: ~/scripts/beammp-gui.py oder python3 ~/scripts/beammp-gui.py +""" +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib +import os, re, sys, shutil, struct, binascii, json +import urllib.request, threading, subprocess, time + +BEAMNG_APPID = "284160" +# Windows-Launcher (funktioniert im Proton-Prefix). Quelle: GitHub Releases. +BEAMMP_EXE_URL = "https://github.com/BeamMP/BeamMP-Launcher/releases/latest/download/BeamMP-Launcher.exe" +BEAMMP_FALLBACK_URL = "https://beammp.com/installer/BeamMP_Installer.zip" +CONFIG_FILE = os.path.expanduser("~/.config/beammp-gui/config.json") +WRAPPER_NAME = "beammp-launch.sh" + + +# -------------------------------------------------------------------------- +# Steam / BeamNG Erkennung +# -------------------------------------------------------------------------- + +def find_steam_root(): + candidates = [ + os.path.expanduser("~/.local/share/Steam"), + os.path.expanduser("~/.steam/steam"), + os.path.expanduser("~/.steam/root"), + ] + for p in candidates: + if os.path.isfile(os.path.join(p, "steamapps", "libraryfolders.vdf")): + return p + if os.path.isdir(os.path.join(p, "steamapps")): + return p + return None + + +def parse_libraryfolders(steam_root): + """Liest libraryfolders.vdf und gibt Liste von Bibliotheks-Pfaden zurück.""" + libs = [] + if steam_root and os.path.isdir(steam_root): + libs.append(steam_root) + lf = os.path.join(steam_root or "", "steamapps", "libraryfolders.vdf") + if os.path.isfile(lf): + try: + with open(lf, errors="replace") as f: + for line in f: + m = re.search(r'"path"\s+"([^"]+)"', line) + if m: + p = m.group(1).replace("\\\\", "/").replace("\\", "/") + # Steam escapt ggf. doppelte Slashes + p = os.path.normpath(p) + if os.path.isdir(p) and p not in libs: + libs.append(p) + except Exception: + pass + # Deduplizieren, existierende zuerst + out = [] + for p in libs: + if p not in out: + out.append(p) + return out + + +def parse_acf_installdir(acf_path): + try: + with open(acf_path, errors="replace") as f: + for line in f: + m = re.search(r'"installdir"\s+"([^"]+)"', line) + if m: + return m.group(1) + except Exception: + return None + return None + + +def find_beamng(): + """ + Gibt dict zurück: {game_dir, library, prefix, compatdata} + prefix = .../compatdata/284160/pfx + compatdata = .../compatdata/284160 + """ + steam = find_steam_root() + if not steam: + return None + libs = parse_libraryfolders(steam) + # 1. per appmanifest suchen (korrekte AppID 284160!) + for lib in libs: + acf = os.path.join(lib, "steamapps", f"appmanifest_{BEAMNG_APPID}.acf") + if os.path.isfile(acf): + installdir = parse_acf_installdir(acf) or "BeamNG.drive" + game_dir = os.path.join(lib, "steamapps", "common", installdir) + if os.path.isdir(game_dir): + compat = os.path.join(lib, "steamapps", "compatdata", BEAMNG_APPID) + prefix = os.path.join(compat, "pfx") + return {"game_dir": game_dir, "library": lib, + "compatdata": compat, "prefix": prefix, "steam": steam} + # 2. Fallback: common/BeamNG.drive Verzeichnis-Scan + for lib in libs: + gd = os.path.join(lib, "steamapps", "common", "BeamNG.drive") + if os.path.isdir(gd) and os.path.isfile(os.path.join(gd, "BeamNG.drive.exe")): + compat = os.path.join(lib, "steamapps", "compatdata", BEAMNG_APPID) + return {"game_dir": gd, "library": lib, + "compatdata": compat, "prefix": os.path.join(compat, "pfx"), + "steam": steam} + return None + + +def find_proton_versions(): + """Liste von (name, proton_binary, tool_dir).""" + steam = find_steam_root() + if not steam: + return [] + libs = parse_libraryfolders(steam) + search_bases = [] + for lib in libs: + search_bases.append(os.path.join(lib, "steamapps", "common")) + search_bases.append(os.path.join(lib, "compatibilitytools.d")) + # zusätzlich user-weite tools + search_bases.append(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d")) + search_bases.append(os.path.join(steam, "compatibilitytools.d")) + found = {} + for base in search_bases: + if not os.path.isdir(base): + continue + try: + for name in sorted(os.listdir(base)): + low = name.lower() + if "proton" not in low and "ge-" not in low and "wine" not in low: + continue + tool_dir = os.path.join(base, name) + proton_bin = os.path.join(tool_dir, "proton") + if os.path.isfile(proton_bin) and name not in found: + found[name] = (proton_bin, tool_dir) + except Exception: + continue + return [(n, pb, td) for n, (pb, td) in sorted(found.items())] + + +def proton_to_compat_name(folder_name): + low = folder_name.lower() + if "experimental" in low: + return "proton_experimental" + m = re.search(r"proton\s*0*(\d+)", low) + if m: + return f"proton_{m.group(1)}" + if "ge-proton" in low: + # GE-Proton: Steam nutzt internen Namen, Ordnername funktioniert meist nicht. + # Fallback auf proton_9 + Hinweis; Mapping wird trotzdem gesetzt. + mm = re.search(r"(\d+)", low) + if mm: + return f"proton_{mm.group(1)}" + return "proton_9" + if "hotfix" in low: + return "proton_hotfix" + return "proton_9" + + +def steam_running(): + # steam.pid existiert + Prozess läuft, oder pgrep steam + for cmd in (["pgrep", "-x", "steam"], ["pgrep", "-x", "steamwebhelper"]): + try: + r = subprocess.run(cmd, capture_output=True, timeout=5) + if r.returncode == 0 and r.stdout.strip(): + return True + except Exception: + pass + return False + + +# -------------------------------------------------------------------------- +# shortcuts.vdf (binär) lesen/schreiben – typ-erhaltend! +# Typen: 0x00 = Objekt, 0x01 = String, 0x02 = int32, 0x08 = Ende +# -------------------------------------------------------------------------- + +def _read_cstr(data, pos): + end = data.index(b"\x00", pos) + s = data[pos:end].decode("utf-8", errors="replace") + return s, end + 1 + + +def _parse_obj(data, pos): + """Parst Objekt-Inhalt ab pos, gibt (dict, neue_pos) zurück. End-Byte 0x08 wird konsumiert.""" + obj = {} + order = [] # Key-Reihenfolge + Typ merken + n = len(data) + while pos < n: + t = data[pos] + pos += 1 + if t == 0x08: + break + key, pos = _read_cstr(data, pos) + if t == 0x01: + val, pos = _read_cstr(data, pos) + obj[key] = val + order.append((key, 0x01)) + elif t == 0x02: + val = struct.unpack_from(" 0: + c = text[i] + if in_str: + if esc: + esc = False + elif c == "\\": + esc = True + elif c == '"': + in_str = False + else: + if c == '"': + in_str = True + elif c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + break + i += 1 + if depth != 0: + raise ValueError("config.vdf Klammern unausgeglichen") + text = text[:i] + "\n" + block + "\n\t\t\t\t" + text[i:] + backup = config_vdf_path + ".beammp-gui.bak" + shutil.copy2(config_vdf_path, backup) + with open(config_vdf_path, "w") as f: + f.write(text) + return backup + + +def remove_compat_mapping(config_vdf_path, unsigned_id): + """Entfernt Proton-Mapping wieder (Wrapper läuft nativ). Gibt True zurück wenn etwas entfernt wurde.""" + with open(config_vdf_path, errors="replace") as f: + text = f.read() + key = str(unsigned_id) + pat = re.compile(r'\s*"' + re.escape(key) + r'"\s*\{\s*"name"\s*"[^"]*"\s*"config"\s*"[^"]*"\s*"priority"\s*"[^"]*"\s*\}', re.DOTALL) + if not pat.search(text): + return False + text = pat.sub("", text, count=1) + backup = config_vdf_path + ".beammp-gui.bak" + shutil.copy2(config_vdf_path, backup) + with open(config_vdf_path, "w") as f: + f.write(text) + return True + + +# -------------------------------------------------------------------------- +# Download / Installation +# -------------------------------------------------------------------------- + +def download_file(url, dest, log, progress_cb=None): + req = urllib.request.Request(url, headers={"User-Agent": "beammp-gui/1.0"}) + with urllib.request.urlopen(req, timeout=120) as resp: + total = int(resp.headers.get("Content-Length") or 0) + got = 0 + chunk = 1024 * 256 + with open(dest, "wb") as f: + while True: + buf = resp.read(chunk) + if not buf: + break + f.write(buf) + got += len(buf) + if progress_cb: + GLib.idle_add(progress_cb, got, total) + return dest + + +def official_launcher_dest(prefix): + """Offizieller Ablageort im Prefix (wie Windows %AppData%).""" + return os.path.join(prefix, "drive_c", "users", "steamuser", + "AppData", "Roaming", "BeamMP-Launcher", "BeamMP-Launcher.exe") + + +# -------------------------------------------------------------------------- +# GUI +# -------------------------------------------------------------------------- + +class MainWindow(Gtk.ApplicationWindow): + def __init__(self, app): + super().__init__(application=app, title="BeamMP für BeamNG (Proton) – Setup") + self.set_default_size(760, 660) + self.set_border_width(10) + self.config = self._load_config() + self.beamng = None + self._build_ui() + GLib.idle_add(self.refresh) + + # -- config -- + def _load_config(self): + try: + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE) as f: + return json.load(f) + except Exception: + pass + return {} + + def _save_config(self): + try: + self.config.update({ + "launcher_path": self.ent_launcher.get_text(), + "steam_name": self.ent_name.get_text(), + "proton": self.cmb_proton.get_active_text() or "", + }) + os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True) + with open(CONFIG_FILE, "w") as f: + json.dump(self.config, f, indent=2) + except Exception: + pass + + # -- UI -- + def _build_ui(self): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + self.add(vbox) + + # Status + fr = Gtk.Frame(label=" Erkennung ") + vbox.pack_start(fr, False, False, 0) + sb = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + sb.set_border_width(6) + fr.add(sb) + self.lbl_steam = Gtk.Label(xalign=0, selectable=True) + self.lbl_beamng = Gtk.Label(xalign=0, selectable=True) + self.lbl_prefix = Gtk.Label(xalign=0, selectable=True) + self.lbl_proton = Gtk.Label(xalign=0, selectable=True) + for lab in (self.lbl_steam, self.lbl_beamng, self.lbl_prefix, self.lbl_proton): + lab.set_line_wrap(True) + sb.pack_start(lab, False, False, 1) + + # Einstellungen + fr2 = Gtk.Frame(label=" Einstellungen ") + vbox.pack_start(fr2, False, False, 0) + grid = Gtk.Grid(column_spacing=8, row_spacing=6) + grid.set_border_width(8) + fr2.add(grid) + + grid.attach(Gtk.Label(label="BeamMP-Launcher (.exe):", xalign=0), 0, 0, 1, 1) + self.ent_launcher = Gtk.Entry(hexpand=True) + self.ent_launcher.set_text(self.config.get("launcher_path", + os.path.expanduser("~/Downloads/BeamMP-Launcher.exe"))) + self.ent_launcher.set_tooltip_text("Pfad zur heruntergeladenen BeamMP-Launcher.exe") + grid.attach(self.ent_launcher, 1, 0, 1, 1) + hb = Gtk.Box(spacing=4) + b_dl = Gtk.Button(label="Herunterladen") + b_dl.connect("clicked", self.on_download) + b_br = Gtk.Button(label="...") + b_br.set_size_request(36, -1) + b_br.connect("clicked", self.on_browse_exe) + hb.pack_start(b_dl, False, False, 0) + hb.pack_start(b_br, False, False, 0) + grid.attach(hb, 2, 0, 1, 1) + + grid.attach(Gtk.Label(label="Proton-Version:", xalign=0), 0, 1, 1, 1) + self.cmb_proton = Gtk.ComboBoxText(hexpand=True) + grid.attach(self.cmb_proton, 1, 1, 1, 1) + b_rf = Gtk.Button(label="Aktualisieren") + b_rf.connect("clicked", lambda *_: self.refresh()) + grid.attach(b_rf, 2, 1, 1, 1) + + grid.attach(Gtk.Label(label="Name in Steam:", xalign=0), 0, 2, 1, 1) + self.ent_name = Gtk.Entry(hexpand=True) + self.ent_name.set_text(self.config.get("steam_name", "BeamMP")) + grid.attach(self.ent_name, 1, 2, 2, 1) + + self.progress = Gtk.ProgressBar(show_text=True) + self.progress.set_text("bereit") + grid.attach(self.progress, 0, 3, 3, 1) + + # Aktionen + fr3 = Gtk.Frame(label=" Aktionen (Reihenfolge einhalten) ") + vbox.pack_start(fr3, False, False, 0) + hbox = Gtk.Box(spacing=6) + hbox.set_border_width(8) + fr3.add(hbox) + b1 = Gtk.Button(label="1. Im Prefix installieren") + b1.get_style_context().add_class("suggested-action") + b1.connect("clicked", self.on_install) + b2 = Gtk.Button(label="2. Zu Steam hinzufügen") + b2.get_style_context().add_class("suggested-action") + b2.connect("clicked", self.on_add_steam) + b_all = Gtk.Button(label="Alles nacheinander") + b_all.connect("clicked", self.on_all) + for b in (b1, b2, b_all): + hbox.pack_start(b, True, True, 0) + + # Log + fr4 = Gtk.Frame(label=" Log ") + vbox.pack_start(fr4, True, True, 0) + sw = Gtk.ScrolledWindow() + sw.set_min_content_height(220) + fr4.add(sw) + self.log_buf = Gtk.TextBuffer() + self.log_view = Gtk.TextView(buffer=self.log_buf) + self.log_view.set_editable(False) + self.log_view.set_monospace(True) + self.log_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + sw.add(self.log_view) + self.show_all() + + # -- helpers -- + def log(self, msg): + def do(): + it = self.log_buf.get_end_iter() + self.log_buf.insert(it, f"[{time.strftime('%H:%M:%S')}] {msg}\n") + mark = self.log_buf.create_mark(None, self.log_buf.get_end_iter(), False) + self.log_view.scroll_mark_onscreen(mark) + GLib.idle_add(do) + print(msg, flush=True) + + def refresh(self): + steam = find_steam_root() + if steam: + self.lbl_steam.set_markup(f'Steam: OK {GLib.markup_escape_text(steam)}') + else: + self.lbl_steam.set_markup('Steam: nicht gefunden') + + self.beamng = find_beamng() + if self.beamng: + self.lbl_beamng.set_markup(f'BeamNG: OK {GLib.markup_escape_text(self.beamng["game_dir"])}') + pfx = self.beamng["prefix"] + if os.path.isdir(os.path.join(pfx, "drive_c")): + self.lbl_prefix.set_markup(f'Prefix (284160): OK {GLib.markup_escape_text(pfx)}') + else: + self.lbl_prefix.set_markup(f'Prefix (284160): noch nicht von Steam erstellt – bitte BeamNG einmal starten {GLib.markup_escape_text(pfx)}') + else: + self.lbl_beamng.set_markup('BeamNG: nicht gefunden (AppID 284160 – bitte in Steam installieren)') + self.lbl_prefix.set_markup('Prefix: –') + + pvs = find_proton_versions() + self.cmb_proton.remove_all() + if pvs: + self.lbl_proton.set_markup(f'Proton: {len(pvs)} gefunden') + for name, _pb, _td in pvs: + self.cmb_proton.append_text(name) + saved = self.config.get("proton", "") + names = [n for n, _, _ in pvs] + self.cmb_proton.set_active(names.index(saved) if saved in names else 0) + else: + self.lbl_proton.set_markup('Proton: keins gefunden – bitte z.B. Proton 9 in Steam installieren') + + def _selected_proton(self): + name = self.cmb_proton.get_active_text() + if not name: + return None + for n, pb, td in find_proton_versions(): + if n == name: + return (n, pb, td) + return None + + # -- Aktionen -- + def on_browse_exe(self, _): + dlg = Gtk.FileChooserDialog(title="BeamMP-Launcher.exe wählen", + action=Gtk.FileChooserAction.OPEN) + dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + dlg.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK) + flt = Gtk.FileFilter() + flt.set_name("Windows-Executables") + flt.add_pattern("*.exe") + dlg.add_filter(flt) + if dlg.run() == Gtk.ResponseType.OK: + self.ent_launcher.set_text(dlg.get_filename()) + dlg.destroy() + + def on_download(self, _): + dest = self.ent_launcher.get_text().strip() or os.path.expanduser("~/Downloads/BeamMP-Launcher.exe") + if os.path.isdir(dest): + dest = os.path.join(dest, "BeamMP-Launcher.exe") + os.makedirs(os.path.dirname(dest) or ".", exist_ok=True) + self.ent_launcher.set_text(dest) + self._save_config() + self.log(f"Download von {BEAMMP_EXE_URL} nach {dest} …") + + def work(): + try: + def prog(got, total): + if total: + self.progress.set_fraction(min(got / total, 1.0)) + self.progress.set_text(f"{got//1024//1024} / {total//1024//1024} MB") + return False + try: + download_file(BEAMMP_EXE_URL, dest, self.log, prog) + except Exception as e1: + self.log(f"Primär-URL fehlgeschlagen ({e1}), versuche Fallback …") + download_file(BEAMMP_FALLBACK_URL, dest + ".zip", self.log, prog) + self.log(f"Fallback-ZIP gespeichert. Bitte BeamMP-Launcher.exe daraus entpacken. ({dest}.zip)") + return + size = os.path.getsize(dest) + if size < 1_000_000: + # evtl. HTML-Fehlerseite + with open(dest, "rb") as f: + head = f.read(200) + if b" Launcher-Fehler Code 7. + wrapper = os.path.join(os.path.expanduser("~/scripts"), WRAPPER_NAME) + with open(wrapper, "w") as f: + f.write("#!/bin/bash\n") + f.write("# BeamMP im BeamNG-Protonprefix (284160) starten. Wird vom GUI-Skript erzeugt.\n") + f.write("# Steam-Shortcut zeigt auf DIESE Datei (nativ, KEIN Proton-Compat erzwingen).\n") + f.write(f'export STEAM_COMPAT_DATA_PATH="{compatdata}"\n') + f.write(f'export STEAM_COMPAT_CLIENT_INSTALL_PATH="{steam}"\n') + f.write(f'export STEAM_COMPAT_INSTALL_PATH="{game_dir}"\n') + f.write(f'cd "{game_dir}"\n') + f.write(f'exec "{proton_bin}" run "{exe_linux}"\n') + os.chmod(wrapper, 0o755) + self.log(f"Wrapper-Skript: {wrapper}") + + appid = steam_shortcut_appid(wrapper, name) + unsigned = appid & 0xFFFFFFFF + # Alte AppID (direkte Exe) zum Aufräumen des stale Compat-Mappings + old_appid = steam_shortcut_appid(exe_linux, name) + old_unsigned = old_appid & 0xFFFFFFFF + self.log(f"Shortcut-AppID: {appid} (unsigned {unsigned})") + + for uid in sorted(os.listdir(os.path.join(steam, "userdata"))): + cfg = os.path.join(steam, "userdata", uid, "config") + if not os.path.isdir(cfg): + continue + vdf = os.path.join(cfg, "shortcuts.vdf") + if os.path.isfile(vdf): + shutil.copy2(vdf, vdf + ".beammp-gui.bak") + try: + top, keys = read_shortcuts_vdf(vdf) if os.path.isfile(vdf) else ({}, []) + except Exception as e: + self.log(f"{vdf} konnte nicht gelesen werden ({e}) – lege neu an.") + top, keys = {}, [] + # Duplikat? (gleicher Name, alte direkte Exe oder Wrapper) + dup_key = None + for k, entry in top.items(): + if entry.get("AppName") == name or entry.get("Exe") in (exe_linux, wrapper): + dup_key = k + break + entry = { + "appid": appid, + "AppName": name, + "Exe": wrapper, + "StartDir": startdir, + "icon": "", + "ShortcutPath": "", + "LaunchOptions": "", + "IsHidden": 0, + "AllowDesktopConfig": 1, + "AllowOverlay": 1, + "OpenVR": 0, + "Devkit": 0, + "DevkitGameID": "", + "DevkitOverrideAppID": 0, + "LastPlayTime": int(time.time()), + "FlatpakAppID": "", + "tags": {}, + } + if dup_key is not None: + # Felder aktualisieren, LastPlayTime behalten falls neuer=0 + old = top[dup_key] + old.update(entry) + old["appid"] = appid + self.log(f"Eintrag '{name}' aktualisiert (User {uid}).") + else: + new_index = str(max([int(k) for k in top.keys()] + [-1]) + 1) + top[new_index] = entry + keys = sorted(top.keys(), key=int) + self.log(f"Eintrag '{name}' neu erstellt (User {uid}, Index {new_index}).") + write_shortcuts_vdf(vdf, top, keys) + self.log(f"shortcuts.vdf geschrieben: {vdf}") + + # Wrapper läuft NATIV (ruft selbst proton auf) -> KEIN Proton-Compat + # für den Shortcut erzwingen, sonst läuft der Wrapper doppelt unter Proton. + # Altes Mapping der direkten Exe ebenfalls entfernen. + config_vdf = os.path.join(steam, "config", "config.vdf") + if os.path.isfile(config_vdf): + try: + for uid_num in {unsigned, old_unsigned}: + if remove_compat_mapping(config_vdf, uid_num): + self.log(f"CompatToolMapping für {uid_num} entfernt (nativer Wrapper).") + self.log("Wichtig: In Steam bei BeamMP unter Eigenschaften → Kompatibilität " + "KEIN Häkchen setzen (nativ über Wrapper).") + except Exception as e: + self.log(f"Hinweis: CompatMapping-Aufräumen fehlgeschlagen ({e}).") + else: + self.log(f"Hinweis: {config_vdf} nicht gefunden.") + self.log("Fertig! Steam neu starten – BeamMP erscheint in der Bibliothek.") + self.log("Tipp: Falls Launcher Code 7 meldet, BeamNG einmal normal starten (erzeugt BeamNG.drive.ini).") + except Exception as e: + import traceback + self.log(f"FEHLER beim Steam-Eintrag: {e}") + self.log(traceback.format_exc(limit=3).replace(chr(10), " | ")) + + def on_all(self, _): + self.log("Führe Schritt 1 aus, Schritt 2 bitte nach Abschluss von 1 klicken.") + self.on_install(_) + + +class App(Gtk.Application): + def __init__(self): + super().__init__(application_id="com.beammp.gui") + self.win = None + + def do_activate(self): + if not self.win: + self.win = MainWindow(self) + self.win.show_all() + self.win.present() + + +def main(): + if "--detect" in sys.argv: + # Headless-Diagnose für Tests + print("steam:", find_steam_root()) + print("libs:", parse_libraryfolders(find_steam_root() or "")) + print("beamng:", find_beamng()) + print("proton:", [(n, pb) for n, pb, _ in find_proton_versions()]) + return + app = App() + app.run() + + +if __name__ == "__main__": + main()