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
@@ -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.
+46 -30
View File
@@ -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 {
+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
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.
Binary file not shown.
@@ -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
}
+42 -1
View File
@@ -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)
}
@@ -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
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="<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
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 ;;
+1 -1
View File
@@ -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."
+2 -2
View File
@@ -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"
],