diff --git a/README.md b/README.md index 3362be6..f1f9028 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Citizen Launcher 1.0.0 +# Citizen Launcher 1.0.1 Citizen Launcher is a distro-neutral Star Citizen setup, launch, repair and maintenance application for Linux. Omarchy support is optional; the core is one static Go binary shared by Debian/Ubuntu, Fedora, Arch/Omarchy and other desktop distributions. @@ -30,7 +30,7 @@ The launcher owns the fragile user-space gaming stack so users do not have to pi Install the release `.deb`: ```bash -sudo apt install ./citizen-launcher_1.0.0_amd64.deb +sudo apt install ./citizen-launcher_1.0.1_amd64.deb ``` The package adds the desktop application and enables the system package-update timer. Future Citizen Launcher `.deb` releases can be installed automatically after release-asset digest and package metadata verification. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0c21542..7ca8b7a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,42 +1,31 @@ -# Citizen Launcher 1.0.0 +# Citizen Launcher 1.0.1 -First productized distro-neutral release. +Reliability hotfix based on the first real-machine 1.0.0 acceptance bundle. -## Setup and gameplay +## Fixed -- Replaces the old LUG-controlled installation path with one Citizen Launcher core. -- Chooses the newest stable LUG Wine release that actually passes a fresh-prefix self-test on the local CPU/glibc environment; incompatible releases are cached and skipped. -- Preserves a previous working Wine runner and never activates an untested candidate. -- Uses a deterministic SHA-256-pinned Winetricks 20260125 base (`arial`, `tahoma`, `win11` only). -- Avoids the unreliable Winetricks PowerShell MSI path. RSI PowerShell compatibility is provided by verified portable PowerShell Core 7.4.19 and the RSI-compatible Wine wrapper 3.0.5. -- Manages DXVK itself, including DLL verification and Wine native overrides. -- Parses current Electron Builder RSI `latest.yml` layouts and verifies the RSI installer SHA-512 before execution. -- Repairs prefix components, RSI compatibility, DXVK, RSI Launcher and desktop integration without deleting game data. +- Vulkan hardware detection now evaluates individual `vulkaninfo --summary` GPU blocks instead of treating the presence of any software ICD as a software-only system. +- A real AMD/NVIDIA/Intel Vulkan GPU is accepted even when Mesa also exposes `llvmpipe`/lavapipe/softpipe. +- Discrete GPUs are preferred for display/status, then integrated GPUs, then non-software virtual GPUs. +- QEMU/bochs remains blocked when it is the only usable graphics path, but no longer blocks a machine that also exposes a real working Vulkan GPU. +- Vulkan API compatibility is evaluated on the selected real GPU rather than the first arbitrary device in the summary. +- PowerShell compatibility self-test no longer requires stdout to be relayed through Wine. PowerShell Core is tested directly and the RSI-compatible wrapper is tested by its propagated exit status. +- Game/support status now discovers Prefix, DXVK, PowerShell, RSI Launcher and game files before applying an overall hardware/preflight block. A blocked machine therefore no longer reports existing components as falsely `missing`. -## Reliability hardening +## Regression coverage -- Fixes the historical literal `\\n` Star Citizen desktop-entry regression and automatically repairs old affected launchers. -- Fixes Wine registry path escaping; the file-association tweak is non-fatal. -- Fixes executable permissions when self-sync copies over an existing temporary file. -- Single GUI instance via file lock; a second launch reuses the existing localhost UI. -- One active GUI job at a time plus a cross-process gaming-stack lock. -- Detects a running RSI Launcher or Star Citizen and does not launch duplicates or kill an active game. -- Exact `/proc` `WINEPREFIX` matching avoids false busy/running detections for similarly named prefixes. -- Wine and DXVK release archives use safe extraction with traversal/link checks. -- Downloads are staged and atomically activated; logs rotate and maintenance is deferred while the prefix is busy. +- AMD RADV + llvmpipe must select AMD and report Vulkan ready. +- llvmpipe-only must remain rejected. +- a successful PowerShell wrapper with empty stdout must pass. +- blocked overall health must still expose real installed component states. +- existing 1.0 release-gate coverage remains active: race detector, stack/GUI locks, desktop-entry repair, safe archive extraction, self-update validation, package validation and integration tests. -## System and UX +## Upgrade -- Responsive standalone GUI with concise errors and expandable technical diagnostics. -- Preflight for Vulkan, AVX, RAM+swap, free storage, Linux filesystem, `vm.max_map_count` and open-file limits. -- Debian package installs the recommended system limits and uses Polkit for interactive preparation when needed. -- User-local 0.9.x binaries/desktop entries that shadow a newer Debian package are migrated safely. -- Generic uninstall removes launcher integration but preserves game/config data. +Debian/Ubuntu/Mint: -## Updates and maintenance +```bash +sudo apt install ./citizen-launcher_1.0.1_amd64.deb +``` -- Debian package installs a systemd timer that checks for a new Citizen Launcher release about every six hours. -- Automatic package update requires the expected GitHub release asset, a SHA-256 digest, matching `citizen-launcher` package name/version and `amd64` architecture before APT is invoked. -- Root updater ignores user-controlled release-repository environment variables and rejects writable/non-root repository configuration. -- Running GUIs are not killed during package replacement; they offer a controlled restart into the installed version. -- CI/release gate runs unit tests, vet, race detector, static build, integration test and package verification. +Existing configuration, Wine prefix, RSI Launcher and Star Citizen data are preserved. diff --git a/TEST_REPORT.md b/TEST_REPORT.md index f0ca1d2..daba497 100644 --- a/TEST_REPORT.md +++ b/TEST_REPORT.md @@ -1,10 +1,10 @@ -# Citizen Launcher 1.0.0 – Verification Report +# Citizen Launcher 1.0.1 – Verification Report Date: 2026-08-31 ## Release gate -The 1.0.0 source tree passed the complete automated release gate: +The 1.0.1 source tree passed the complete automated release gate: - shell syntax checks for installer, uninstaller, packaging and integration scripts - `gofmt` cleanliness @@ -15,41 +15,35 @@ The 1.0.0 source tree passed the complete automated release gate: - Omarchy updater integration test against disposable local Git repositories - Debian package build and metadata/payload verification - generic Linux amd64 tarball build and execution check -- process-level GUI single-instance test: a second GUI invocation reused the first localhost instance -- live GUI `/api/ping` check reporting version 1.0.0 -- systemd service/timer syntax verification with `systemd-analyze verify` -- desktop entry newline regression check (no literal `\\n` in Exec entries) -- AppStream XML parse check -- GitHub Actions CI/release YAML parse check +- systemd service/timer syntax verification +- desktop entry regression checks +- AppStream and workflow validation inherited from the 1.0 release gate -## Regression coverage added for 1.0.0 +## Real-machine regressions reproduced from the support bundle -The Go test suite explicitly covers: +The uploaded Debian 13 support bundle exposed two 1.0.0 false negatives: -- safe archive extraction and traversal/symlink rejection -- exact Wine-prefix process matching -- GUI and stack-operation file locks -- duplicate primary-action handler regression -- Star Citizen/RSI duplicate launch handling -- DXVK DLL/override state -- deterministic Winetricks checksum and minimal base verbs -- legacy user-local binary migration -- broken desktop entry repair from the 0.9.2 literal-newline bug -- copied executable permission preservation -- Debian update package name/version/architecture validation -- release asset selection/version comparison -- Electron-builder RSI `latest.yml` variants -- non-MSI portable PowerShell compatibility path -- Wine registry path escaping -- support-bundle sanitizer behavior -- required SHA-256 release digest fail-closed behavior +1. `vulkaninfo` reported both `AMD Radeon Graphics (RADV PHOENIX2)` and Mesa `llvmpipe`. 1.0.0 rejected the whole system merely because the software ICD was present. 1.0.1 parses devices independently and selects the real AMD GPU. +2. The RSI PowerShell wrapper returned a successful process exit but no captured stdout. 1.0.0 incorrectly required a marker string in stdout. 1.0.1 validates PowerShell Core and then treats the wrapper's propagated exit status as authoritative. + +Automated regression tests cover both conditions. + +## Additional process-level verification + +A real two-process GUI test was run against the built binary: + +- first `gui --no-open` created a localhost endpoint +- second `gui --no-open` returned the exact same endpoint instead of creating another backend +- `/api/ping` on that endpoint reported version `1.0.1` + +A synthetic `vulkaninfo` run matching the support bundle's AMD + llvmpipe layout selected `AMD Radeon Graphics (RADV PHOENIX2)` and reported Vulkan `ready`. The sandbox itself has insufficient RAM for Star Citizen, so its overall hardware state remained blocked for RAM as expected; the GPU path was no longer the blocker. ## Final binary / package hashes -- `backend/bin/citizen-launcher`: `5e55630dc47b89a11185384f09d79fb3813fce51b4d0e30a9660e6526c93a46d` -- `dist/citizen-launcher_1.0.0_amd64.deb`: `976a4cb3efc193f9f05e880ab0456d80e49cb7b5db0a61621d4dc4a788e7c8a1` -- `dist/citizen-launcher-1.0.0-linux-amd64.tar.gz`: `551bca5656382f79fd6e388d14f58555d0db66418a0388ab8a8ba2ac1a058e90` +- `backend/bin/citizen-launcher`: `7cd0d85046b2c9ac295c6ef5b86f93a3452bfb3cd94cff048e7511db581f3d07` +- `dist/citizen-launcher_1.0.1_amd64.deb`: `aba0944a0d2334f14f425e12a55ddd28d011449387e5be1ef3e0731e9d9a5119` +- `dist/citizen-launcher-1.0.1-linux-amd64.tar.gz`: `59edf81938c305b1d86d917d38222470b4f5e42c9c3dc350bf1ac430d8748bfc` ## Scope boundary -The release gate validates the launcher, package, updater, GUI, locks, migration, install/repair logic and local safety properties in the build environment. It cannot perform an RSI account login, download the full Star Citizen game, or execute a real game session with an external GPU from this sandbox. Those remain real-machine acceptance tests rather than simulated release-gate claims. +The release gate validates the launcher, package, updater, GUI, locks, migration, install/repair logic and local safety properties. It cannot perform an RSI account login or a complete live Star Citizen game session from this sandbox. Those remain real-machine acceptance tests. diff --git a/VERSION b/VERSION index 3eefcb9..7dea76e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/backend/bin/citizen-launcher b/backend/bin/citizen-launcher index 0edb3be..c719ffe 100644 Binary files a/backend/bin/citizen-launcher and b/backend/bin/citizen-launcher differ diff --git a/backend/cmd/citizen-launcher/main.go b/backend/cmd/citizen-launcher/main.go index 8c1a346..d69e8b7 100644 --- a/backend/cmd/citizen-launcher/main.go +++ b/backend/cmd/citizen-launcher/main.go @@ -16,7 +16,7 @@ import ( ) var ( - appVersion = "1.0.0" + appVersion = "1.0.1" releaseRepo = "sendnwv/omarchy-sc" ) diff --git a/backend/cmd/citizen-launcher/stack.go b/backend/cmd/citizen-launcher/stack.go index 252d3ee..fd7e4ae 100644 --- a/backend/cmd/citizen-launcher/stack.go +++ b/backend/cmd/citizen-launcher/stack.go @@ -147,20 +147,10 @@ func (a *App) gameStatus() GameStatus { Autopilot: uc.AutoMaintain, System: sys, } - if err := validatePrefixPath(gc.Prefix); err != nil { - st.Health = "hardware-blocked" - st.HardwareReason = err.Error() - return st - } - if hs.State == "blocked" { - st.Health = "hardware-blocked" - return st - } - if sys.State == "blocked" { - st.Health = "hardware-blocked" - st.HardwareReason = sys.Reason - return st - } + // Always inspect the existing stack before deciding the overall health. + // Support bundles must remain truthful even when hardware/preflight is + // blocked; otherwise a perfectly valid prefix/launcher appears as missing. + prefixPathErr := validatePrefixPath(gc.Prefix) if prefixInitialized(gc.Prefix) { st.PrefixState = "ready" } else if fi, err := os.Stat(gc.Prefix); err == nil && fi.IsDir() { @@ -179,6 +169,23 @@ func (a *App) gameStatus() GameStatus { st.GameState = "partial" } + // Health precedence is evaluated only after component discovery so diagnosis + // never loses useful state. + if prefixPathErr != nil { + st.Health = "hardware-blocked" + st.HardwareReason = prefixPathErr.Error() + return st + } + if hs.State == "blocked" { + st.Health = "hardware-blocked" + return st + } + if sys.State == "blocked" { + st.Health = "hardware-blocked" + st.HardwareReason = sys.Reason + return st + } + // Around 150 GiB of free space is the current LUG quick-start recommendation // for a fresh install. Do not block an already-installed game because free // space naturally drops after installation. @@ -222,6 +229,90 @@ type hardwareInfo struct { DiskFreeGiB int } +type vulkanDevice struct { + Name string + Type string + APIMajor int + APIMinor int +} + +func parseVulkanDevices(summary string) []vulkanDevice { + var devices []vulkanDevice + var current *vulkanDevice + flush := func() { + if current != nil && (current.Name != "" || current.Type != "" || current.APIMajor != 0) { + devices = append(devices, *current) + } + } + apiRE := regexp.MustCompile(`^([0-9]+)\.([0-9]+)`) + gpuRE := regexp.MustCompile(`^GPU[0-9]+:$`) + for _, raw := range strings.Split(summary, "\n") { + line := strings.TrimSpace(raw) + if gpuRE.MatchString(line) { + flush() + current = &vulkanDevice{} + continue + } + if current == nil { + continue + } + key, val, ok := strings.Cut(line, "=") + if !ok { + continue + } + key, val = strings.TrimSpace(key), strings.TrimSpace(val) + switch key { + case "deviceName": + current.Name = val + case "deviceType": + current.Type = val + case "apiVersion": + if m := apiRE.FindStringSubmatch(val); len(m) == 3 { + current.APIMajor, _ = strconv.Atoi(m[1]) + current.APIMinor, _ = strconv.Atoi(m[2]) + } + } + } + flush() + return devices +} + +func softwareVulkanDevice(d vulkanDevice) bool { + low := strings.ToLower(d.Name + " " + d.Type) + return strings.Contains(low, "physical_device_type_cpu") || + strings.Contains(low, "llvmpipe") || strings.Contains(low, "lavapipe") || strings.Contains(low, "softpipe") +} + +func vulkanDeviceRank(d vulkanDevice) int { + if softwareVulkanDevice(d) { + return -1 + } + switch strings.ToUpper(d.Type) { + case "PHYSICAL_DEVICE_TYPE_DISCRETE_GPU": + return 30 + case "PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU": + return 20 + case "PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU": + return 10 + default: + if d.Name != "" { + return 1 + } + } + return 0 +} + +func selectVulkanHardwareDevice(summary string) (vulkanDevice, bool) { + best, rank := vulkanDevice{}, -1 + for _, d := range parseVulkanDevices(summary) { + r := vulkanDeviceRank(d) + if r > rank { + best, rank = d, r + } + } + return best, rank >= 0 +} + func (a *App) hardwareStatus(prefix string) hardwareInfo { h := hardwareInfo{State: "ready", Vulkan: "unknown"} if runtime.GOARCH != "amd64" { @@ -267,8 +358,8 @@ func (a *App) hardwareStatus(prefix string) hardwareInfo { pci, _ := exec.Command("lspci", "-nnk").CombinedOutput() p := string(pci) if strings.TrimSpace(p) == "" { - // lspci/pciutils is optional on minimal Debian installations. Detect the - // QEMU VGA device through sysfs as a dependency-free fallback. + // lspci/pciutils is optional on minimal installations. Detect QEMU VGA + // through sysfs as a dependency-free fallback. vendors, _ := filepath.Glob("/sys/class/drm/card*/device/vendor") for _, vp := range vendors { v, _ := os.ReadFile(vp) @@ -287,45 +378,48 @@ func (a *App) hardwareStatus(prefix string) hardwareInfo { } } } - 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 - } + qemuFallback := strings.Contains(strings.ToLower(p), "1234:1111") || strings.Contains(strings.ToLower(p), "kernel driver in use: bochs") + if path, err := exec.LookPath("vulkaninfo"); err == nil { cmd := exec.Command(path, "--summary") out, err := cmd.CombinedOutput() text := string(out) - low := strings.ToLower(text) if err != nil || strings.Contains(text, "VK_ERROR_INCOMPATIBLE_DRIVER") { h.State = "blocked" h.Vulkan = "blocked" h.Reason = "Vulkan ist nicht funktionsfähig." return h } - if strings.Contains(low, "device_type_cpu") || strings.Contains(low, "llvmpipe") || strings.Contains(low, "lavapipe") || strings.Contains(low, "softpipe") { + dev, hasHardware := selectVulkanHardwareDevice(text) + if !hasHardware { h.State = "blocked" h.Vulkan = "blocked" - h.Reason = "Es wurde nur ein Software-Vulkan-Gerät erkannt. Star Citizen benötigt eine echte Vulkan-GPU." + if qemuFallback { + h.Reason = "QEMU Standard VGA/bochs-drm erkannt und keine echte Vulkan-GPU verfügbar. Für Star Citizen ist GPU-Passthrough erforderlich." + } else { + h.Reason = "Es wurde nur ein Software-Vulkan-Gerät erkannt. Star Citizen benötigt eine echte Vulkan-GPU." + } return h } - if m := regexp.MustCompile(`(?m)deviceName\s*=\s*(.+)$`).FindStringSubmatch(text); len(m) == 2 { - h.GPU = strings.TrimSpace(m[1]) + if dev.Name != "" { + h.GPU = dev.Name } - if m := regexp.MustCompile(`(?m)apiVersion\s*=\s*([0-9]+)\.([0-9]+)`).FindStringSubmatch(text); len(m) == 3 { - major, _ := strconv.Atoi(m[1]) - minor, _ := strconv.Atoi(m[2]) - if major < 1 || (major == 1 && minor < 3) { - h.State = "blocked" - h.Vulkan = "blocked" - h.Reason = fmt.Sprintf("Vulkan %d.%d erkannt; der aktuelle DXVK-Stack benötigt Vulkan 1.3 oder neuer.", major, minor) - return h - } + if dev.APIMajor < 1 || (dev.APIMajor == 1 && dev.APIMinor < 3) { + h.State = "blocked" + h.Vulkan = "blocked" + h.Reason = fmt.Sprintf("Vulkan %d.%d erkannt; der aktuelle DXVK-Stack benötigt Vulkan 1.3 oder neuer.", dev.APIMajor, dev.APIMinor) + return h } h.Vulkan = "ready" } else { + // Without vulkaninfo we can only make a provisional assessment. QEMU's + // bochs framebuffer is still a hard block because it is not a Vulkan GPU. + if qemuFallback { + 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 + } matches, _ := filepath.Glob("/usr/share/vulkan/icd.d/*.json") if len(matches) > 0 { h.Vulkan = "probable" @@ -886,17 +980,27 @@ func (a *App) writePowerShellMarker(prefix, version string) error { } func (a *App) probePowerShell(gc GameConfig, env []string) error { - _, _, wrapper64, _ := a.powerShellPaths(gc.Prefix) + core, _, wrapper64, _ := a.powerShellPaths(gc.Prefix) runner, err := a.currentRunner() if err != nil { return err } wine := filepath.Join(runner, "bin", "wine") - probe := exec.Command(wine, wrapper64, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Write-Output CITIZEN_POWERSHELL_OK") - probe.Env = env - out, err := probe.CombinedOutput() - if err != nil || !strings.Contains(string(out), "CITIZEN_POWERSHELL_OK") { - return fmt.Errorf("PowerShell Selbsttest: %s", formatCommandFailure(err, out)) + + // Validate PowerShell Core directly first. Then validate the compatibility + // wrapper using the invocation shape documented by the wrapper project. + // Wine does not reliably forward stdout from a Windows child spawned by + // another Windows executable, while wrapper v3+ deliberately propagates the + // child's exit status. A successful exit is therefore authoritative. + coreProbe := exec.Command(wine, core, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "exit 0") + coreProbe.Env = env + if out, err := coreProbe.CombinedOutput(); err != nil { + return fmt.Errorf("PowerShell Core Selbsttest: %s", formatCommandFailure(err, out)) + } + wrapperProbe := exec.Command(wine, wrapper64, "-NoLogo", "-NonInteractive", "exit 0") + wrapperProbe.Env = env + if out, err := wrapperProbe.CombinedOutput(); err != nil { + return fmt.Errorf("PowerShell Wrapper Selbsttest: %s", formatCommandFailure(err, out)) } wait := exec.Command(filepath.Join(runner, "bin", "wineserver"), "-w") wait.Env = env diff --git a/backend/cmd/citizen-launcher/stack_test.go b/backend/cmd/citizen-launcher/stack_test.go index 330486e..92307bf 100644 --- a/backend/cmd/citizen-launcher/stack_test.go +++ b/backend/cmd/citizen-launcher/stack_test.go @@ -179,3 +179,116 @@ func TestRequiredReleaseDigestFailsClosed(t *testing.T) { t.Fatalf("known SHA-256 rejected: %v", err) } } + +func TestVulkanHardwareSelectionIgnoresLLVMPipeWhenRealGPUExists(t *testing.T) { + summary := `Devices: +======== +GPU0: + apiVersion = 1.4.305 + deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU + deviceName = AMD Radeon Graphics (RADV PHOENIX2) +GPU1: + apiVersion = 1.4.305 + deviceType = PHYSICAL_DEVICE_TYPE_CPU + deviceName = llvmpipe (LLVM 19.1.7, 256 bits) +` + dev, ok := selectVulkanHardwareDevice(summary) + if !ok { + t.Fatal("real Vulkan GPU was rejected because a software ICD was also present") + } + if dev.Name != "AMD Radeon Graphics (RADV PHOENIX2)" { + t.Fatalf("selected GPU=%q", dev.Name) + } + if dev.APIMajor != 1 || dev.APIMinor != 4 { + t.Fatalf("selected Vulkan version=%d.%d", dev.APIMajor, dev.APIMinor) + } +} + +func TestVulkanHardwareSelectionRejectsSoftwareOnly(t *testing.T) { + summary := `Devices: +======== +GPU0: + apiVersion = 1.4.305 + deviceType = PHYSICAL_DEVICE_TYPE_CPU + deviceName = llvmpipe (LLVM 19.1.7, 256 bits) +` + if dev, ok := selectVulkanHardwareDevice(summary); ok { + t.Fatalf("software-only Vulkan device accepted: %#v", dev) + } +} + +func TestPowerShellProbeAcceptsSuccessfulWrapperWithoutStdout(t *testing.T) { + a := testApp(t) + prefix := filepath.Join(a.home, "Games", "star-citizen") + gc := GameConfig{Prefix: prefix} + core, _, wrapper64, _ := a.powerShellPaths(prefix) + for _, p := range []string{core, wrapper64} { + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, make([]byte, 2048), 0o644); err != nil { + t.Fatal(err) + } + } + runner := filepath.Join(a.vendorDir, "wine", "fixture") + bin := filepath.Join(runner, "bin") + if err := os.MkdirAll(bin, 0o755); err != nil { + t.Fatal(err) + } + // The fixture intentionally emits no stdout: this reproduces the real Wine + // behavior from the 1.0.0 support bundle. + for _, name := range []string{"wine", "wineserver"} { + p := filepath.Join(bin, name) + if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + } + if err := os.MkdirAll(filepath.Join(a.vendorDir, "wine"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(runner, filepath.Join(a.vendorDir, "wine", "current")); err != nil { + t.Fatal(err) + } + if err := a.probePowerShell(gc, os.Environ()); err != nil { + t.Fatalf("successful wrapper without stdout was rejected: %v", err) + } +} + +func TestGameStatusStillReportsInstalledComponentsWhenHealthIsBlocked(t *testing.T) { + a := testApp(t) + prefix := filepath.Join(a.home, "Games", "star citizen") // intentionally invalid path: forces blocked health + gc := GameConfig{ + Prefix: prefix, + LauncherEXE: filepath.Join(prefix, "drive_c", "Program Files", "Roberts Space Industries", "RSI Launcher", "RSI Launcher.exe"), + GameDir: filepath.Join(prefix, "drive_c", "Program Files", "Roberts Space Industries", "StarCitizen"), + } + for _, p := range []string{ + filepath.Join(prefix, "drive_c"), + filepath.Dir(gc.LauncherEXE), + filepath.Join(gc.GameDir, "LIVE", "Bin64"), + } { + if err := os.MkdirAll(p, 0o755); err != nil { + t.Fatal(err) + } + } + for _, p := range []string{ + filepath.Join(prefix, "system.reg"), + filepath.Join(prefix, "user.reg"), + gc.LauncherEXE, + filepath.Join(gc.GameDir, "LIVE", "Bin64", "StarCitizen.exe"), + } { + if err := os.WriteFile(p, []byte("fixture"), 0o644); err != nil { + t.Fatal(err) + } + } + if err := a.saveGameConfig(gc); err != nil { + t.Fatal(err) + } + st := a.gameStatus() + if st.Health != "hardware-blocked" { + t.Fatalf("health=%q want hardware-blocked", st.Health) + } + if st.PrefixState != "ready" || st.LauncherState != "ready" || st.GameState != "ready" { + t.Fatalf("blocked health hid real component state: prefix=%s launcher=%s game=%s", st.PrefixState, st.LauncherState, st.GameState) + } +} diff --git a/dist/citizen-launcher-1.0.1-linux-amd64.tar.gz b/dist/citizen-launcher-1.0.1-linux-amd64.tar.gz new file mode 100644 index 0000000..fd79d3c Binary files /dev/null and b/dist/citizen-launcher-1.0.1-linux-amd64.tar.gz differ diff --git a/dist/citizen-launcher_1.0.1_amd64.deb b/dist/citizen-launcher_1.0.1_amd64.deb new file mode 100644 index 0000000..9766218 Binary files /dev/null and b/dist/citizen-launcher_1.0.1_amd64.deb differ diff --git a/integrations/omarchy/Panel.qml b/integrations/omarchy/Panel.qml index d675556..c25f87e 100644 --- a/integrations/omarchy/Panel.qml +++ b/integrations/omarchy/Panel.qml @@ -12,7 +12,7 @@ Panel { property var anchorItem: null property var hostWidget: null - property string pluginVersion: "1.0.0" + property string pluginVersion: "1.0.1" property string health: "checking" property string depsState: "checking" property string depsMissing: "" @@ -106,7 +106,7 @@ Panel { values[lines[i].slice(0, p)] = lines[i].slice(p + 1) } - pluginVersion = values.plugin_version || "1.0.0" + pluginVersion = values.plugin_version || "1.0.1" health = values.health || "setup" depsState = values.deps || "missing" depsMissing = values.deps_missing || "" diff --git a/integrations/omarchy/citizenctl b/integrations/omarchy/citizenctl index e2d803e..ce9a5f6 100644 --- a/integrations/omarchy/citizenctl +++ b/integrations/omarchy/citizenctl @@ -2,7 +2,7 @@ set -u PLUGIN_ID="local.omarchy-citizen" -PLUGIN_VERSION="1.0.0" +PLUGIN_VERSION="1.0.1" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" diff --git a/integrations/omarchy/manifest.json b/integrations/omarchy/manifest.json index d4720e3..ded468f 100644 --- a/integrations/omarchy/manifest.json +++ b/integrations/omarchy/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "local.omarchy-citizen", "name": "Citizen Launcher · Omarchy", - "version": "1.0.0", + "version": "1.0.1", "author": "Community prototype", "license": "MIT", "description": "Optional Omarchy bar integration for the distro-neutral Citizen Launcher.", diff --git a/packaging/citizen-launcher.spec b/packaging/citizen-launcher.spec index 289f2be..c5461e9 100644 --- a/packaging/citizen-launcher.spec +++ b/packaging/citizen-launcher.spec @@ -1,5 +1,5 @@ Name: citizen-launcher -Version: 1.0.0 +Version: 1.0.1 Release: 1%{?dist} Summary: Star Citizen launcher and self-maintaining Wine stack for Linux License: MIT diff --git a/packaging/metainfo/io.github.citizenlauncher.CitizenLauncher.metainfo.xml b/packaging/metainfo/io.github.citizenlauncher.CitizenLauncher.metainfo.xml index 8235af1..387a183 100644 --- a/packaging/metainfo/io.github.citizenlauncher.CitizenLauncher.metainfo.xml +++ b/packaging/metainfo/io.github.citizenlauncher.CitizenLauncher.metainfo.xml @@ -13,6 +13,6 @@ https://github.com/sendnwv/omarchy-sc Game citizen-launcher - +