This commit is contained in:
2026-08-31 18:17:56 +02:00
parent 907f05a22b
commit d8a11bb2bf
11 changed files with 1107 additions and 690 deletions
+41
View File
@@ -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.
+2 -2
View File
@@ -1,4 +1,4 @@
# Omarchy Citizen 0.7.1 – Rundum-Sorglos # Omarchy Citizen 0.8.0 – Rundum-Sorglos
## Installation ## Installation
@@ -85,7 +85,7 @@ Das Paket enthält zusätzlich:
- Maintenance-Ergebnis - Maintenance-Ergebnis
- Updater-Log - Updater-Log
## Wine-Kompatibilitätsfallback (0.7.1) ## Wine-Kompatibilitätsfallback (0.8.0)
Autopilot verwendet nicht blind die höchste Versionsnummer. Autopilot verwendet nicht blind die höchste Versionsnummer.
+46 -30
View File
@@ -12,7 +12,7 @@ Panel {
property var anchorItem: null property var anchorItem: null
property var hostWidget: null property var hostWidget: null
property string pluginVersion: "0.7.1" property string pluginVersion: "0.8.0"
property string health: "checking" property string health: "checking"
property string depsState: "checking" property string depsState: "checking"
property string depsMissing: "" property string depsMissing: ""
@@ -39,6 +39,10 @@ Panel {
property string managedWineVersion: "" property string managedWineVersion: ""
property string managedDXVKVersion: "" property string managedDXVKVersion: ""
property string maintenanceResult: "" property string maintenanceResult: ""
property string hardwareState: "unknown"
property string hardwareReason: ""
property string hardwareGPU: ""
property string hardwareVulkan: "unknown"
property string actionStatus: "" property string actionStatus: ""
property string actionError: "" property string actionError: ""
property string actionKind: "" property string actionKind: ""
@@ -102,7 +106,7 @@ Panel {
values[lines[i].slice(0, p)] = lines[i].slice(p + 1) 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" health = values.health || "setup"
depsState = values.deps || "missing" depsState = values.deps || "missing"
depsMissing = values.deps_missing || "" depsMissing = values.deps_missing || ""
@@ -127,6 +131,10 @@ Panel {
managedWineVersion = values.managed_wine_version || "" managedWineVersion = values.managed_wine_version || ""
managedDXVKVersion = values.managed_dxvk_version || "" managedDXVKVersion = values.managed_dxvk_version || ""
maintenanceResult = values.maintenance_result || "" maintenanceResult = values.maintenance_result || ""
hardwareState = values.hardware || "unknown"
hardwareReason = values.hardware_reason || ""
hardwareGPU = values.hardware_gpu || ""
hardwareVulkan = values.hardware_vulkan || "unknown"
} }
function stateIcon(state) { function stateIcon(state) {
@@ -163,6 +171,7 @@ Panel {
} }
function healthColor() { function healthColor() {
if (health === "hardware-blocked") return danger
if (health === "ready") return success if (health === "ready") return success
if (health === "recover-prefix" || health === "repair") return warning if (health === "recover-prefix" || health === "repair") return warning
if (health === "checking") return accent if (health === "checking") return accent
@@ -170,6 +179,7 @@ Panel {
} }
function headline() { function headline() {
if (health === "hardware-blocked") return "HARDWARE NICHT SPIELBEREIT"
if (health === "ready" && root.autopilot === "true") return "FLIGHT READY · AUTOPILOT" if (health === "ready" && root.autopilot === "true") return "FLIGHT READY · AUTOPILOT"
if (health === "ready") return "FLIGHT READY" if (health === "ready") return "FLIGHT READY"
if (health === "recover-prefix") return "SETUP REPAIR" if (health === "recover-prefix") return "SETUP REPAIR"
@@ -179,23 +189,22 @@ Panel {
} }
function subline() { 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") if (health === "ready")
return "Alles Wesentliche wurde gefunden. Star Citizen ist einen Klick entfernt." return "Wine, DXVK und RSI Launcher sind geprüft. 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."
if (health === "install-game") if (health === "install-game")
return "Die Linux-Basis steht. Der RSI Launcher führt dich durch die Spielinstallation." return "Der Software-Stack ist bereit. Melde dich im offiziellen RSI Launcher an und starte dort den Spieldownload."
if (health === "install") if (health === "repair" || health === "launcher-repair")
return "Die Linux-Basis steht. Der grafische Star-Citizen-Assistent übernimmt den Rest." return "Omarchy Citizen kann den gesamten verwalteten Gaming-Stack automatisch reparieren."
return "Omarchy Citizen richtet Abhängigkeiten, LUG Helper und Star-Citizen-Basis geführt ein." return "Omarchy Citizen wählt selbst einen kompatiblen Wine-Runner, erstellt den Prefix, installiert DXVK/Komponenten und den offiziellen RSI Launcher."
} }
function primaryText() { function primaryText() {
if (health === "hardware-blocked") return "ⓘ HARDWARE-DETAILS"
if (health === "ready") return "▶ STAR CITIZEN STARTEN" if (health === "ready") return "▶ STAR CITIZEN STARTEN"
if (health === "recover-prefix") return "↻ SETUP SICHER REPARIEREN" if (health === "install-game") return "▶ RSI LAUNCHER ÖFFNEN"
if (health === "repair") return "↻ ERKANNTES PROBLEM BEHEBEN" if (health === "repair" || health === "launcher-repair") return "↻ AUTOMATISCH REPARIEREN"
return "▶ EINRICHTEN & STARTKLAR MACHEN" return "▶ EINRICHTEN & STARTKLAR MACHEN"
} }
@@ -259,10 +268,10 @@ Panel {
onExited: function(exitCode) { onExited: function(exitCode) {
if (exitCode === 0) { if (exitCode === 0) {
root.actionError = "" root.actionError = ""
root.actionStatus = root.actionKind === "primary" && root.health === "recover-prefix" root.actionStatus = root.actionKind === "primary" && root.health === "install-game"
? "Reparatur-Fenster wurde geöffnet. Dort läuft die sichere Wiederherstellung." ? "RSI Launcher wurde gestartet."
: (root.actionKind === "primary" && root.health !== "ready" : (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.") : "Aktion wurde gestartet.")
root.refreshStatus() root.refreshStatus()
@@ -391,7 +400,8 @@ Panel {
bordered: true bordered: true
active: root.health === "ready" || actionProc.running active: root.health === "ready" || actionProc.running
onClicked: root.runAction( 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" root.health === "ready"
) )
} }
@@ -580,6 +590,12 @@ Panel {
font.bold: true 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 { StatusLine {
label: "Abhängigkeiten" label: "Abhängigkeiten"
state: root.depsState state: root.depsState
@@ -587,11 +603,11 @@ Panel {
} }
StatusLine { StatusLine {
label: "LUG Helper" label: "Support-Helper"
state: root.helperState state: root.helperState
value: root.helperState === "ready" value: root.helperState === "ready"
? ((root.helperVersion !== "" ? root.helperVersion : "gefunden") + ? ((root.helperVersion !== "" ? root.helperVersion : "gefunden") +
(root.helperManaged === "aur" ? " · AUR" : " · extern")) " · optional")
: "nicht installiert" : "nicht installiert"
} }
@@ -604,9 +620,9 @@ Panel {
} }
StatusLine { StatusLine {
label: "Start-Script" label: "RSI Launcher"
state: root.launcherState state: root.launcherState
value: root.launcherState === "ready" ? "bereit" : "fehlt" value: root.launcherState === "ready" ? "installiert" : "fehlt"
} }
PanelSeparator { foreground: root.barForeground } PanelSeparator { foreground: root.barForeground }
@@ -722,11 +738,11 @@ Panel {
Button { Button {
width: setupGrid.cellWidth width: setupGrid.cellWidth
text: root.checkingUpdate ? "Update wird geprüft…" : "LUG Update prüfen" text: "Autopilot synchronisieren"
foreground: root.barForeground foreground: root.barForeground
fontFamily: root.uiFont fontFamily: root.uiFont
bordered: true bordered: true
onClicked: root.checkForUpdates() onClicked: root.runAction("autopilot-maintain")
} }
Button { Button {
@@ -749,7 +765,7 @@ Panel {
Button { Button {
width: setupGrid.cellWidth width: setupGrid.cellWidth
text: "LUG Helper öffnen" text: "Support-Helper öffnen"
foreground: root.barForeground foreground: root.barForeground
fontFamily: root.uiFont fontFamily: root.uiFont
bordered: true bordered: true
@@ -767,29 +783,29 @@ Panel {
Button { Button {
width: setupGrid.cellWidth width: setupGrid.cellWidth
text: "DXVK verwalten" text: "DXVK automatisch prüfen"
foreground: root.barForeground foreground: root.barForeground
fontFamily: root.uiFont fontFamily: root.uiFont
bordered: true bordered: true
onClicked: root.runAction("dxvk") onClicked: root.runAction("autopilot-maintain")
} }
Button { Button {
width: setupGrid.cellWidth width: setupGrid.cellWidth
text: "RSI reparieren" text: "Gaming-Stack reparieren"
foreground: root.barForeground foreground: root.barForeground
fontFamily: root.uiFont fontFamily: root.uiFont
bordered: true bordered: true
onClicked: root.runAction("repair-rsi") onClicked: root.runAction("repair")
} }
Button { Button {
width: setupGrid.cellWidth width: setupGrid.cellWidth
text: "Start-Script reparieren" text: "Launcher-Dateien reparieren"
foreground: root.barForeground foreground: root.barForeground
fontFamily: root.uiFont fontFamily: root.uiFont
bordered: true bordered: true
onClicked: root.runAction("repair-launch") onClicked: root.runAction("repair")
} }
Button { Button {
+36 -69
View File
@@ -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 ## Architecture
user maintains" to "self-maintaining Star Citizen appliance".
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) 1. hardware / Vulkan readiness gate
- official Star Citizen LUG Helper **AppImage** 2. select the newest locally compatible stable LUG Wine runner
- stable `lug-wine-tkg-git` Wine runner 3. validate it in a disposable Wine prefix
- DXVK 4. create the real Wine prefix
- Wine prefix health testing 5. dynamically fetch current Winetricks and install required verbs
- launch-script Wine runner selection 6. dynamically fetch and verify the current RSI Launcher
- repair / recovery 7. manage DXVK and the stable launch entry point
- support logs 8. periodically maintain the user-space gaming stack
The LUG Helper AppImage is preferred over the AUR package. Current LUG releases LUG Helper is **optional support/compatibility tooling**. Its failure must not
bundle their GUI/runtime dependencies, reducing the amount of Arch package block installation and it never chooses the active Wine runner.
knowledge required from the player.
### Update model ## Stable paths
A systemd user timer runs every six hours. Each candidate Wine runner is tested User-space components live below:
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:
```text ```text
~/.local/share/omarchy-citizen/
~/.local/state/omarchy-citizen/ ~/.local/state/omarchy-citizen/
├── omarchy-citizen.log ~/.config/omarchy-citizen/
└── updater.log
``` ```
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 ## License
MIT. 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.
Binary file not shown.
@@ -30,6 +30,7 @@ type githubAsset struct {
Name string `json:"name"` Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"` BrowserDownloadURL string `json:"browser_download_url"`
Size int64 `json:"size"` Size int64 `json:"size"`
Digest string `json:"digest,omitempty"`
} }
type componentMeta struct { type componentMeta struct {
@@ -100,7 +101,7 @@ func (a *App) maintainGamingStack() error {
var problems []string var problems []string
if err := a.syncLUGHelper(); err != nil { 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 { if err := a.syncWineRunner(); err != nil {
problems = append(problems, "Wine runner: "+err.Error()) problems = append(problems, "Wine runner: "+err.Error())
@@ -108,6 +109,9 @@ func (a *App) maintainGamingStack() error {
if err := a.syncDXVK(); err != nil { if err := a.syncDXVK(); err != nil {
problems = append(problems, "DXVK: "+err.Error()) problems = append(problems, "DXVK: "+err.Error())
} }
if err := a.syncRSILauncherIfNeeded(); err != nil {
problems = append(problems, "RSI Launcher: "+err.Error())
}
result := "ok" result := "ok"
if len(problems) > 0 { if len(problems) > 0 {
@@ -154,6 +158,10 @@ func (a *App) syncLUGHelper() error {
if err := download(asset.BrowserDownloadURL, tmp); err != nil { if err := download(asset.BrowserDownloadURL, tmp); err != nil {
return err return err
} }
if err := verifyReleaseDigest(tmp, asset.Digest); err != nil {
_ = os.Remove(tmp)
return err
}
if err := os.Chmod(tmp, 0o755); err != nil { if err := os.Chmod(tmp, 0o755); err != nil {
return err return err
} }
@@ -232,6 +240,11 @@ func (a *App) syncWineRunner() error {
failures = append(failures, release.TagName+" download: "+err.Error()) failures = append(failures, release.TagName+" download: "+err.Error())
continue 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-") stage, err := os.MkdirTemp(a.cacheDir, "wine-stage-")
if err != nil { if err != nil {
@@ -527,6 +540,10 @@ func (a *App) syncDXVK() error {
if err := download(asset.BrowserDownloadURL, archive); err != nil { if err := download(asset.BrowserDownloadURL, archive); err != nil {
return err return err
} }
if err := verifyReleaseDigest(archive, asset.Digest); err != nil {
_ = os.Remove(archive)
return err
}
stage, err := os.MkdirTemp(a.cacheDir, "dxvk-stage-") stage, err := os.MkdirTemp(a.cacheDir, "dxvk-stage-")
if err != nil { if err != nil {
return err return err
@@ -593,12 +610,7 @@ func installDLLDir(srcDir, dstDir, backup string) error {
} }
func (a *App) configuredPrefix() string { func (a *App) configuredPrefix() string {
configHome := envOr("XDG_CONFIG_HOME", filepath.Join(a.home, ".config")) return a.loadGameConfig().Prefix
data, err := os.ReadFile(filepath.Join(configHome, "starcitizen-lug", "winedir.conf"))
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
} }
func prefixInitialized(prefix string) bool { func prefixInitialized(prefix string) bool {
@@ -895,3 +907,22 @@ func (a *App) acquireMaintenanceLock() (func(), error) {
_ = f.Close() _ = f.Close()
}, nil }, 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
}
+42 -1
View File
@@ -16,7 +16,7 @@ import (
) )
const ( const (
appVersion = "0.7.1" appVersion = "0.8.0"
pluginID = "local.omarchy-citizen" pluginID = "local.omarchy-citizen"
) )
@@ -121,6 +121,25 @@ func main() {
if err := app.wineDoctor(); err != nil { if err := app.wineDoctor(); err != nil {
fatal(err) 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": case "autopilot":
if len(args) < 2 { if len(args) < 2 {
fatal(errors.New("usage: autopilot enable|disable|status")) fatal(errors.New("usage: autopilot enable|disable|status"))
@@ -739,3 +758,25 @@ func printStatusKV(s Status) {
fmt.Printf("last_result=%s\n", s.LastResult) fmt.Printf("last_result=%s\n", s.LastResult)
} }
func fatal(err error) { fmt.Fprintln(os.Stderr, "omarchy-citizen-backend:", err); os.Exit(1) } 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)
}
@@ -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
+114 -578
View File
@@ -2,7 +2,7 @@
set -u set -u
PLUGIN_ID="local.omarchy-citizen" PLUGIN_ID="local.omarchy-citizen"
PLUGIN_VERSION="0.7.1" PLUGIN_VERSION="0.8.0"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/starcitizen-lug" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/starcitizen-lug"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}"
@@ -58,408 +58,70 @@ find_backend() {
} }
backend_status() { backend_status() {
local backend local game_output="" plugin_output="" health="setup" prefix_state="missing" launcher_state="missing" game_state="missing"
backend="$(find_backend 2>/dev/null)" || return 1 local wine_version="" dxvk_version="" lug_version="" hardware="unknown" hardware_reason="" gpu="" vulkan="unknown"
"$backend" status
}
autopilot_enable() { if game_output="$(backend_game_status 2>/dev/null)"; then
local backend health="$(printf '%s\n' "$game_output" | sed -n 's/^health=//p' | head -n1)"
backend="$(find_backend 2>/dev/null)" || { prefix_state="$(printf '%s\n' "$game_output" | sed -n 's/^prefix_state=//p' | head -n1)"
echo "Omarchy-Citizen-Backend fehlt." >&2 launcher_state="$(printf '%s\n' "$game_output" | sed -n 's/^launcher_state=//p' | head -n1)"
return 1 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)"
"$backend" autopilot enable 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)"
autopilot_maintain() { hardware_reason="$(printf '%s\n' "$game_output" | sed -n 's/^hardware_reason=//p' | head -n1)"
local backend gpu="$(printf '%s\n' "$game_output" | sed -n 's/^gpu=//p' | head -n1)"
backend="$(find_backend 2>/dev/null)" || return 1 vulkan="$(printf '%s\n' "$game_output" | sed -n 's/^vulkan=//p' | head -n1)"
"$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"
fi fi
printf 'plugin_version=%s\n' "$PLUGIN_VERSION" printf 'plugin_version=%s\n' "$PLUGIN_VERSION"
printf 'health=%s\n' "$health" printf 'health=%s\n' "${health:-setup}"
printf 'deps=%s\n' "$deps_state" printf 'deps=ready\n'
printf 'deps_missing=%s\n' "$missing_deps" printf 'deps_missing=\n'
printf 'helper=%s\n' "$helper_state" printf 'helper=%s\n' "$([[ -n "$lug_version" ]] && echo ready || echo missing)"
printf 'helper_managed=%s\n' "$helper_managed" printf 'helper_managed=autopilot\n'
printf 'helper_version=%s\n' "$version" printf 'helper_version=%s\n' "$lug_version"
printf 'update=%s\n' "$update_state" printf 'update=unknown\n'
printf 'update_version=%s\n' "$update_version" printf 'update_version=\n'
printf 'prefix=%s\n' "$prefix_state" printf 'prefix=%s\n' "${prefix_state:-missing}"
printf 'prefix_detail=%s\n' "$prefix_detail" if [[ "$prefix_state" == "partial" ]]; then printf 'prefix_detail=unvollständig\n'; else printf 'prefix_detail=\n'; fi
printf 'launcher=%s\n' "$launcher_state" printf 'launcher=%s\n' "${launcher_state:-missing}"
printf 'game=%s\n' "$game_state" 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 plugin_output="$(backend_status 2>/dev/null)"; then
if backend_output="$(backend_status 2>/dev/null)"; then printf 'backend=ready\n'
backend_state="ready" 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="<b>Star Citizen ist auf diesem System noch nicht spielbereit.</b>\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 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() { probe_detached_start() {
@@ -700,11 +362,7 @@ guided_setup_interactive() {
} }
guided_setup() { guided_setup() {
if ! package_installed zenity || ! find_helper >/dev/null 2>&1; then launch_terminal_action install-stack-terminal
launch_terminal_action install-stack-terminal
return
fi
guided_setup_interactive
} }
install_stack_terminal() { install_stack_terminal() {
@@ -720,81 +378,53 @@ install_stack_terminal() {
echo "==================================================" echo "=================================================="
echo echo
echo "Fehlercode: $rc" echo "Fehlercode: $rc"
echo "Omarchy Citizen hat alle Ausgaben im Debug-Log gespeichert." echo "Im SC-Panel kannst du mit einem Klick ein Support-Paket erstellen."
echo "Im SC-Panel genügt „Support-Paket erstellen“." log_line "owned-stack setup failed rc=$rc"
echo notify "Omarchy Citizen" "Setup fehlgeschlagen. Bitte Support-Paket erstellen."
log_line "autopilot setup failed rc=$rc" if [[ -t 0 ]]; then read -r -p "Drücke Enter zum Schließen … " _ || true; fi
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
exit "$rc" exit "$rc"
} }
trap setup_failed ERR trap setup_failed ERR
echo echo
echo "==================================================" echo "=================================================="
echo " Omarchy Citizen – Rundum-Sorglos Autopilot" echo " Omarchy Citizen – Spielbereit machen"
echo "==================================================" echo "=================================================="
echo echo
echo "Das Plugin verwaltet jetzt selbst:" echo "Omarchy Citizen verwaltet den kritischen Gaming-Stack jetzt selbst."
echo " ✓ Omarchy-Citizen-Updates (bei Git-Installation)" echo "Der LUG Helper entscheidet nicht mehr, welche Wine-Version installiert wird."
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 echo
echo "Aktiviere Autopilot und synchronisiere den Gaming-Stack …" local hw
autopilot_enable hw="$(backend_game_status)"
if [[ "$(printf '%s\n' "$hw" | sed -n 's/^health=//p')" == "hardware-blocked" ]]; then
echo echo "Hardware-Prüfung nicht bestanden:"
echo "Prüfe den verwalteten Wine-Runner in einem temporären Test-Prefix …" printf '%s\n' "$hw" | grep -E '^(hardware_reason|gpu|vulkan)='
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
trap - ERR trap - ERR
launch_terminal_action recover-prefix-terminal return 2
return 0
fi fi
echo "Starte LUG Preflight …" echo "1/4 Autopilot aktivieren und Quellen synchronisieren …"
run_helper_visible --preflight-check || true autopilot_enable
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 echo
echo "Aktualisiere Wine/DXVK für den neu eingerichteten Prefix …" echo "2/4 Kompatiblen Wine-Runner prüfen …"
autopilot_maintain 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 trap - ERR
notify "Omarchy Citizen" "Autopilot ist aktiv. Der Gaming-Stack wird künftig automatisch gepflegt."
log_line "autopilot setup complete"
echo echo
echo "==================================================" echo "=================================================="
echo " AUTOPILOT AKTIV" echo " SOFTWARE-STACK BEREIT"
echo "==================================================" echo "=================================================="
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() { update_helper_terminal() {
@@ -814,147 +444,50 @@ system_update_terminal() {
} }
launch_game() { launch_game() {
local prefix script log_line "launch via owned backend"
prefix="$(read_prefix 2>/dev/null)" || { if ! backend_game_launch >>"$LOG_FILE" 2>&1; then
guided_setup notify_error "Star Citizen konnte nicht gestartet werden. Nutze „Problem beheben“ oder erstelle ein Support-Paket."
return return 1
}
script="$prefix/sc-launch.sh"
if [[ ! -f "$script" ]]; then
log_line "launch script missing; opening repair"
run_helper_gui --update-launch-script
return
fi fi
log_line "launch game via sc-launch.sh"
nohup bash "$script" >>"$LOG_FILE" 2>&1 &
} }
primary_action() { primary_action() {
local st health backend 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)" backend="$(find_backend 2>/dev/null || true)"
if [[ -n "$backend" ]]; then [[ -n "$backend" ]] && systemctl --user start omarchy-citizen-update.service >/dev/null 2>&1 || true
systemctl --user start omarchy-citizen-update.service >/dev/null 2>&1 || true st="$(status)"
fi
st="$("$SELF" status)"
health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)"
case "$health" in case "$health" in
ready) ready|install-game)
launch_game launch_game
;; ;;
setup) hardware-blocked)
launch_terminal_action install-stack-terminal hardware_info
;;
recover-prefix)
recover_incomplete_prefix
;;
install)
guided_setup
;;
install-game)
launch_game
;;
repair)
run_helper_gui --update-launch-script
;; ;;
*) *)
guided_setup launch_terminal_action install-stack-terminal
;; ;;
esac 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() { repair_assistant_interactive() {
local st health choice prefix local st health
st="$(status)"
st="$("$SELF" status)"
health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)"
log_line "repair health=$health" if [[ "$health" == "hardware-blocked" ]]; then hardware_info; return; fi
launch_terminal_action repair-stack-terminal
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
} }
repair_assistant() { repair_assistant() { repair_assistant_interactive; }
repair_assistant_interactive
}
sanitize_to() { sanitize_to() {
local source="$1" local source="$1"
@@ -1040,6 +573,7 @@ EOF
if backend="$(find_backend 2>/dev/null)"; then if backend="$(find_backend 2>/dev/null)"; then
capture_sanitized "$work/autopilot-status.txt" "$backend" status 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 capture_sanitized "$work/wine-doctor.txt" "$backend" doctor
fi fi
@@ -1327,6 +861,7 @@ ngl_install() {
case "$ACTION" in case "$ACTION" in
status) status ;; status) status ;;
hardware-info) hardware_info ;;
autopilot-enable) autopilot_enable ;; autopilot-enable) autopilot_enable ;;
autopilot-maintain) autopilot_maintain ;; autopilot-maintain) autopilot_maintain ;;
wine-doctor) wine_doctor ;; wine-doctor) wine_doctor ;;
@@ -1337,6 +872,7 @@ case "$ACTION" in
guided-setup-interactive) guided_setup_interactive ;; guided-setup-interactive) guided_setup_interactive ;;
repair) repair_assistant ;; repair) repair_assistant ;;
repair-interactive) repair_assistant_interactive ;; repair-interactive) repair_assistant_interactive ;;
repair-stack-terminal) repair_stack_terminal ;;
support) support_start ;; support) support_start ;;
support-build) support_build ;; support-build) support_build ;;
debug-log) open_debug_log ;; debug-log) open_debug_log ;;
+1 -1
View File
@@ -48,7 +48,7 @@ omarchy plugin enable "$PLUGIN_ID"
omarchy restart shell >/dev/null 2>&1 || true omarchy restart shell >/dev/null 2>&1 || true
echo echo
echo "Omarchy Citizen 0.7.1 wurde installiert:" echo "Omarchy Citizen 0.8.0 wurde installiert:"
echo " $DEST" echo " $DEST"
echo echo
echo "Links-Klick auf 'SC' in der Omarchy-Leiste öffnet das Star-Citizen-Panel." echo "Links-Klick auf 'SC' in der Omarchy-Leiste öffnet das Star-Citizen-Panel."
+2 -2
View File
@@ -2,10 +2,10 @@
"schemaVersion": 1, "schemaVersion": 1,
"id": "local.omarchy-citizen", "id": "local.omarchy-citizen",
"name": "Omarchy Citizen", "name": "Omarchy Citizen",
"version": "0.7.1", "version": "0.8.0",
"author": "Community prototype", "author": "Community prototype",
"license": "MIT", "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": [ "kinds": [
"bar-widget" "bar-widget"
], ],