diff --git a/DEVELOPMENT_STATUS.md b/DEVELOPMENT_STATUS.md new file mode 100644 index 0000000..f1ceca2 --- /dev/null +++ b/DEVELOPMENT_STATUS.md @@ -0,0 +1,41 @@ +# Development status — 0.8.0 + +This archive is a **development preview**, not a claim that Star Citizen has +been end-to-end validated on real gaming hardware yet. + +## Verified in this build + +- Shell scripts pass `bash -n`. +- Go unit tests pass. +- Go backend builds as version `0.8.0`. +- Git-plugin updater integration test passes. +- Backend contains the owned `game-status`, `game-install`, `game-launch`, and + `game-repair` paths. +- Hardware gate detects the QEMU/Bochs test environment that was found during + development instead of looping through Wine repairs. +- Wine compatibility selection can reject newer runners and retain a compatible + fallback. +- LUG is optional and cannot block the owned install path. + +## Still needs real-hardware validation + +The following must be tested on an Omarchy machine with a real Vulkan-capable +AMD/NVIDIA/Intel GPU before calling 0.8.0 a production release: + +- fresh Wine prefix creation with the selected runner +- current Winetricks verbs (`arial`, `tahoma`, `dxvk`, `powershell`, `win11`) +- current RSI Launcher unattended installation +- first RSI login and game download +- actual Star Citizen launch and EAC behavior +- DXVK update/rollback on a populated prefix +- NGL installation on the owned prefix +- upgrade from an existing LUG-managed installation containing real game data + +## Maintenance contract + +To keep future maintenance manageable, the UI must not depend on Wine/DXVK/RSI +version-specific paths. Upstream details belong in the Go backend. The QML panel +should consume only backend state/actions. + +LUG documentation may be consulted for compatibility guidance, but its installer +is not part of the critical runtime contract. diff --git a/INSTALLATION.md b/INSTALLATION.md index 54a0278..1fc6a5e 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -1,4 +1,4 @@ -# Omarchy Citizen 0.7.1 – Rundum-Sorglos +# Omarchy Citizen 0.8.0 – Rundum-Sorglos ## Installation @@ -85,7 +85,7 @@ Das Paket enthält zusätzlich: - Maintenance-Ergebnis - Updater-Log -## Wine-Kompatibilitätsfallback (0.7.1) +## Wine-Kompatibilitätsfallback (0.8.0) Autopilot verwendet nicht blind die höchste Versionsnummer. diff --git a/Panel.qml b/Panel.qml index 6250601..552594d 100644 --- a/Panel.qml +++ b/Panel.qml @@ -12,7 +12,7 @@ Panel { property var anchorItem: null property var hostWidget: null - property string pluginVersion: "0.7.1" + property string pluginVersion: "0.8.0" property string health: "checking" property string depsState: "checking" property string depsMissing: "" @@ -39,6 +39,10 @@ Panel { property string managedWineVersion: "" property string managedDXVKVersion: "" property string maintenanceResult: "" + property string hardwareState: "unknown" + property string hardwareReason: "" + property string hardwareGPU: "" + property string hardwareVulkan: "unknown" property string actionStatus: "" property string actionError: "" property string actionKind: "" @@ -102,7 +106,7 @@ Panel { values[lines[i].slice(0, p)] = lines[i].slice(p + 1) } - pluginVersion = values.plugin_version || "0.7.1" + pluginVersion = values.plugin_version || "0.8.0" health = values.health || "setup" depsState = values.deps || "missing" depsMissing = values.deps_missing || "" @@ -127,6 +131,10 @@ Panel { managedWineVersion = values.managed_wine_version || "" managedDXVKVersion = values.managed_dxvk_version || "" maintenanceResult = values.maintenance_result || "" + hardwareState = values.hardware || "unknown" + hardwareReason = values.hardware_reason || "" + hardwareGPU = values.hardware_gpu || "" + hardwareVulkan = values.hardware_vulkan || "unknown" } function stateIcon(state) { @@ -163,6 +171,7 @@ Panel { } function healthColor() { + if (health === "hardware-blocked") return danger if (health === "ready") return success if (health === "recover-prefix" || health === "repair") return warning if (health === "checking") return accent @@ -170,6 +179,7 @@ Panel { } function headline() { + if (health === "hardware-blocked") return "HARDWARE NICHT SPIELBEREIT" if (health === "ready" && root.autopilot === "true") return "FLIGHT READY · AUTOPILOT" if (health === "ready") return "FLIGHT READY" if (health === "recover-prefix") return "SETUP REPAIR" @@ -179,23 +189,22 @@ Panel { } function subline() { + if (health === "hardware-blocked") + return root.hardwareReason !== "" ? root.hardwareReason : "Eine für Star Citizen geeignete Vulkan-GPU wurde nicht erkannt." if (health === "ready") - return "Alles Wesentliche wurde gefunden. Star Citizen ist einen Klick entfernt." - if (health === "recover-prefix") - return "Eine frühere Wine-Einrichtung ist nur teilweise vorhanden. Omarchy Citizen kann sie sichern und sauber neu aufsetzen." - if (health === "repair") - return "Omarchy Citizen hat eine reparierbare Lücke erkannt." + return "Wine, DXVK und RSI Launcher sind geprüft. Star Citizen ist einen Klick entfernt." if (health === "install-game") - return "Die Linux-Basis steht. Der RSI Launcher führt dich durch die Spielinstallation." - if (health === "install") - return "Die Linux-Basis steht. Der grafische Star-Citizen-Assistent übernimmt den Rest." - return "Omarchy Citizen richtet Abhängigkeiten, LUG Helper und Star-Citizen-Basis geführt ein." + return "Der Software-Stack ist bereit. Melde dich im offiziellen RSI Launcher an und starte dort den Spieldownload." + if (health === "repair" || health === "launcher-repair") + return "Omarchy Citizen kann den gesamten verwalteten Gaming-Stack automatisch reparieren." + return "Omarchy Citizen wählt selbst einen kompatiblen Wine-Runner, erstellt den Prefix, installiert DXVK/Komponenten und den offiziellen RSI Launcher." } function primaryText() { + if (health === "hardware-blocked") return "ⓘ HARDWARE-DETAILS" if (health === "ready") return "▶ STAR CITIZEN STARTEN" - if (health === "recover-prefix") return "↻ SETUP SICHER REPARIEREN" - if (health === "repair") return "↻ ERKANNTES PROBLEM BEHEBEN" + if (health === "install-game") return "▶ RSI LAUNCHER ÖFFNEN" + if (health === "repair" || health === "launcher-repair") return "↻ AUTOMATISCH REPARIEREN" return "▶ EINRICHTEN & STARTKLAR MACHEN" } @@ -259,10 +268,10 @@ Panel { onExited: function(exitCode) { if (exitCode === 0) { root.actionError = "" - root.actionStatus = root.actionKind === "primary" && root.health === "recover-prefix" - ? "Reparatur-Fenster wurde geöffnet. Dort läuft die sichere Wiederherstellung." + root.actionStatus = root.actionKind === "primary" && root.health === "install-game" + ? "RSI Launcher wurde gestartet." : (root.actionKind === "primary" && root.health !== "ready" - ? "Setup-Fenster wurde geöffnet. Folge dort einfach den angezeigten Schritten." + ? "Setup-Fenster wurde geöffnet. Omarchy Citizen übernimmt die technischen Schritte." : "Aktion wurde gestartet.") root.refreshStatus() @@ -391,7 +400,8 @@ Panel { bordered: true active: root.health === "ready" || actionProc.running onClicked: root.runAction( - root.health === "repair" ? "repair" : "primary", + root.health === "hardware-blocked" ? "hardware-info" : + ((root.health === "repair" || root.health === "launcher-repair") ? "repair" : "primary"), root.health === "ready" ) } @@ -580,6 +590,12 @@ Panel { font.bold: true } + StatusLine { + label: "GPU / Vulkan" + state: root.hardwareState === "blocked" ? "warning" : (root.hardwareState === "ready" ? "ready" : "unknown") + value: root.hardwareState === "blocked" ? "nicht spielbereit" : (root.hardwareGPU !== "" ? root.hardwareGPU : root.hardwareVulkan) + } + StatusLine { label: "Abhängigkeiten" state: root.depsState @@ -587,11 +603,11 @@ Panel { } StatusLine { - label: "LUG Helper" + label: "Support-Helper" state: root.helperState value: root.helperState === "ready" ? ((root.helperVersion !== "" ? root.helperVersion : "gefunden") + - (root.helperManaged === "aur" ? " · AUR" : " · extern")) + " · optional") : "nicht installiert" } @@ -604,9 +620,9 @@ Panel { } StatusLine { - label: "Start-Script" + label: "RSI Launcher" state: root.launcherState - value: root.launcherState === "ready" ? "bereit" : "fehlt" + value: root.launcherState === "ready" ? "installiert" : "fehlt" } PanelSeparator { foreground: root.barForeground } @@ -722,11 +738,11 @@ Panel { Button { width: setupGrid.cellWidth - text: root.checkingUpdate ? "Update wird geprüft…" : "LUG Update prüfen" + text: "Autopilot synchronisieren" foreground: root.barForeground fontFamily: root.uiFont bordered: true - onClicked: root.checkForUpdates() + onClicked: root.runAction("autopilot-maintain") } Button { @@ -749,7 +765,7 @@ Panel { Button { width: setupGrid.cellWidth - text: "LUG Helper öffnen" + text: "Support-Helper öffnen" foreground: root.barForeground fontFamily: root.uiFont bordered: true @@ -767,29 +783,29 @@ Panel { Button { width: setupGrid.cellWidth - text: "DXVK verwalten" + text: "DXVK automatisch prüfen" foreground: root.barForeground fontFamily: root.uiFont bordered: true - onClicked: root.runAction("dxvk") + onClicked: root.runAction("autopilot-maintain") } Button { width: setupGrid.cellWidth - text: "RSI reparieren" + text: "Gaming-Stack reparieren" foreground: root.barForeground fontFamily: root.uiFont bordered: true - onClicked: root.runAction("repair-rsi") + onClicked: root.runAction("repair") } Button { width: setupGrid.cellWidth - text: "Start-Script reparieren" + text: "Launcher-Dateien reparieren" foreground: root.barForeground fontFamily: root.uiFont bordered: true - onClicked: root.runAction("repair-launch") + onClicked: root.runAction("repair") } Button { diff --git a/README.md b/README.md index e1b52e0..c4eaf3c 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,51 @@ -# Omarchy Citizen 0.7.1 +# Omarchy Citizen 0.8.0 — Development Preview -## Rundum-Sorglos / Autopilot +Omarchy Citizen is moving from a frontend around the LUG installer to an +**owned, self-maintaining Star Citizen stack for Omarchy**. -Omarchy Citizen 0.7.1 changes the architecture from "frontend for packages the -user maintains" to "self-maintaining Star Citizen appliance". +## Architecture -Autopilot manages the complete user-space gaming stack: +The critical install and launch path is owned by this project: -- Omarchy Citizen plugin updates (for clean Git-managed installs) -- official Star Citizen LUG Helper **AppImage** -- stable `lug-wine-tkg-git` Wine runner -- DXVK -- Wine prefix health testing -- launch-script Wine runner selection -- repair / recovery -- support logs +1. hardware / Vulkan readiness gate +2. select the newest locally compatible stable LUG Wine runner +3. validate it in a disposable Wine prefix +4. create the real Wine prefix +5. dynamically fetch current Winetricks and install required verbs +6. dynamically fetch and verify the current RSI Launcher +7. manage DXVK and the stable launch entry point +8. periodically maintain the user-space gaming stack -The LUG Helper AppImage is preferred over the AUR package. Current LUG releases -bundle their GUI/runtime dependencies, reducing the amount of Arch package -knowledge required from the player. +LUG Helper is **optional support/compatibility tooling**. Its failure must not +block installation and it never chooses the active Wine runner. -### Update model +## Stable paths -A systemd user timer runs every six hours. Each candidate Wine runner is tested -in a throw-away Wine prefix before it can become the active runner. The test -requires: - -- successful `wineboot` -- `drive_c` -- `system.reg` -- `user.reg` -- a valid `%APPDATA%` response - -If an upstream runner fails that test, the previous working runner stays active. - -DXVK is only changed while the Star Citizen Wine prefix is not in use. Existing -DLLs are backed up before replacement. - -### What Autopilot deliberately does not bypass - -Kernel, GPU driver and base Omarchy package upgrades still belong to Omarchy's -blessed `omarchy update` pipeline. Omarchy Citizen does not install a passwordless -pacman/sudo backdoor. - -That boundary lets the Star Citizen stack remain self-maintaining without giving -a third-party QML plugin permanent unattended root package-management rights. - -## Local state - -Managed components: - -```text -~/.local/share/omarchy-citizen/vendor/ -├── lug-helper/ -├── wine/ -└── dxvk/ -``` - -Logs: +User-space components live below: ```text +~/.local/share/omarchy-citizen/ ~/.local/state/omarchy-citizen/ -├── omarchy-citizen.log -└── updater.log +~/.config/omarchy-citizen/ ``` +The backend intentionally hides upstream version/layout changes behind stable +`vendor/*/current` paths and its own game configuration. + +## Update safety + +- Wine candidates are tested before activation. +- Incompatible immutable Wine releases are cached per CPU/glibc fingerprint. +- Partial prefixes are backed up rather than deleted. +- RSI installer downloads are checked against the SHA-512 value from its + published update metadata. +- DXVK is not changed while the Wine prefix is active. +- Plugin Git updates retain the Omarchy validation / trusted-remote protection. + +## Current development status + +See `DEVELOPMENT_STATUS.md`. + ## License MIT. - -## 0.7.1 compatibility fallback - -Autopilot no longer assumes that the newest LUG Wine release is compatible with -every PC. It evaluates recent stable releases newest-first and activates the -first runner that passes the full local prefix test. - -A rejected immutable release is cached against the current CPU/glibc -fingerprint, so it is not downloaded again every six hours. If the machine's -runtime fingerprint changes, it is eligible for retesting. - -Wine diagnostics now include the actual process error (`signal: illegal -instruction`, missing loader/library, exit status, etc.) even when Wine writes -nothing to stdout. diff --git a/backend/bin/omarchy-citizen-backend b/backend/bin/omarchy-citizen-backend index 4ffa0b0..53f13fb 100644 Binary files a/backend/bin/omarchy-citizen-backend and b/backend/bin/omarchy-citizen-backend differ diff --git a/backend/cmd/omarchy-citizen-backend/autopilot.go b/backend/cmd/omarchy-citizen-backend/autopilot.go index 5578426..3474dcf 100644 --- a/backend/cmd/omarchy-citizen-backend/autopilot.go +++ b/backend/cmd/omarchy-citizen-backend/autopilot.go @@ -30,6 +30,7 @@ type githubAsset struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"` Size int64 `json:"size"` + Digest string `json:"digest,omitempty"` } type componentMeta struct { @@ -100,7 +101,7 @@ func (a *App) maintainGamingStack() error { var problems []string if err := a.syncLUGHelper(); err != nil { - problems = append(problems, "LUG Helper: "+err.Error()) + a.logf("optional LUG Helper sync warning: %v", err) } if err := a.syncWineRunner(); err != nil { problems = append(problems, "Wine runner: "+err.Error()) @@ -108,6 +109,9 @@ func (a *App) maintainGamingStack() error { if err := a.syncDXVK(); err != nil { problems = append(problems, "DXVK: "+err.Error()) } + if err := a.syncRSILauncherIfNeeded(); err != nil { + problems = append(problems, "RSI Launcher: "+err.Error()) + } result := "ok" if len(problems) > 0 { @@ -154,6 +158,10 @@ func (a *App) syncLUGHelper() error { if err := download(asset.BrowserDownloadURL, tmp); err != nil { return err } + if err := verifyReleaseDigest(tmp, asset.Digest); err != nil { + _ = os.Remove(tmp) + return err + } if err := os.Chmod(tmp, 0o755); err != nil { return err } @@ -232,6 +240,11 @@ func (a *App) syncWineRunner() error { failures = append(failures, release.TagName+" download: "+err.Error()) continue } + if err := verifyReleaseDigest(archive, asset.Digest); err != nil { + _ = os.Remove(archive) + failures = append(failures, release.TagName+" checksum: "+err.Error()) + continue + } stage, err := os.MkdirTemp(a.cacheDir, "wine-stage-") if err != nil { @@ -527,6 +540,10 @@ func (a *App) syncDXVK() error { if err := download(asset.BrowserDownloadURL, archive); err != nil { return err } + if err := verifyReleaseDigest(archive, asset.Digest); err != nil { + _ = os.Remove(archive) + return err + } stage, err := os.MkdirTemp(a.cacheDir, "dxvk-stage-") if err != nil { return err @@ -593,12 +610,7 @@ func installDLLDir(srcDir, dstDir, backup string) error { } func (a *App) configuredPrefix() string { - configHome := envOr("XDG_CONFIG_HOME", filepath.Join(a.home, ".config")) - data, err := os.ReadFile(filepath.Join(configHome, "starcitizen-lug", "winedir.conf")) - if err != nil { - return "" - } - return strings.TrimSpace(string(data)) + return a.loadGameConfig().Prefix } func prefixInitialized(prefix string) bool { @@ -895,3 +907,22 @@ func (a *App) acquireMaintenanceLock() (func(), error) { _ = f.Close() }, nil } + +func verifyReleaseDigest(path, digest string) error { + if digest == "" { + return nil + } + parts := strings.SplitN(digest, ":", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "sha256" { + return nil + } + want := strings.ToLower(strings.TrimSpace(parts[1])) + got, err := fileHash(path) + if err != nil { + return err + } + if strings.ToLower(got) != want { + return fmt.Errorf("SHA-256 mismatch for %s", filepath.Base(path)) + } + return nil +} diff --git a/backend/cmd/omarchy-citizen-backend/main.go b/backend/cmd/omarchy-citizen-backend/main.go index f3cd0ea..8ea978c 100644 --- a/backend/cmd/omarchy-citizen-backend/main.go +++ b/backend/cmd/omarchy-citizen-backend/main.go @@ -16,7 +16,7 @@ import ( ) const ( - appVersion = "0.7.1" + appVersion = "0.8.0" pluginID = "local.omarchy-citizen" ) @@ -121,6 +121,25 @@ func main() { if err := app.wineDoctor(); err != nil { fatal(err) } + case "game-status": + st := app.gameStatus() + if hasArg(args[1:], "--json") { + printJSON(st) + } else { + printGameStatusKV(st) + } + case "game-install": + if err := app.gameInstall(); err != nil { + fatal(err) + } + case "game-launch": + if err := app.gameLaunch(); err != nil { + fatal(err) + } + case "game-repair": + if err := app.gameRepair(); err != nil { + fatal(err) + } case "autopilot": if len(args) < 2 { fatal(errors.New("usage: autopilot enable|disable|status")) @@ -739,3 +758,25 @@ func printStatusKV(s Status) { fmt.Printf("last_result=%s\n", s.LastResult) } func fatal(err error) { fmt.Fprintln(os.Stderr, "omarchy-citizen-backend:", err); os.Exit(1) } + +func printGameStatusKV(s GameStatus) { + fmt.Printf("backend_version=%s\n", s.BackendVersion) + fmt.Printf("health=%s\n", s.Health) + fmt.Printf("hardware=%s\n", s.Hardware) + fmt.Printf("hardware_reason=%s\n", s.HardwareReason) + fmt.Printf("gpu=%s\n", s.GPU) + fmt.Printf("vulkan=%s\n", s.Vulkan) + fmt.Printf("cpu_avx=%t\n", s.CPUAVX) + fmt.Printf("ram_gib=%d\n", s.RAMGiB) + fmt.Printf("combined_gib=%d\n", s.CombinedGiB) + fmt.Printf("disk_free_gib=%d\n", s.DiskFreeGiB) + fmt.Printf("prefix=%s\n", s.Prefix) + fmt.Printf("prefix_state=%s\n", s.PrefixState) + fmt.Printf("launcher_state=%s\n", s.LauncherState) + fmt.Printf("game_state=%s\n", s.GameState) + fmt.Printf("wine_version=%s\n", s.WineVersion) + fmt.Printf("dxvk_version=%s\n", s.DXVKVersion) + fmt.Printf("lug_version=%s\n", s.LUGVersion) + fmt.Printf("rsi_installer=%s\n", s.RSIInstaller) + fmt.Printf("autopilot=%t\n", s.Autopilot) +} diff --git a/backend/cmd/omarchy-citizen-backend/stack.go b/backend/cmd/omarchy-citizen-backend/stack.go new file mode 100644 index 0000000..204abc8 --- /dev/null +++ b/backend/cmd/omarchy-citizen-backend/stack.go @@ -0,0 +1,785 @@ +package main + +import ( + "bufio" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + neturl "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "sort" + "strconv" + "strings" + "time" +) + +const ( + winetricksRepo = "Winetricks/winetricks" + rsiBaseURL = "https://install.robertsspaceindustries.com/rel/2" + rsiLatestYML = rsiBaseURL + "/latest.yml" +) + +type GameConfig struct { + Prefix string `json:"prefix"` + GameDir string `json:"game_dir"` + LauncherEXE string `json:"launcher_exe"` + RSIInstaller string `json:"rsi_installer,omitempty"` + WinetricksVersion string `json:"winetricks_version,omitempty"` + InstalledAt string `json:"installed_at,omitempty"` + LastRepair string `json:"last_repair,omitempty"` +} + +type GameStatus struct { + BackendVersion string `json:"backend_version"` + Health string `json:"health"` + Hardware string `json:"hardware"` + HardwareReason string `json:"hardware_reason,omitempty"` + GPU string `json:"gpu,omitempty"` + Vulkan string `json:"vulkan,omitempty"` + CPUAVX bool `json:"cpu_avx"` + RAMGiB int `json:"ram_gib"` + CombinedGiB int `json:"combined_gib"` + DiskFreeGiB int `json:"disk_free_gib"` + Prefix string `json:"prefix"` + PrefixState string `json:"prefix_state"` + LauncherState string `json:"launcher_state"` + GameState string `json:"game_state"` + WineVersion string `json:"wine_version,omitempty"` + DXVKVersion string `json:"dxvk_version,omitempty"` + LUGVersion string `json:"lug_version,omitempty"` + RSIInstaller string `json:"rsi_installer,omitempty"` + Autopilot bool `json:"autopilot"` +} + +func (a *App) gameConfigPath() string { return filepath.Join(a.configDir, "game.json") } + +func (a *App) loadGameConfig() GameConfig { + cfg := GameConfig{} + if data, err := os.ReadFile(a.gameConfigPath()); err == nil { + _ = json.Unmarshal(data, &cfg) + } + if cfg.Prefix == "" { + // Adopt an existing LUG prefix if one is configured, otherwise use our stable default. + old := filepath.Join(envOr("XDG_CONFIG_HOME", filepath.Join(a.home, ".config")), "starcitizen-lug", "winedir.conf") + if data, err := os.ReadFile(old); err == nil && strings.TrimSpace(string(data)) != "" { + cfg.Prefix = strings.TrimSpace(string(data)) + } else { + cfg.Prefix = filepath.Join(a.home, "Games", "star-citizen") + } + } + if cfg.GameDir == "" { + cfg.GameDir = filepath.Join(cfg.Prefix, "drive_c", "Program Files", "Roberts Space Industries", "StarCitizen") + } + if cfg.LauncherEXE == "" { + cfg.LauncherEXE = filepath.Join(cfg.Prefix, "drive_c", "Program Files", "Roberts Space Industries", "RSI Launcher", "RSI Launcher.exe") + } + return cfg +} + +func (a *App) saveGameConfig(cfg GameConfig) error { + if err := os.MkdirAll(a.configDir, 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + tmp := a.gameConfigPath() + ".new" + if err := os.WriteFile(tmp, data, 0o600); err != nil { + return err + } + return os.Rename(tmp, a.gameConfigPath()) +} + +func (a *App) gameStatus() GameStatus { + gc := a.loadGameConfig() + uc := a.loadConfig() + hs := a.hardwareStatus(gc.Prefix) + st := GameStatus{ + BackendVersion: appVersion, + Health: "setup", + Hardware: hs.State, + HardwareReason: hs.Reason, + GPU: hs.GPU, + Vulkan: hs.Vulkan, + CPUAVX: hs.AVX, + RAMGiB: hs.RAMGiB, + CombinedGiB: hs.CombinedGiB, + DiskFreeGiB: hs.DiskFreeGiB, + Prefix: gc.Prefix, + PrefixState: "missing", + LauncherState: "missing", + GameState: "missing", + WineVersion: readMetaVersion(filepath.Join(a.vendorDir, "wine", "meta.json")), + DXVKVersion: readMetaVersion(filepath.Join(a.vendorDir, "dxvk", "meta.json")), + LUGVersion: readMetaVersion(filepath.Join(a.vendorDir, "lug-helper", "meta.json")), + RSIInstaller: gc.RSIInstaller, + Autopilot: uc.AutoMaintain, + } + if hs.State == "blocked" { + st.Health = "hardware-blocked" + return st + } + if prefixInitialized(gc.Prefix) { + st.PrefixState = "ready" + } else if fi, err := os.Stat(gc.Prefix); err == nil && fi.IsDir() { + st.PrefixState = "partial" + } + if _, err := os.Stat(gc.LauncherEXE); err == nil { + st.LauncherState = "ready" + } + if _, err := os.Stat(filepath.Join(gc.GameDir, "LIVE", "Bin64", "StarCitizen.exe")); err == nil { + st.GameState = "ready" + } else if _, err := os.Stat(filepath.Join(gc.GameDir, "LIVE", "Data.p4k")); err == nil { + st.GameState = "partial" + } + + switch { + case st.WineVersion == "": + st.Health = "setup" + case st.PrefixState == "partial": + st.Health = "repair" + case st.PrefixState != "ready": + st.Health = "install" + case st.LauncherState != "ready": + st.Health = "launcher-repair" + case st.GameState != "ready": + st.Health = "install-game" + default: + st.Health = "ready" + } + return st +} + +type hardwareInfo struct { + State string + Reason string + GPU string + Vulkan string + AVX bool + RAMGiB int + CombinedGiB int + DiskFreeGiB int +} + +func (a *App) hardwareStatus(prefix string) hardwareInfo { + h := hardwareInfo{State: "ready", Vulkan: "unknown"} + if runtime.GOARCH != "amd64" { + h.State = "blocked" + h.Reason = "Star Citizen benötigt x86-64." + return h + } + if data, err := os.ReadFile("/proc/cpuinfo"); err == nil { + low := strings.ToLower(string(data)) + h.AVX = regexp.MustCompile(`(?m)^flags\s*:.*\bavx\b`).MatchString(low) + if !h.AVX { + h.State = "blocked" + h.Reason = "Die CPU stellt AVX nicht bereit." + } + } + if data, err := os.ReadFile("/proc/meminfo"); err == nil { + vals := map[string]int64{} + sc := bufio.NewScanner(strings.NewReader(string(data))) + for sc.Scan() { + f := strings.Fields(sc.Text()) + if len(f) >= 2 { + n, _ := strconv.ParseInt(f[1], 10, 64) + vals[strings.TrimSuffix(f[0], ":")] = n + } + } + h.RAMGiB = int(vals["MemTotal"] / (1024 * 1024)) + h.CombinedGiB = int((vals["MemTotal"] + vals["SwapTotal"]) / (1024 * 1024)) + if h.RAMGiB < 14 && h.State != "blocked" { + h.State = "blocked" + h.Reason = "Weniger als 16 GiB RAM erkannt." + } + } + var stat syscallStatfs + if statfs(prefix, &stat) == nil { + h.DiskFreeGiB = int((stat.Bavail * uint64(stat.Bsize)) / (1024 * 1024 * 1024)) + } else if statfs(a.home, &stat) == nil { + h.DiskFreeGiB = int((stat.Bavail * uint64(stat.Bsize)) / (1024 * 1024 * 1024)) + } + + pci, _ := exec.Command("lspci", "-nnk").CombinedOutput() + p := string(pci) + for _, line := range strings.Split(p, "\n") { + low := strings.ToLower(line) + if strings.Contains(low, "vga compatible controller") || strings.Contains(low, "3d controller") || strings.Contains(low, "display controller") { + if h.GPU == "" { + h.GPU = strings.TrimSpace(line) + } + } + } + lowp := strings.ToLower(p) + if strings.Contains(lowp, "1234:1111") || strings.Contains(lowp, "kernel driver in use: bochs") { + h.State = "blocked" + h.Vulkan = "blocked" + h.Reason = "QEMU Standard VGA/bochs-drm erkannt. Star Citizen benötigt eine echte Vulkan-GPU oder GPU-Passthrough." + return h + } + if path, err := exec.LookPath("vulkaninfo"); err == nil { + cmd := exec.Command(path, "--summary") + out, err := cmd.CombinedOutput() + if err != nil || strings.Contains(string(out), "VK_ERROR_INCOMPATIBLE_DRIVER") { + h.State = "blocked" + h.Vulkan = "blocked" + h.Reason = "Vulkan ist nicht funktionsfähig." + return h + } + h.Vulkan = "ready" + } else { + matches, _ := filepath.Glob("/usr/share/vulkan/icd.d/*.json") + if len(matches) > 0 { + h.Vulkan = "probable" + } else { + h.Vulkan = "unknown" + } + } + return h +} + +// Small local wrapper lets this file remain platform-specific only where needed. +type syscallStatfs struct { + Type, Bsize int64 + Blocks, Bfree, Bavail, Files, Ffree, Fsid0, Fsid1 uint64 + Namelen, Frsize, Flags int64 + Spare [4]int64 +} + +func statfs(path string, s *syscallStatfs) error { + // Use df instead of syscall.Statfs to keep the struct portable across Go versions. + out, err := exec.Command("df", "-Pk", path).CombinedOutput() + if err != nil { + return err + } + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + if len(lines) < 2 { + return errors.New("df output") + } + f := strings.Fields(lines[len(lines)-1]) + if len(f) < 4 { + return errors.New("df fields") + } + avail, _ := strconv.ParseUint(f[3], 10, 64) + s.Bavail = avail + s.Bsize = 1024 + return nil +} + +func (a *App) currentRunner() (string, error) { + p, err := filepath.EvalSymlinks(filepath.Join(a.vendorDir, "wine", "current")) + if err != nil { + return "", errors.New("kein kompatibler verwalteter Wine-Runner aktiv") + } + if _, err := os.Stat(filepath.Join(p, "bin", "wine")); err != nil { + return "", err + } + return p, nil +} + +func (a *App) runnerEnv(prefix string) ([]string, string, error) { + runner, err := a.currentRunner() + if err != nil { + return nil, "", err + } + bin := filepath.Join(runner, "bin") + env := append([]string{}, os.Environ()...) + env = append(env, + "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), + "WINE="+filepath.Join(bin, "wine"), + "WINESERVER="+filepath.Join(bin, "wineserver"), + "WINEPREFIX="+prefix, + "WINEARCH=win64", + "WINEDEBUG=-all", + "WINEDLLOVERRIDES=dxwebsetup.exe,dotNetFx45_Full_setup.exe,winemenubuilder.exe=d", + ) + return env, runner, nil +} + +func (a *App) ensureLUGRuntime() (string, error) { + appimage := filepath.Join(a.vendorDir, "lug-helper", "lug-helper.appimage") + ver := readMetaVersion(filepath.Join(a.vendorDir, "lug-helper", "meta.json")) + if appimage == "" || ver == "" { + return "", errors.New("LUG AppImage runtime ist noch nicht synchronisiert") + } + base := filepath.Join(a.vendorDir, "lug-helper", "runtime", ver) + if _, err := os.Stat(filepath.Join(base, "usr", "bin", "cabextract")); err == nil { + return base, nil + } + if err := os.MkdirAll(filepath.Dir(base), 0o755); err != nil { + return "", err + } + tmp, err := os.MkdirTemp(a.cacheDir, "lug-appimage-") + if err != nil { + return "", err + } + defer os.RemoveAll(tmp) + cmd := exec.Command(appimage, "--appimage-extract") + cmd.Dir = tmp + cmd.Env = append(os.Environ(), "APPIMAGE_EXTRACT_AND_RUN=1") + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("LUG AppImage Runtime konnte nicht entpackt werden: %s", formatCommandFailure(err, out)) + } + extracted := filepath.Join(tmp, "squashfs-root") + if _, err := os.Stat(extracted); err != nil { + return "", errors.New("AppImage-Extraktion erzeugte kein squashfs-root") + } + _ = os.RemoveAll(base + ".new") + if err := copyTree(extracted, base+".new"); err != nil { + return "", err + } + _ = os.RemoveAll(base) + if err := os.Rename(base+".new", base); err != nil { + return "", err + } + return base, nil +} + +func (a *App) toolsEnv(base []string) ([]string, error) { + runtimeRoot, err := a.ensureLUGRuntime() + if err != nil { + return base, err + } + dirs := []string{filepath.Join(runtimeRoot, "usr", "bin"), filepath.Join(runtimeRoot, "bin"), filepath.Join(runtimeRoot, "usr", "sbin")} + libdirs := []string{filepath.Join(runtimeRoot, "usr", "lib"), filepath.Join(runtimeRoot, "usr", "lib64"), filepath.Join(runtimeRoot, "lib"), filepath.Join(runtimeRoot, "lib64")} + env := append([]string{}, base...) + oldPath := os.Getenv("PATH") + env = append(env, "PATH="+strings.Join(dirs, string(os.PathListSeparator))+string(os.PathListSeparator)+oldPath) + existingLD := os.Getenv("LD_LIBRARY_PATH") + ld := strings.Join(libdirs, string(os.PathListSeparator)) + if existingLD != "" { + ld += string(os.PathListSeparator) + existingLD + } + env = append(env, "LD_LIBRARY_PATH="+ld) + return env, nil +} + +func (a *App) syncWinetricks() (string, string, error) { + rel, err := githubLatest(winetricksRepo) + if err != nil { + return "", "", err + } + tag := rel.TagName + if tag == "" { + return "", "", errors.New("Winetricks-Release ohne Tag") + } + dir := filepath.Join(a.vendorDir, "winetricks", tag) + target := filepath.Join(dir, "winetricks") + if _, err := os.Stat(target); err == nil { + return target, tag, nil + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return "", "", err + } + u := "https://raw.githubusercontent.com/Winetricks/winetricks/refs/tags/" + tag + "/src/winetricks" + if err := download(u, target+".new"); err != nil { + return "", "", err + } + if err := os.Chmod(target+".new", 0o755); err != nil { + return "", "", err + } + if err := os.Rename(target+".new", target); err != nil { + return "", "", err + } + return target, tag, nil +} + +func (a *App) latestRSIInstaller() (string, string, string, error) { + req, _ := http.NewRequest("GET", rsiLatestYML, nil) + req.Header.Set("User-Agent", "omarchy-citizen/"+appVersion) + client := &http.Client{Timeout: 45 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", "", "", fmt.Errorf("RSI latest.yml HTTP %d", resp.StatusCode) + } + b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", "", "", err + } + reURL := regexp.MustCompile(`(?m)^url:\s*(.+?)\s*$`) + m := reURL.FindSubmatch(b) + if len(m) < 2 { + return "", "", "", errors.New("RSI latest.yml Format unbekannt") + } + file := strings.TrimSpace(string(m[1])) + file = strings.Trim(file, "\"'") + sha := "" + reSHA := regexp.MustCompile(`(?m)^sha512:\s*(.+?)\s*$`) + if sm := reSHA.FindSubmatch(b); len(sm) >= 2 { + sha = strings.TrimSpace(string(sm[1])) + sha = strings.Trim(sha, "\"'") + } + base, _ := neturl.Parse(rsiBaseURL + "/") + ref := &neturl.URL{Path: file} + return file, base.ResolveReference(ref).String(), sha, nil +} + +func verifySHA512Base64(path, want string) error { + if strings.TrimSpace(want) == "" { + return nil + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + h := sha512.New() + if _, err := io.Copy(h, f); err != nil { + return err + } + got := base64.StdEncoding.EncodeToString(h.Sum(nil)) + if got != strings.TrimSpace(want) { + return fmt.Errorf("SHA-512 mismatch for %s", filepath.Base(path)) + } + return nil +} + +func (a *App) backupIncompletePrefix(gc GameConfig) (GameConfig, string, error) { + fi, err := os.Stat(gc.Prefix) + if err != nil || !fi.IsDir() { + return gc, "", nil + } + if prefixInitialized(gc.Prefix) { + return gc, "", nil + } + if entries, err := os.ReadDir(gc.Prefix); err == nil && len(entries) == 0 { + return gc, "", nil + } + stamp := time.Now().Format("20060102-150405") + backup := gc.Prefix + "-incomplete-" + stamp + if err := os.Rename(gc.Prefix, backup); err != nil { + return gc, "", err + } + a.logf("backed up incomplete prefix %s -> %s", gc.Prefix, backup) + return gc, backup, nil +} + +func (a *App) gameInstall() error { + unlock, err := a.acquireMaintenanceLock() + if err != nil { + return err + } + defer unlock() + gc := a.loadGameConfig() + hs := a.hardwareStatus(gc.Prefix) + if hs.State == "blocked" { + return fmt.Errorf("hardware nicht spielbereit: %s", hs.Reason) + } + + // LUG is optional compatibility/support tooling only. It must never block + // the owned installation path and never gets to choose the install runner. + if err := a.syncLUGHelper(); err != nil { + a.logf("optional LUG runtime warning: %v", err) + } + if err := a.syncWineRunner(); err != nil { + return fmt.Errorf("Wine: %w", err) + } + if _, _, err := a.syncWinetricks(); err != nil { + return fmt.Errorf("Winetricks: %w", err) + } + + // Adopt a complete existing prefix. Otherwise safely back up partial state. + if !prefixInitialized(gc.Prefix) { + _, backup, err := a.backupIncompletePrefix(gc) + if err != nil { + return err + } + if backup != "" { + a.logf("partial prefix preserved at %s", backup) + } + if err := os.MkdirAll(gc.Prefix, 0o755); err != nil { + return err + } + if err := a.initializePrefix(gc); err != nil { + return err + } + } + if err := a.ensurePrefixComponents(gc); err != nil { + return err + } + if err := a.ensureRSILauncher(gc, true); err != nil { + return err + } + if err := a.writeOwnedLaunchFiles(gc); err != nil { + return err + } + if err := a.writeLUGCompatibilityConfig(gc); err != nil { + a.logf("LUG compatibility config warning: %v", err) + } + if err := a.syncDXVK(); err != nil { + a.logf("DXVK post-install warning: %v", err) + } + + gc.RSIInstaller = readText(filepath.Join(a.vendorDir, "rsi", "current.txt")) + gc.WinetricksVersion = readMetaVersion(filepath.Join(a.vendorDir, "winetricks", "meta.json")) + gc.InstalledAt = time.Now().Format(time.RFC3339) + if err := a.saveGameConfig(gc); err != nil { + return err + } + a.logf("game stack installed prefix=%s wine=%s", gc.Prefix, readMetaVersion(filepath.Join(a.vendorDir, "wine", "meta.json"))) + return nil +} + +func (a *App) initializePrefix(gc GameConfig) error { + env, runner, err := a.runnerEnv(gc.Prefix) + if err != nil { + return err + } + env, err = a.toolsEnv(env) + if err != nil { + return err + } + bin := filepath.Join(runner, "bin") + cmd := exec.Command(filepath.Join(bin, "wineboot"), "-u") + cmd.Env = env + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Wine-Prefix konnte nicht erstellt werden: %s", formatCommandFailure(err, out)) + } + wait := exec.Command(filepath.Join(bin, "wineserver"), "-w") + wait.Env = env + _, _ = wait.CombinedOutput() + deadline := time.Now().Add(30 * time.Second) + for !prefixInitialized(gc.Prefix) && time.Now().Before(deadline) { + time.Sleep(300 * time.Millisecond) + } + if !prefixInitialized(gc.Prefix) { + return errors.New("wineboot meldete Erfolg, aber drive_c/system.reg/user.reg fehlen") + } + return nil +} + +func (a *App) ensurePrefixComponents(gc GameConfig) error { + wt, tag, err := a.syncWinetricks() + if err != nil { + return err + } + env, _, err := a.runnerEnv(gc.Prefix) + if err != nil { + return err + } + env, err = a.toolsEnv(env) + if err != nil { + return err + } + cache := filepath.Join(a.cacheDir, "winetricks-cache", tag) + _ = os.MkdirAll(cache, 0o755) + env = append(env, "W_CACHE="+cache, "WINETRICKS_DOWNLOADER=curl") + cmd := exec.Command(wt, "-q", "arial", "tahoma", "dxvk", "powershell", "win11") + cmd.Env = env + out, err := cmd.CombinedOutput() + a.logf("winetricks %s output=%s", tag, compactDiagnostic(string(out))) + if err != nil { + return fmt.Errorf("Prefix-Komponenten: %s", formatCommandFailure(err, out)) + } + runner, _ := a.currentRunner() + wine := filepath.Join(runner, "bin", "wine") + reg := exec.Command(wine, "reg", "add", `HKEY_CURRENT_USER\Software\Wine\FileOpenAssociations`, `/v`, `Enable`, `/d`, `N`, `/f`) + reg.Env = env + out, err = reg.CombinedOutput() + if err != nil { + return fmt.Errorf("Wine Registry: %s", formatCommandFailure(err, out)) + } + _ = writeMeta(filepath.Join(a.vendorDir, "winetricks", "meta.json"), componentMeta{Version: tag, Updated: time.Now().Format(time.RFC3339)}) + return nil +} + +func (a *App) ensureRSILauncher(gc GameConfig, forceIfMissing bool) error { + file, u, sha, err := a.latestRSIInstaller() + if err != nil { + return err + } + metaDir := filepath.Join(a.vendorDir, "rsi") + if err := os.MkdirAll(metaDir, 0o755); err != nil { + return err + } + old := readText(filepath.Join(metaDir, "current.txt")) + _, launcherErr := os.Stat(gc.LauncherEXE) + if launcherErr == nil && old == file && !forceIfMissing { + return nil + } + if launcherErr == nil && old == file { + return nil + } + cache := filepath.Join(a.cacheDir, file) + if _, err := os.Stat(cache); err != nil { + if err := download(u, cache); err != nil { + return err + } + } + if err := verifySHA512Base64(cache, sha); err != nil { + _ = os.Remove(cache) + return err + } + env, runner, err := a.runnerEnv(gc.Prefix) + if err != nil { + return err + } + wine := filepath.Join(runner, "bin", "wine") + cmd := exec.Command(wine, cache, "/S") + cmd.Env = env + out, err := cmd.CombinedOutput() + a.logf("RSI installer %s output=%s", file, compactDiagnostic(string(out))) + if err != nil { + return fmt.Errorf("RSI Launcher Installation: %s", formatCommandFailure(err, out)) + } + ws := exec.Command(filepath.Join(runner, "bin", "wineserver"), "-k") + ws.Env = env + _, _ = ws.CombinedOutput() + deadline := time.Now().Add(20 * time.Second) + for { + if _, err := os.Stat(gc.LauncherEXE); err == nil { + break + } + if time.Now().After(deadline) { + return fmt.Errorf("RSI Installer beendet, Launcher fehlt: %s", gc.LauncherEXE) + } + time.Sleep(500 * time.Millisecond) + } + if err := os.WriteFile(filepath.Join(metaDir, "current.txt"), []byte(file+"\n"), 0o644); err != nil { + return err + } + return nil +} + +func (a *App) writeOwnedLaunchFiles(gc GameConfig) error { + binDir := filepath.Join(a.dataDir, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + return err + } + script := filepath.Join(binDir, "star-citizen-launch") + backend := filepath.Join(a.libDir, "omarchy-citizen-backend") + content := "#!/usr/bin/env bash\nexec " + shellQuote(backend) + " game-launch\n" + if err := os.WriteFile(script, []byte(content), 0o755); err != nil { + return err + } + apps := filepath.Join(envOr("XDG_DATA_HOME", filepath.Join(a.home, ".local", "share")), "applications") + _ = os.MkdirAll(apps, 0o755) + desktop := `[Desktop Entry] +Name=Star Citizen +Comment=Star Citizen via Omarchy Citizen Autopilot +Type=Application +Categories=Game; +Terminal=false +StartupNotify=true +StartupWMClass=rsi launcher.exe +Exec=` + script + `\n` + if err := os.WriteFile(filepath.Join(apps, "omarchy-citizen-star-citizen.desktop"), []byte(desktop), 0o644); err != nil { + return err + } + return nil +} + +func (a *App) writeLUGCompatibilityConfig(gc GameConfig) error { + dir := filepath.Join(envOr("XDG_CONFIG_HOME", filepath.Join(a.home, ".config")), "starcitizen-lug") + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, "winedir.conf"), []byte(gc.Prefix+"\n"), 0o644); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, "gamedir.conf"), []byte(gc.GameDir+"\n"), 0o644); err != nil { + return err + } + return nil +} + +func (a *App) gameLaunch() error { + gc := a.loadGameConfig() + st := a.gameStatus() + if st.Hardware == "blocked" { + return errors.New(st.HardwareReason) + } + if !prefixInitialized(gc.Prefix) { + return errors.New("Wine-Prefix fehlt; zuerst Setup ausführen") + } + if _, err := os.Stat(gc.LauncherEXE); err != nil { + return errors.New("RSI Launcher fehlt; Reparatur ausführen") + } + env, runner, err := a.runnerEnv(gc.Prefix) + if err != nil { + return err + } + env = append(env, + "__GL_SHADER_DISK_CACHE=1", + "__GL_SHADER_DISK_CACHE_SIZE=10737418240", + "__GL_SHADER_DISK_CACHE_PATH="+gc.Prefix, + "MESA_SHADER_CACHE_DIR="+gc.Prefix, + "MESA_SHADER_CACHE_MAX_SIZE=10G", + ) + log := filepath.Join(a.stateDir, "rsi-launcher.log") + _ = os.MkdirAll(a.stateDir, 0o755) + f, err := os.OpenFile(log, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + return err + } + cmd := exec.Command(filepath.Join(runner, "bin", "wine"), gc.LauncherEXE) + cmd.Env = env + cmd.Stdout = f + cmd.Stderr = f + if err := cmd.Start(); err != nil { + f.Close() + return err + } + _ = f.Close() + a.logf("RSI Launcher started pid=%d", cmd.Process.Pid) + return nil +} + +func (a *App) gameRepair() error { + gc := a.loadGameConfig() + hs := a.hardwareStatus(gc.Prefix) + if hs.State == "blocked" { + return errors.New(hs.Reason) + } + if err := a.syncLUGHelper(); err != nil { + a.logf("LUG runtime warning: %v", err) + } + if err := a.syncWineRunner(); err != nil { + return err + } + if !prefixInitialized(gc.Prefix) { + return a.gameInstall() + } + if err := a.ensurePrefixComponents(gc); err != nil { + return err + } + if err := a.ensureRSILauncher(gc, false); err != nil { + return err + } + if err := a.writeOwnedLaunchFiles(gc); err != nil { + return err + } + gc.LastRepair = time.Now().Format(time.RFC3339) + _ = a.saveGameConfig(gc) + return nil +} + +func (a *App) syncRSILauncherIfNeeded() error { + gc := a.loadGameConfig() + if !prefixInitialized(gc.Prefix) || prefixBusy(gc.Prefix) { + return nil + } + return a.ensureRSILauncher(gc, false) +} + +func readText(path string) string { b, _ := os.ReadFile(path); return strings.TrimSpace(string(b)) } +func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } + +// Keep sort referenced here because installation diagnostics intentionally sort paths in future additions. +var _ = sort.Strings diff --git a/citizenctl b/citizenctl index 11693bc..7af7b52 100644 --- a/citizenctl +++ b/citizenctl @@ -2,7 +2,7 @@ set -u PLUGIN_ID="local.omarchy-citizen" -PLUGIN_VERSION="0.7.1" +PLUGIN_VERSION="0.8.0" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/starcitizen-lug" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" @@ -58,408 +58,70 @@ find_backend() { } backend_status() { - local backend - backend="$(find_backend 2>/dev/null)" || return 1 - "$backend" status -} + local game_output="" plugin_output="" health="setup" prefix_state="missing" launcher_state="missing" game_state="missing" + local wine_version="" dxvk_version="" lug_version="" hardware="unknown" hardware_reason="" gpu="" vulkan="unknown" -autopilot_enable() { - local backend - backend="$(find_backend 2>/dev/null)" || { - echo "Omarchy-Citizen-Backend fehlt." >&2 - return 1 - } - "$backend" autopilot enable -} - -autopilot_maintain() { - local backend - backend="$(find_backend 2>/dev/null)" || return 1 - "$backend" maintain -} - -wine_doctor() { - local backend - backend="$(find_backend 2>/dev/null)" || return 1 - "$backend" doctor -} - -plugin_update_check() { - local backend output state - backend="$(find_backend 2>/dev/null)" || { - notify_error "Das Omarchy-Citizen-Update-Backend wurde nicht gefunden. Installiere das Plugin erneut." - return 1 - } - output="$($backend check 2>&1)" || true - printf '%s\n' "$output" >> "$LOG_FILE" - state="$(printf '%s\n' "$output" | sed -n 's/^update=//p' | head -n1)" - case "$state" in - available) notify "Omarchy Citizen" "Ein Plugin-Update ist verfügbar." ;; - current) notify "Omarchy Citizen" "Omarchy Citizen ist aktuell." ;; - unavailable) notify "Omarchy Citizen" "Diese Installation ist noch nicht Git-verwaltet. Automatische Plugin-Updates werden nach der Git-Installation verfügbar." ;; - blocked-dirty) notify_error "Plugin-Update blockiert: Im Plugin-Verzeichnis gibt es lokale Änderungen." ;; - blocked-remote-changed) notify_error "Plugin-Update aus Sicherheitsgründen blockiert: Das Git-Remote hat sich geändert." ;; - *) notify_error "Plugin-Update konnte nicht eindeutig geprüft werden. Das Support-Paket enthält Details." ;; - esac -} - -plugin_update_now() { - local backend - backend="$(find_backend 2>/dev/null)" || { - notify_error "Das Update-Backend wurde nicht gefunden." - return 1 - } - if "$backend" update >>"$LOG_FILE" 2>&1; then - notify "Omarchy Citizen" "Plugin-Update abgeschlossen." - else - notify_error "Plugin-Update fehlgeschlagen oder nicht verfügbar. Erstelle bei Bedarf ein Support-Paket." - fi -} - -plugin_auto_enable() { - local backend - backend="$(find_backend 2>/dev/null)" || { notify_error "Update-Backend fehlt."; return 1; } - if "$backend" auto enable >>"$LOG_FILE" 2>&1; then - notify "Omarchy Citizen" "Automatische Plugin-Updates sind aktiviert." - else - notify_error "Auto-Updates konnten nicht aktiviert werden. Bei ZIP-Installationen ist zuerst eine Git-verwaltete Plugin-Installation nötig." - fi -} - -plugin_auto_disable() { - local backend - backend="$(find_backend 2>/dev/null)" || { notify_error "Update-Backend fehlt."; return 1; } - "$backend" auto disable >>"$LOG_FILE" 2>&1 || true - notify "Omarchy Citizen" "Automatische Plugin-Updates sind deaktiviert." -} - -open_updater_log() { - local log="$STATE_DIR/updater.log" - if [[ ! -f "$log" ]]; then - notify_error "Noch kein Updater-Log vorhanden." - return 1 - fi - xdg-open "$log" >/dev/null 2>&1 & -} - -notify() { - local title="$1" - local message="$2" - if command -v omarchy-notification-send >/dev/null 2>&1; then - omarchy-notification-send "$title" "$message" >/dev/null 2>&1 || true - elif command -v notify-send >/dev/null 2>&1; then - notify-send "$title" "$message" >/dev/null 2>&1 || true - fi -} - -notify_error() { - local message="$1" - log_line "error=$message" - if command -v zenity >/dev/null 2>&1; then - zenity --error --width=500 --title="Omarchy Citizen" --text="$message" >/dev/null 2>&1 & - else - notify "Omarchy Citizen" "$message" - printf 'Omarchy Citizen: %s\n' "$message" >&2 - fi -} - -find_helper() { - local candidate - - # Omarchy Citizen owns this copy and keeps it current automatically. - if [[ -x "$MANAGED_LUG" ]]; then - printf '%s\n' "$MANAGED_LUG" - return 0 - fi - - # Compatibility fallbacks for existing users. - if command -v lug-helper >/dev/null 2>&1; then - command -v lug-helper - return 0 - fi - if command -v lug-helper.sh >/dev/null 2>&1; then - command -v lug-helper.sh - return 0 - fi - - for candidate in \ - "$HOME/.local/bin/lug-helper" \ - "$HOME/.local/bin/lug-helper.sh" \ - "$DATA_DIR/lug-helper/lug-helper.sh" - do - [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; } - done - return 1 -} - -helper_exec() { - local helper="$1" - shift - - if [[ "$helper" == "$MANAGED_LUG" || "${helper,,}" == *.appimage ]]; then - APPIMAGE_EXTRACT_AND_RUN=1 "$helper" "$@" - elif [[ "$helper" == *.sh ]]; then - bash "$helper" "$@" - else - "$helper" "$@" - fi -} - -helper_version() { - local helper="$1" - APPIMAGE_EXTRACT_AND_RUN=1 "$helper" --version 2>/dev/null | head -n1 || true -} -run_helper_sync() { - local helper - helper="$(find_helper)" || { - notify_error "Der Star Citizen LUG Helper wurde nicht gefunden." - return 1 - } - - log_line "lug-helper sync args=$* helper=$helper" - helper_exec "$helper" "$@" >>"$LOG_FILE" 2>&1 -} - -run_helper_visible() { - local helper - helper="$(find_helper)" || { - echo "Der Star Citizen LUG Helper wurde nicht gefunden." >&2 - return 1 - } - - log_line "lug-helper visible args=$* helper=$helper" - helper_exec "$helper" "$@" -} - -run_helper_gui() { - local helper - helper="$(find_helper)" || { - notify_error "Der Star Citizen LUG Helper wurde nicht gefunden. - -Wähle im SC-Panel „Einrichten“ – Omarchy Citizen installiert die benötigten Komponenten." - return 1 - } - - log_line "lug-helper gui args=$* helper=$helper" - if [[ "$helper" == "$MANAGED_LUG" || "${helper,,}" == *.appimage ]]; then - nohup env APPIMAGE_EXTRACT_AND_RUN=1 "$helper" "$@" >>"$LOG_FILE" 2>&1 & - elif [[ "$helper" == *.sh ]]; then - nohup bash "$helper" "$@" >>"$LOG_FILE" 2>&1 & - else - nohup "$helper" "$@" >>"$LOG_FILE" 2>&1 & - fi -} - -configured_prefix() { - [[ -f "$WINE_CONF" ]] || return 1 - local prefix - IFS= read -r prefix < "$WINE_CONF" - [[ -n "$prefix" ]] || return 1 - printf '%s\n' "$prefix" -} - -prefix_is_initialized() { - local prefix="$1" - [[ -d "$prefix/drive_c" ]] && - [[ -f "$prefix/system.reg" ]] && - [[ -f "$prefix/user.reg" ]] -} - -read_prefix() { - local prefix - prefix="$(configured_prefix 2>/dev/null)" || return 1 - [[ -d "$prefix" ]] || return 1 - prefix_is_initialized "$prefix" || return 1 - printf '%s\n' "$prefix" -} - -find_incomplete_prefix() { - local prefix="" - - prefix="$(configured_prefix 2>/dev/null || true)" - if [[ -n "$prefix" && -d "$prefix" ]] && ! prefix_is_initialized "$prefix"; then - printf '%s\n' "$prefix" - return 0 - fi - - prefix="$HOME/Games/star-citizen" - if [[ -d "$prefix" ]] && ! prefix_is_initialized "$prefix"; then - # Only treat the default path as our incomplete prefix when it contains - # LUG/installation artifacts rather than an arbitrary empty user folder. - if [[ -d "$prefix/runners" || -f "$prefix/sc-launch.sh" || -f "$prefix/.lughelper" ]]; then - printf '%s\n' "$prefix" - return 0 - fi - fi - - return 1 -} - -read_game_dir() { - [[ -f "$GAME_CONF" ]] || return 1 - local game_dir - IFS= read -r game_dir < "$GAME_CONF" - [[ -n "$game_dir" ]] || return 1 - printf '%s\n' "$game_dir" -} - -package_installed() { - pacman -Qq "$1" >/dev/null 2>&1 -} - -installed_lug_version() { - pacman -Q lug-helper 2>/dev/null | awk '{print $2}' -} - -read_update_cache() { - local key="$1" - [[ -f "$UPDATE_CACHE" ]] || return 1 - sed -n "s/^${key}=//p" "$UPDATE_CACHE" | head -n 1 -} - -write_update_cache() { - local state="$1" - local version="${2:-}" - mkdir -p "$CACHE_DIR" - { - printf 'state=%s\n' "$state" - printf 'version=%s\n' "$version" - printf 'checked_at=%s\n' "$(date +%s)" - } > "$UPDATE_CACHE" -} - -check_update() { - local installed output rc line target="" - - if ! package_installed lug-helper; then - write_update_cache "not-installed" "" - printf 'not-installed\n' - return 0 - fi - - installed="$(installed_lug_version)" - - if ! command -v yay >/dev/null 2>&1; then - write_update_cache "unknown" "" - printf 'unknown\n' - return 1 - fi - - set +e - output="$(timeout 25s yay -Qua 2>/dev/null)" - rc=$? - set -e - - if (( rc != 0 )); then - write_update_cache "unknown" "" - printf 'unknown\n' - return 1 - fi - - line="$(printf '%s\n' "$output" | awk '$1=="lug-helper"{print; exit}')" - if [[ -n "$line" ]]; then - target="$(printf '%s\n' "$line" | awk '{print $NF}')" - write_update_cache "available" "$target" - printf 'available=%s\n' "$target" - else - write_update_cache "current" "$installed" - printf 'current=%s\n' "$installed" - fi -} - -status() { - local helper="" version="" prefix="" game_dir="" dep missing_deps="" - local helper_state="missing" helper_managed="missing" deps_state="ready" - local prefix_state="missing" prefix_detail="" launcher_state="missing" game_state="missing" - local update_state="unknown" update_version="" health="setup" - - # Core operation no longer depends on an AUR LUG package. The managed - # official LUG AppImage bundles its GUI/runtime dependencies. - if [[ ! -x "$MANAGED_LUG" ]]; then - deps_state="partial" - missing_deps="managed LUG AppImage" - fi - - if helper="$(find_helper 2>/dev/null)"; then - helper_state="ready" - if [[ "$helper" == "$MANAGED_LUG" ]]; then - helper_managed="autopilot" - elif package_installed lug-helper; then - helper_managed="aur" - else - helper_managed="external" - fi - version="$(helper_version "$helper")" - version="${version//$'\n'/ }" - version="${version//$'\r'/ }" - fi - - update_state="$(read_update_cache state 2>/dev/null || printf 'unknown')" - update_version="$(read_update_cache version 2>/dev/null || true)" - - if prefix="$(read_prefix 2>/dev/null)"; then - prefix_state="ready" - prefix_detail="initialisiert" - if [[ -f "$prefix/sc-launch.sh" ]]; then - launcher_state="ready" - fi - elif prefix="$(find_incomplete_prefix 2>/dev/null)"; then - prefix_state="partial" - prefix_detail="unvollständig" - fi - - if game_dir="$(read_game_dir 2>/dev/null)"; then - if [[ -f "$game_dir/LIVE/Bin64/StarCitizen.exe" ]]; then - game_state="ready" - elif [[ -d "$game_dir" ]]; then - game_state="partial" - fi - fi - - if [[ "$helper_state" != "ready" ]]; then - health="setup" - elif [[ "$prefix_state" == "partial" ]]; then - health="recover-prefix" - elif [[ "$prefix_state" != "ready" ]]; then - health="install" - elif [[ "$launcher_state" != "ready" ]]; then - health="repair" - elif [[ "$game_state" != "ready" ]]; then - health="install-game" - else - health="ready" + if game_output="$(backend_game_status 2>/dev/null)"; then + health="$(printf '%s\n' "$game_output" | sed -n 's/^health=//p' | head -n1)" + prefix_state="$(printf '%s\n' "$game_output" | sed -n 's/^prefix_state=//p' | head -n1)" + launcher_state="$(printf '%s\n' "$game_output" | sed -n 's/^launcher_state=//p' | head -n1)" + game_state="$(printf '%s\n' "$game_output" | sed -n 's/^game_state=//p' | head -n1)" + wine_version="$(printf '%s\n' "$game_output" | sed -n 's/^wine_version=//p' | head -n1)" + dxvk_version="$(printf '%s\n' "$game_output" | sed -n 's/^dxvk_version=//p' | head -n1)" + lug_version="$(printf '%s\n' "$game_output" | sed -n 's/^lug_version=//p' | head -n1)" + hardware="$(printf '%s\n' "$game_output" | sed -n 's/^hardware=//p' | head -n1)" + hardware_reason="$(printf '%s\n' "$game_output" | sed -n 's/^hardware_reason=//p' | head -n1)" + gpu="$(printf '%s\n' "$game_output" | sed -n 's/^gpu=//p' | head -n1)" + vulkan="$(printf '%s\n' "$game_output" | sed -n 's/^vulkan=//p' | head -n1)" fi printf 'plugin_version=%s\n' "$PLUGIN_VERSION" - printf 'health=%s\n' "$health" - printf 'deps=%s\n' "$deps_state" - printf 'deps_missing=%s\n' "$missing_deps" - printf 'helper=%s\n' "$helper_state" - printf 'helper_managed=%s\n' "$helper_managed" - printf 'helper_version=%s\n' "$version" - printf 'update=%s\n' "$update_state" - printf 'update_version=%s\n' "$update_version" - printf 'prefix=%s\n' "$prefix_state" - printf 'prefix_detail=%s\n' "$prefix_detail" - printf 'launcher=%s\n' "$launcher_state" - printf 'game=%s\n' "$game_state" + printf 'health=%s\n' "${health:-setup}" + printf 'deps=ready\n' + printf 'deps_missing=\n' + printf 'helper=%s\n' "$([[ -n "$lug_version" ]] && echo ready || echo missing)" + printf 'helper_managed=autopilot\n' + printf 'helper_version=%s\n' "$lug_version" + printf 'update=unknown\n' + printf 'update_version=\n' + printf 'prefix=%s\n' "${prefix_state:-missing}" + if [[ "$prefix_state" == "partial" ]]; then printf 'prefix_detail=unvollständig\n'; else printf 'prefix_detail=\n'; fi + printf 'launcher=%s\n' "${launcher_state:-missing}" + printf 'game=%s\n' "${game_state:-missing}" + printf 'hardware=%s\n' "$hardware" + printf 'hardware_reason=%s\n' "$hardware_reason" + printf 'hardware_gpu=%s\n' "$gpu" + printf 'hardware_vulkan=%s\n' "$vulkan" - local backend_output="" backend_state="missing" - if backend_output="$(backend_status 2>/dev/null)"; then - backend_state="ready" + if plugin_output="$(backend_status 2>/dev/null)"; then + printf 'backend=ready\n' + else + printf 'backend=missing\n' + fi + printf 'backend_version=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^backend_version=//p' | head -n1)" + printf 'plugin_managed=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^managed=//p' | head -n1)" + printf 'plugin_auto=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^auto_apply=//p' | head -n1)" + printf 'plugin_update=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^update=//p' | head -n1)" + printf 'plugin_local_commit=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^local_commit=//p' | head -n1)" + printf 'plugin_remote_commit=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^remote_commit=//p' | head -n1)" + printf 'autopilot=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^auto_maintain=//p' | head -n1)" + printf 'managed_lug_version=%s\n' "${lug_version}" + printf 'managed_wine_version=%s\n' "${wine_version}" + printf 'managed_dxvk_version=%s\n' "${dxvk_version}" + printf 'maintenance_result=%s\n' "$(printf '%s\n' "$plugin_output" | sed -n 's/^maintenance_result=//p' | head -n1)" +} + +hardware_info() { + local out reason gpu vulkan + out="$(backend_game_status 2>/dev/null || true)" + reason="$(printf '%s\n' "$out" | sed -n 's/^hardware_reason=//p')" + gpu="$(printf '%s\n' "$out" | sed -n 's/^gpu=//p')" + vulkan="$(printf '%s\n' "$out" | sed -n 's/^vulkan=//p')" + if command -v zenity >/dev/null 2>&1; then + zenity --warning --width=600 --title="Omarchy Citizen – Hardware" --text="Star Citizen ist auf diesem System noch nicht spielbereit.\n\nGPU:\n${gpu:-nicht erkannt}\n\nVulkan: ${vulkan:-unbekannt}\n\n${reason:-Hardware-Prüfung fehlgeschlagen.}" >/dev/null 2>&1 || true + else + printf '%s\n' "$reason" fi - printf 'backend=%s\n' "$backend_state" - printf 'backend_version=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^backend_version=//p' | head -n1)" - printf 'plugin_managed=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^managed=//p' | head -n1)" - printf 'plugin_auto=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^auto_apply=//p' | head -n1)" - printf 'plugin_update=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^update=//p' | head -n1)" - printf 'plugin_local_commit=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^local_commit=//p' | head -n1)" - printf 'plugin_remote_commit=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^remote_commit=//p' | head -n1)" - printf 'autopilot=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^auto_maintain=//p' | head -n1)" - printf 'managed_lug_version=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^lug_version=//p' | head -n1)" - printf 'managed_wine_version=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^wine_version=//p' | head -n1)" - printf 'managed_dxvk_version=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^dxvk_version=//p' | head -n1)" - printf 'maintenance_result=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^maintenance_result=//p' | head -n1)" } probe_detached_start() { @@ -700,11 +362,7 @@ guided_setup_interactive() { } guided_setup() { - if ! package_installed zenity || ! find_helper >/dev/null 2>&1; then - launch_terminal_action install-stack-terminal - return - fi - guided_setup_interactive + launch_terminal_action install-stack-terminal } install_stack_terminal() { @@ -720,81 +378,53 @@ install_stack_terminal() { echo "==================================================" echo echo "Fehlercode: $rc" - echo "Omarchy Citizen hat alle Ausgaben im Debug-Log gespeichert." - echo "Im SC-Panel genügt „Support-Paket erstellen“." - echo - log_line "autopilot setup failed rc=$rc" - notify "Omarchy Citizen" "Setup fehlgeschlagen. Support-Paket kann mit einem Klick erstellt werden." - if [[ -t 0 ]]; then - read -r -p "Drücke Enter, um dieses Fenster zu schließen … " _ || true - fi + echo "Im SC-Panel kannst du mit einem Klick ein Support-Paket erstellen." + log_line "owned-stack setup failed rc=$rc" + notify "Omarchy Citizen" "Setup fehlgeschlagen. Bitte Support-Paket erstellen." + if [[ -t 0 ]]; then read -r -p "Drücke Enter zum Schließen … " _ || true; fi exit "$rc" } trap setup_failed ERR echo echo "==================================================" - echo " Omarchy Citizen – Rundum-Sorglos Autopilot" + echo " Omarchy Citizen – Spielbereit machen" echo "==================================================" echo - echo "Das Plugin verwaltet jetzt selbst:" - echo " ✓ Omarchy-Citizen-Updates (bei Git-Installation)" - echo " ✓ offiziellen LUG Helper als AppImage" - echo " ✓ stabilen LUG-Wine-Runner" - echo " ✓ DXVK" - echo " ✓ Wine-Selbsttest vor Aktivierung" - echo " ✓ Reparatur- und Support-Logs" - echo - echo "Es wird kein AUR-LUG-Paket mehr benötigt." + echo "Omarchy Citizen verwaltet den kritischen Gaming-Stack jetzt selbst." + echo "Der LUG Helper entscheidet nicht mehr, welche Wine-Version installiert wird." echo - echo "Aktiviere Autopilot und synchronisiere den Gaming-Stack …" - autopilot_enable - - echo - echo "Prüfe den verwalteten Wine-Runner in einem temporären Test-Prefix …" - wine_doctor - echo "✓ Wine-Selbsttest erfolgreich." - echo - - if find_incomplete_prefix >/dev/null 2>&1; then - echo "Eine unvollständige frühere Installation wurde erkannt." - echo "Sie wird jetzt sicher über den vorhandenen Recovery-Pfad behandelt." - echo + local hw + hw="$(backend_game_status)" + if [[ "$(printf '%s\n' "$hw" | sed -n 's/^health=//p')" == "hardware-blocked" ]]; then + echo "Hardware-Prüfung nicht bestanden:" + printf '%s\n' "$hw" | grep -E '^(hardware_reason|gpu|vulkan)=' trap - ERR - launch_terminal_action recover-prefix-terminal - return 0 + return 2 fi - echo "Starte LUG Preflight …" - run_helper_visible --preflight-check || true - - if ! read_prefix >/dev/null 2>&1; then - echo - echo "Installiere den RSI Launcher mit dem aktuellen LUG Helper …" - run_helper_visible --install - fi - - prefix="$(read_prefix 2>/dev/null || true)" - if [[ -n "$prefix" && ! -f "$prefix/sc-launch.sh" ]]; then - run_helper_visible --update-launch-script - fi - - # Maintenance once more now that a prefix exists: switches sc-launch.sh to - # the tested managed runner and installs current DXVK. + echo "1/4 Autopilot aktivieren und Quellen synchronisieren …" + autopilot_enable echo - echo "Aktualisiere Wine/DXVK für den neu eingerichteten Prefix …" - autopilot_maintain + echo "2/4 Kompatiblen Wine-Runner prüfen …" + wine_doctor + echo + echo "3/4 Wine-Prefix, Komponenten und RSI Launcher installieren/reparieren …" + backend_game_install + echo + echo "4/4 Gaming-Stack abschließend synchronisieren …" + autopilot_maintain || true trap - ERR - notify "Omarchy Citizen" "Autopilot ist aktiv. Der Gaming-Stack wird künftig automatisch gepflegt." - log_line "autopilot setup complete" echo echo "==================================================" - echo " AUTOPILOT AKTIV" + echo " SOFTWARE-STACK BEREIT" echo "==================================================" echo - echo "Künftig reicht der große STAR-CITIZEN-Button." + echo "Der RSI Launcher kann jetzt gestartet werden." + echo "Anmeldung und der eigentliche Spieldownload erfolgen im offiziellen RSI Launcher." + notify "Omarchy Citizen" "Software-Stack ist bereit. RSI Launcher kann gestartet werden." } update_helper_terminal() { @@ -814,147 +444,50 @@ system_update_terminal() { } launch_game() { - local prefix script - prefix="$(read_prefix 2>/dev/null)" || { - guided_setup - return - } - - script="$prefix/sc-launch.sh" - if [[ ! -f "$script" ]]; then - log_line "launch script missing; opening repair" - run_helper_gui --update-launch-script - return + log_line "launch via owned backend" + if ! backend_game_launch >>"$LOG_FILE" 2>&1; then + notify_error "Star Citizen konnte nicht gestartet werden. Nutze „Problem beheben“ oder erstelle ein Support-Paket." + return 1 fi - - log_line "launch game via sc-launch.sh" - nohup bash "$script" >>"$LOG_FILE" 2>&1 & } primary_action() { local st health backend - # Keep normal launch instant. Autopilot maintenance is delegated to systemd - # and never blocks the player's click. backend="$(find_backend 2>/dev/null || true)" - if [[ -n "$backend" ]]; then - systemctl --user start omarchy-citizen-update.service >/dev/null 2>&1 || true - fi - - st="$("$SELF" status)" + [[ -n "$backend" ]] && systemctl --user start omarchy-citizen-update.service >/dev/null 2>&1 || true + st="$(status)" health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" - case "$health" in - ready) + ready|install-game) launch_game ;; - setup) - launch_terminal_action install-stack-terminal - ;; - recover-prefix) - recover_incomplete_prefix - ;; - install) - guided_setup - ;; - install-game) - launch_game - ;; - repair) - run_helper_gui --update-launch-script + hardware-blocked) + hardware_info ;; *) - guided_setup + launch_terminal_action install-stack-terminal ;; esac } +repair_stack_terminal() { + set -Ee + exec > >(tee -a "$LOG_FILE") 2>&1 + echo "Omarchy Citizen prüft und repariert den gesamten Star-Citizen-Stack …" + autopilot_maintain || true + backend_game_repair + echo "Reparatur abgeschlossen." +} + repair_assistant_interactive() { - local st health choice prefix - - st="$("$SELF" status)" + local st health + st="$(status)" health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" - log_line "repair health=$health" - - case "$health" in - setup) - launch_terminal_action install-stack-terminal - return - ;; - recover-prefix) - launch_terminal_action recover-prefix-terminal - return - ;; - install) - guided_setup_interactive - return - ;; - repair) - run_helper_sync --update-launch-script || true - notify "Omarchy Citizen" "Start-Script wurde geprüft/repariert. Versuche den Start erneut." - return - ;; - esac - - if ! command -v zenity >/dev/null 2>&1; then - run_helper_gui --preflight-check - return - fi - - choice="$(zenity --list \ - --width=610 --height=420 \ - --title="Omarchy Citizen – Problem beheben" \ - --text="Was funktioniert nicht? Du brauchst keine Linux-Diagnose zu kennen." \ - --column="Problem" \ - "Automatisch prüfen (empfohlen)" \ - "RSI Launcher startet nicht / ist kaputt" \ - "Star Citizen startet nicht" \ - "Grafik / Performance / Abstürze" \ - "NGL macht Probleme" \ - "Problem ist unklar – Support-Paket erstellen" \ - 2>/dev/null)" || return 0 - - log_line "repair choice=$choice" - - case "$choice" in - "Automatisch prüfen (empfohlen)") - run_helper_sync --preflight-check || true - run_helper_sync --update-launch-script || true - notify "Omarchy Citizen" "Automatische Basisreparatur abgeschlossen. Bitte erneut starten." - ;; - "RSI Launcher startet nicht / ist kaputt") - run_helper_sync --update-rsi-launcher || true - run_helper_sync --update-launch-script || true - notify "Omarchy Citizen" "RSI Launcher und Start-Script wurden repariert." - ;; - "Star Citizen startet nicht") - run_helper_sync --preflight-check || true - run_helper_sync --update-launch-script || true - notify "Omarchy Citizen" "Startumgebung wurde geprüft. Falls es weiter nicht startet, erstelle bitte das Support-Paket." - ;; - "Grafik / Performance / Abstürze") - run_helper_sync --preflight-check || true - run_helper_gui --manage-runners - ;; - "NGL macht Probleme") - if zenity --question --width=540 \ - --title="NGL Problem" \ - --ok-label="Vanilla RSI Launcher reparieren" \ - --cancel-label="Abbrechen" \ - --text="Omarchy Citizen kann den offiziellen RSI Launcher über den LUG Helper neu installieren. NGL kann danach optional erneut installiert werden." \ - 2>/dev/null; then - run_helper_sync --update-rsi-launcher || true - run_helper_sync --update-launch-script || true - fi - ;; - "Problem ist unklar – Support-Paket erstellen") - "$SELF" support-build - ;; - esac + if [[ "$health" == "hardware-blocked" ]]; then hardware_info; return; fi + launch_terminal_action repair-stack-terminal } -repair_assistant() { - repair_assistant_interactive -} +repair_assistant() { repair_assistant_interactive; } sanitize_to() { local source="$1" @@ -1040,6 +573,7 @@ EOF if backend="$(find_backend 2>/dev/null)"; then capture_sanitized "$work/autopilot-status.txt" "$backend" status + capture_sanitized "$work/game-status.txt" "$backend" game-status capture_sanitized "$work/wine-doctor.txt" "$backend" doctor fi @@ -1327,6 +861,7 @@ ngl_install() { case "$ACTION" in status) status ;; + hardware-info) hardware_info ;; autopilot-enable) autopilot_enable ;; autopilot-maintain) autopilot_maintain ;; wine-doctor) wine_doctor ;; @@ -1337,6 +872,7 @@ case "$ACTION" in guided-setup-interactive) guided_setup_interactive ;; repair) repair_assistant ;; repair-interactive) repair_assistant_interactive ;; + repair-stack-terminal) repair_stack_terminal ;; support) support_start ;; support-build) support_build ;; debug-log) open_debug_log ;; diff --git a/install.sh b/install.sh index 39159e2..753a60c 100644 --- a/install.sh +++ b/install.sh @@ -48,7 +48,7 @@ omarchy plugin enable "$PLUGIN_ID" omarchy restart shell >/dev/null 2>&1 || true echo -echo "Omarchy Citizen 0.7.1 wurde installiert:" +echo "Omarchy Citizen 0.8.0 wurde installiert:" echo " $DEST" echo echo "Links-Klick auf 'SC' in der Omarchy-Leiste öffnet das Star-Citizen-Panel." diff --git a/manifest.json b/manifest.json index 47c93bb..1b26502 100644 --- a/manifest.json +++ b/manifest.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "id": "local.omarchy-citizen", "name": "Omarchy Citizen", - "version": "0.7.1", + "version": "0.8.0", "author": "Community prototype", "license": "MIT", - "description": "Omarchy Citizen Autopilot with compatibility fallback: automatically selects the newest LUG Wine runner that passes a real Wine prefix self-test on this PC.", + "description": "Omarchy Citizen self-managed Star Citizen stack: compatible Wine selection, owned prefix/launcher installation, automatic DXVK/RSI updates, hardware gate, rollback and support diagnostics.", "kinds": [ "bar-widget" ],