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
-
+