v0.8.0
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user