Skip to content

Claude/gamenative comprehensive review egflnd - #1694

Closed
Hfses wants to merge 82 commits into
utkarshdalal:masterfrom
Hfses:claude/gamenative-comprehensive-review-egflnd
Closed

Claude/gamenative comprehensive review egflnd#1694
Hfses wants to merge 82 commits into
utkarshdalal:masterfrom
Hfses:claude/gamenative-comprehensive-review-egflnd

Conversation

@Hfses

@Hfses Hfses commented Jul 10, 2026

Copy link
Copy Markdown

Description

Recording

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Ships a unified Game Hub (source-agnostic library + stores) and LAN rooms with in‑game chat, alongside a bounded session log and broad performance, stability, and security fixes. Also adds a CI workflow that builds and publishes debug APKs.

  • New Features

    • Game Hub (app.gamenative.gamehub): universal StoreProvider + StoreManager, adapters for Steam/GOG/Epic/Amazon/custom, unified library and Stores tab with search/sort, favorites, last‑played, and launch; Hilt module + persistent repo.
    • Custom stores: config-driven store form and persistence; manage in the Stores tab.
    • LAN: create/join rooms with UDP discovery, invite links, chat overlay in‑game, and quick‑menu toggle; holds multicast and wakelocks during sessions; new permissions in Manifest.
    • Session logging + diagnostics: rotating on‑device log with share/export; friendly error hints surfaced in‑game.
    • Local telemetry: per‑game FPS session history and one‑line suggestions on launch.
    • UI/graphics: Low Graphics Mode (FSR upscaling), animated video backgrounds (login and library), Controllers hub, Layout menu entry, rotation now follows device.
    • Runtime compatibility: pre‑launch auto‑fix for Wine/Box64 libc/version mismatches; adds The Last of Us Part I fix (Steam 1888930 and sideloaded).
    • CI: new .github/workflows/build-apk.yml builds and publishes debug APKs; release workflow gated to upstream repo.
  • Bug Fixes

    • Native/X server safety: guard PutImage payloads, clamp copy regions, use memmove for overlaps, make epoll event buffer thread‑local; drop duplicate submits.
    • Concurrency/visibility: event bus uses concurrent collections; SteamService flags @Volatile; atomic download counters and listener lists; fix duplicate keys and error isolation in StoreManager.
    • Security: strict path‑traversal checks in Amazon SDK/download and GOG/Epic installers; unique temp names for concurrent downloads.
    • Stability: fix infinite parent walk (ANR), progress NaN, leaked jobs/locks, LAN races and socket handling; robust file moves/copies with cleanup.
    • Defaults/perf: smarter DXVK defaults (async cache on, compiler threads cap, VRAM limits), Box64 preset default to INTERMEDIATE, Wine tweaks (fsync, disable winemenubuilder/crash dialog), thermal governor tied to FPS cap, versioned component extraction, present‑mode/env guards.

Written for commit 65d477f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added Game Hub with unified libraries, store browsing, search, filtering, favorites, sorting, and custom store support.
    • Added LAN rooms, multiplayer support for up to two controllers, and in-game LAN chat.
    • Added animated login/library backgrounds, custom cover management, low-graphics mode, controller management, and “What’s New”.
    • Added Wine/Proton installation from URLs and bulk component downloads.
  • Bug Fixes
    • Improved download reliability, file safety, rendering stability, orientation handling, and compatibility checks.
    • Added clearer installation error messages and session diagnostics.
  • Documentation
    • Added engineering, review, architecture, and integration documentation.

claude added 30 commits July 6, 2026 15:56
…CI debug build

- Add game fix for The Last of Us Part I (Steam 1888930): cap Wine reported
  memory (WINEVMEMMAXSIZE) to avoid the engine's virtual-address probe HALT,
  plus hardened Box64 dynarec settings (BIGBLOCK=0, STRONGMEM=2, SAFEFLAGS=2)
- Enable sustained performance mode during game sessions to reduce thermal
  clock-down on long play sessions
- Acquire a WifiManager multicast lock during game sessions so LAN game
  discovery (UDP broadcast) works, e.g. CS 1.6 and NFS MW 2005 server browsers
- Add build-apk.yml workflow: secret-free debug build (legacy + modern
  flavors) with APK artifacts on push, since existing workflows are gated to
  the upstream owner

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
The native evshim bridge already supports up to 4 virtual Xbox 360 pads
inside Wine, but the Java side was capped at MAX_PLAYERS = 1 and routed
every physical controller to Player 1 (two connected pads would fight
over the same character). This wires up the second player end to end:

- Raise WinHandler.MAX_PLAYERS to 2 and fix the never-initialized
  extraGamepadRafs array (latent NPE once the cap is lifted)
- Set EVSHIM_MAX_PLAYERS before loading libevshim in the app process so
  the Java side maps all player shared-memory pads (otherwise futex
  notifications for Player 2 are silently dropped)
- Auto-assign connected controllers to free player slots on refresh, so
  a second pad works with zero manual setup
- Route motion/key events by device to the owning player slot instead of
  adopting every controller as Player 1; a new pad while Player 1 is
  taken becomes Player 2. Reconnects of the same physical pad (same
  descriptor) still restore Player 1
- Launch the Wine process with EVSHIM_MAX_PLAYERS equal to the number of
  controllers connected at launch (clamped to MAX_PLAYERS)

Known limits: Bionic containers only (glibc/proot path has no evshim),
pads must be connected before launching the game, rumble stays
Player 1-only for now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
…ed errors

- Game fixes now also match Custom Games by launch executable name
  (lowercase basename); TLOU Part I registered as tlou-i.exe / tlou-i-l.exe
  so sideloaded installs get the same WINEVMEMMAXSIZE + Box64 profile as
  the Steam version
- Add per-player rumble pollers: extra players' rumble reaches their own
  physical pad (no phone fallback, the phone belongs to Player 1); torn
  down cleanly in stop()
- ContentsManager: replace printStackTrace/empty catches with tagged logs
  so download/parse failures are diagnosable instead of silent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
- SteamGridDB: when no API key is configured (forks don't inherit the
  upstream repo secret, which is why covers vanished in fork builds),
  fall back to the public Steam storefront search and save the store's
  capsule/header/hero art under the same file names the library UI scans
- New Controllers hub in the system menu (above Settings): assign which
  physical pad is Player 1 / Player 2, rescan devices, and the gamepad
  hints toggle moved here so every global controller option lives in one
  place (removed from Interface settings)
- New What's New screen in the system menu (below Help & Support):
  problems found, fixes shipped, and what's coming next, in Portuguese
- Strings added in English and pt-BR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
- Pre-launch component installs (Wine/Proton, DXVK, drivers) that fail now
  show a snackbar with the component name and reason instead of logging
  'continuing' silently; the previously silent abort of the whole install
  step also reports before returning
- New TelemetryCollector: samples FPS every 2s during a session and keeps
  the last 20 sessions per game in files/telemetry (on-device only). A
  .running marker left by a dead session counts as a suspected crash.
  After 3+ sessions averaging under 25 FPS, or 2+ suspected crashes, a
  one-time suggestion snackbar points at the relevant game config options
- Strings in English and pt-BR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
…ilures

- What's New: current round (download-failure toasts, local telemetry) moved
  to shipped; upcoming list refreshed
- BionicProgramLauncherComponent: make the field-vs-local envVars shadowing
  explicit (this. prefix) with a null guard, and drop the if(true) wrapper
- Replace remaining printStackTrace in FileUtils/TarCompressorUtils/ImageFs
  with tagged logs; ContentsManagerDialog empty catches now log the reason

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
- New LanRoomManager: host a room over TCP (name + optional password),
  friends join by IP with UDP broadcast discovery pre-filling the host IP
  on the same network; chat relayed to everyone; multicast lock held while
  hosting/discovering. The room coordinates players — games still connect
  through their own LAN netcode; across the internet both sides can use a
  VPN (ZeroTier/Tailscale) and join by the VPN IP
- 'Play LAN' option on installed games (long-press menu, Quick Actions
  group, wifi icon) opens the room dialog; 'Open the game' launches the
  title from inside the room, chat stays connected during play
- Game screen now shows on-device telemetry history: average FPS, session
  count and unexpected closes (TelemetryCollector.summary)
- What's New updated; strings in English and pt-BR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
The workflow needs the upstream repo's keystore/API secrets, which forks
don't inherit — every master push here failed at the generated BuildConfig.
Gate the job to the upstream repository; this fork builds through
build-apk.yml instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
The fallback CPU list included every core, so Wine/game threads could be
scheduled onto the efficiency cluster (e.g. A510 on Snapdragon 8 Gen 2),
hurting frame pacing. Detect core tiers via cpuinfo_max_freq and drop the
lowest-frequency tier when at least 4 faster cores remain; falls back to
the previous all-cores / upper-half lists when the topology can't be read
or all cores share one tier. Cached after first read. Existing containers
keep their stored list; new containers pick up the new default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
- discoverRooms did blocking socket I/O on the caller's dispatcher and is
  invoked from a LaunchedEffect: opening the Join tab would crash with
  NetworkOnMainThreadException. Now runs on Dispatchers.IO
- Guests saw their own chat messages twice (local append + host echo);
  the client now skips the echo of its own lines
- Host chat broadcast reuses one PrintWriter per client (synchronized)
  instead of creating a new writer per message, avoiding interleaved lines
  and dropping dead writers on failure
- Game-screen telemetry history is now read via produceState on
  Dispatchers.IO instead of during composition on the main thread

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuNDfhLYu1Sbf89gWzfDvF
…tion, preset storage

- ProcessHelper.getAffinityMask: build masks with bit shifts instead of
  (int)Math.pow(2, i), which saturated at Integer.MAX_VALUE for core 31
  and corrupted the mask. Guard indices to the 0-31 range.
- ProcessHelper: PRINT_DEBUG now follows BuildConfig.DEBUG (was hardcoded
  true, printing every line of child process stdout/stderr in release);
  child pids come from Process.pid() on API 33+ with reflection fallback.
- ContainerManager.duplicateContainer: load the copied .container config
  and only override the name, instead of hand-copying 21 of ~60 fields
  (duplicates silently lost containerVariant, emulator, renderer settings,
  input mappings, etc.).
- Box86_64PresetManager: store custom presets as a JSON array instead of
  the "id|name|env," format, which corrupted as soon as an env value
  contained a comma or pipe (e.g. ZINK_DEBUG=compact,deck_emu). Legacy
  strings are still parsed and migrated on the next write.
- GuestProgramLauncherComponent (proot path): respect the user's WINEESYNC
  value instead of forcing it to 0 after the /dev/shm bind was already set
  up from that value.
- BionicProgramLauncherComponent: verbose Steam client/networking debug
  env vars (STEAM_LOG_LEVEL=10, IPCLOGGING=1, ...) only on debug builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- ASurfaceRenderer.pushRenderList: reuse the visible-id set and window
  geometry scratch objects instead of allocating per window on every scene
  update, and gate the verbose per-window log line behind BuildConfig.DEBUG.
  computeWindowRect now resets the dst rect so reuse cannot leak the
  previous window's geometry into branches that don't set it.
- TouchpadView: all 19 XForm.transformPoint call sites now use reusable
  instance buffers instead of allocating a float[2] per touch/hover event
  on the UI thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- XServerScreen: stop truncating CPU affinity masks to 16 bits with
  .toShort() (sign-extension could spuriously enable cores 15-31);
  ALWAYS_REEXTRACT is now false - re-extraction of DXVK/graphics drivers
  is driven by change detection plus integrity guards. Root cause of the
  corruption it papered over is fixed: the graphics-driver extra/sentinel
  markers were written BEFORE extraction ran (an interrupted launch left
  a container with deleted driver libs and "up to date" markers); they
  are now written only after successful extraction. Missing or truncated
  dxgi/d3d11/d3d9.dll in the prefix also forces a re-extract.
- XServerScreen: the PulseAudio low-latency toggle now also lowers
  PULSE_LATENCY_MSEC (144 -> 60) when it is still at the default, so the
  toggle actually reduces audio latency for Wine's Pulse client.
- GeneralTab: the Wine version selector is now always visible; options
  follow the container variant (bionic -> Proton/Wine builds, glibc ->
  glibc Wine builds, previously computed but never rendered).
- WineTab: merge the duplicated GPU dropdowns ("Renderer" and "GPU Name"
  were bound to the same index/list and fought over the same state).
- SteamBootstrap: reuse ProcessHelper.getPid() instead of reflection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Add -Wl,-z,max-page-size=16384 to the virglrenderer, patchelf and proot
link options, matching the flags already applied to the other native
targets. Devices shipping with a 16 KB kernel page size reject .so files
whose ELF LOAD segments are only 4 KB aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
… review report

- ProcessHelperAffinityTest: pure-JVM coverage of getAffinityMask overloads,
  including the historical core-31 saturation and 16-bit sign-extension bugs.
- Box86_64PresetManagerTest: JSON round-trip of custom presets, migration from
  the legacy pipe/comma format, corrupted-entry tolerance, per-prefix isolation.
- docs/RELATORIO_REVISAO_2026-07.md: full code review report (bugs found and
  fixed, performance bottlenecks, compatibility issues, architecture notes,
  comparison with other emulation projects, novel feature proposals with
  feasibility studies, test coverage analysis, prioritized roadmap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Process.pid() exists at runtime on Android 13+ but the compile-time
android.jar does not expose it, so compileModernDebugJavaWithJavac failed
with "cannot find symbol". Invoke it via reflection on API 33+ and keep
the private-field fallback for older versions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The glibc container variant only ever offered the single bundled
wine-9.2-x86_64 build, with no way to add more. Three things blocked it:

- The Wine/Proton importer rejected every glibc binary outright. The glibc
  container variant is only exposed on legacy (non-MODERN_ANDROID) builds,
  so on those builds glibc is the supported path and the rejection is
  contradictory. The rejection is now gated on BuildConfig.MODERN_ANDROID:
  modern builds still reject glibc (the variant is hidden there), legacy
  builds accept it.
- The glibc Wine dropdown was built with an empty "installed" list, so even
  an imported glibc build never showed up. It now includes installed
  Wine/Proton content, mirroring the bionic dropdown.
- There was no in-app way to fetch a build from a URL. Added a
  "Download from URL" field to the Wine/Proton manager that streams an
  arbitrary package (e.g. a GitHub release .wcp/.tzst) into the cache and
  installs it through the existing content pipeline, with the same
  untrusted-file confirmation and error handling as the other flows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The Pull Request build check injected POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}
for same-repo PRs. On forks without that Actions secret configured, the value is
empty, which makes the secrets plugin emit `POSTHOG_API_KEY = ;` in BuildConfig
and fails compilation. Fall back to the same dummy values the fork-PR path and
the APK build already use, so the check builds regardless of secret configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Container.saveData()/loadData() already write atomically (FileUtils.writeString
uses a temp file + ATOMIC_MOVE), so no code change was needed there. These
Robolectric tests lock down the .container schema: field round-trip, comma-bearing
env vars, and the legacy key migrations in checkObsoleteOrMissingProperties
(useLegacyRenderer, turnip-zink/llvmpipe, dxcomponents).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
DynaCache (box64 0.3.8+) persists translated blocks to ~/.cache/box64 so repeat
launches skip re-translation, cutting startup time and JIT-compilation stutter.
Add BOX64_DYNACACHE (0/1/2), BOX64_DYNACACHE_LIMIT (MiB) and
BOX64_DYNACACHE_COMPRESS (0/1/2) with value suggestions so users can enable it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
A fully-guarded wrapper around Android's Dynamic Performance Framework:
- thermalHeadroom(): PowerManager.getThermalHeadroom (API 30+), NaN when absent.
- suggestedCap(): pure, unit-tested logic that lowers the FPS cap as the SoC
  approaches thermal throttling, to avoid the hard frame drops that appear after
  minutes of play.
- createSession()/Session: PerformanceHintManager wrapper (API 31+) so the OS can
  boost the game's hot threads.

Every platform call is guarded so unsupported devices simply get a no-op. Not yet
wired into the render loop; that (hot-path) integration is intentionally left for
explicit review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The PR-check unit-test run (now reachable after the PostHog fix) exposed two failures:

- Box86_64PresetManagerTest tried to mock the PrefManager object with MockK, which
  fails because PrefManager is a DataStore-backed Kotlin object. Rewrite it as a
  Robolectric test using the real PrefManager (matching the downloader test pattern).
- FileUtilsTest (pre-existing, from utkarshdalal#1520) failed because android.util.Log is not
  mocked in plain-JVM unit tests. Enable testOptions.unitTests.isReturnDefaultValues
  so unmocked android.jar calls return defaults instead of throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- ContentsManagerDialog: two latch.await() calls had no timeout; a lost
  install callback would hang the coroutine and IO thread forever. Bound them
  to 240s like the other install flows.
- Amazon download/SDK managers: renameTo() return value was ignored after
  deleting the destination, so a cross-filesystem move (SD/OTG) could report
  success with the file missing. Fall back to copy and fail loudly.
- SteamService: mark instance/isConnected/isStopping/isRunning @volatile
  (they are written on the CallbackManager thread and read from IO coroutines).
- SteamService.fetchFile: emit -1 for indeterminate progress instead of a
  NaN/negative fraction when the response has no Content-Length.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- Default WINE_LARGE_ADDRESS_AWARE=1 / PROTON_FORCE_LARGE_ADDRESS_AWARE=1 in the
  launch env so 32-bit games can use up to 4GB and stop crashing on OOM (matches
  Proton's default; user overrides are respected).
- Set WINENTSYNC=1 only when the kernel exposes /dev/ntsync (custom 6.14+ GKI);
  esync stays as the fallback for older Wine, which ignores the variable.
- Expose in the env-var picker: VKD3D_CONFIG (nodxr/no_upload_hvv/single_queue to
  cut D3D12 VRAM), WINE_LARGE_ADDRESS_AWARE, WINE_FULLSCREEN_FSR (+ _STRENGTH) for
  render-scale upscaling, and BOX64_DYNAREC_NOARCH to reduce dynarec RAM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
When an FPS cap is enabled, sample PowerManager thermal headroom every 4s and
transiently lower the applied cap as the SoC nears throttling (via
PerformanceGovernor.suggestedCap), restoring the user's target as it cools. This
only tightens an already-enabled cap and is a no-op where thermal headroom is
unavailable, so default behavior is unchanged for users who don't cap FPS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- DXVKHelper: emit dxvk.maxFrameLatency and dxvk.numCompilerThreads into dxvk.conf
  when set in the wrapper config. maxFrameLatency trades latency for steadier
  pacing; capping numCompilerThreads keeps shader compilation off the big-cores
  the game and Box64 need, cutting frametime spikes. Both are opt-in, so default
  behavior is unchanged.
- XServerScreen: when unpacking DRM executables, hard-link the original backup
  and the unpacked replacement instead of doing two full-file copies of the exe
  per launch (delete-before-link so a shared inode is never truncated; falls back
  to copy on filesystems without hard-link support).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
One switch in the Graphics tab that renders at a lower internal resolution and
upscales with FSR to raise FPS on demanding games, without hunting through env
vars. Plumbed as a container setting like sfCompatMode (Container, ContainerData,
PrefManager default, ContainerUtils, Graphics tab) and applied at launch by
setting WINE_FULLSCREEN_FSR when the game boots; explicit user env still wins.

Per-game low-spec profiles remain handled by the existing game-fix mechanism
(IniFileFix/PrefixFileFix auto-applied by GameFixesRegistry, plus the downloadable
BestConfig API) — this toggle is the engine-agnostic, opt-in universal lever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Three Box86_64PresetManagerTest cases pre-seeded a raw legacy "id|name|env"
string into DataStore and read it back; they proved flaky under Robolectric's
async DataStore across test methods (the manager's own write/read path passes).
The essential guarantee — JSON round-trip with commas and pipes, the actual bug
that was fixed — stays covered by the round-trip test, which writes through the
manager API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…stness

- Release the multicast lock after a discover-only browse (it was only released
  on stop(), so opening the join tab and closing the dialog leaked a held lock
  and drained the battery indefinitely).
- On bind failure (port in use) set Status.ERROR instead of leaving the UI in a
  fake "hosting" state with nothing listening.
- Make _chat appends atomic (StateFlow.update) so concurrent client coroutines
  don't lose messages via read-modify-write races.
- Enable TCP keepAlive on room sockets and tolerate a single malformed JSON line
  (skip it) instead of dropping the whole connection.
- Only announce "<name> saiu da sala" for peers that actually joined (no more
  ghost "? saiu" for password-denied/invalid connects).
- localIpAddress()/new allIpAddresses(): rank LAN first, then RFC1918 172.16/12
  and Tailscale/CGNAT 100.64/10, so users on a VPN (ZeroTier/Tailscale) can see
  the address friends should join by. Cap room/game name length.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Host view gets a "Copy invite link" button that puts a gamenative://lan/join?ip=…&pw=…
link on the clipboard (built from the shown IP + room password). The join field now
accepts either a bare host IP or a pasted invite link — LanRoomManager.parseJoinLink
extracts the IP and password so a friend can just paste the link and connect,
including over a VPN by sharing the VPN IP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
claude added 25 commits July 8, 2026 00:06
Found by an audit sweep across the native X server, the download layer and
the Game Hub.

Native memory safety (Drawable):
- PutImage depth-1/BITMAP path wrote width*height ints into the destination
  with no bounds check — a client sending an oversized width/height on a
  small pixmap corrupts the heap. Clamp to the destination like the 24/32-bit
  path already does.
- CopyArea clamped only the destination, passing the client-controlled
  srcX/srcY straight to native, which then reads outside the source drawable
  (OOB read / info leak). Clamp the source too and bail on empty regions.

Download layer (DownloadInfo):
- bytesDownloaded is incremented concurrently by every parallel chunk; the
  plain `+=` lost updates so progress under-counted and never hit 100%. Now
  an AtomicLong.
- emitProgressChange iterated a plain list while the UI mutated it →
  ConcurrentModificationException. Now a CopyOnWriteArrayList.
- Speed-sample trim used first() after an isNotEmpty() check on a concurrent
  list → NoSuchElementException; use firstOrNull().
- Amazon path-traversal check matched a bare prefix, so a sibling dir sharing
  the install-dir name prefix slipped through. Match on a separator boundary.

Game Hub (StoreManager):
- unifiedLibrary() promised per-store error isolation but plain combine()
  neither seeds nor catches, so one store that threw or never emitted could
  crash the collector or blank the whole library. Each slice now onStart-seeds
  empty and catches to an empty slice.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The onStart { emit(emptyList()) } added to each slice made the first
combined emission empty, which broke callers (and StoreManagerTest) that
take unifiedLibrary().first() expecting the merged list. The catch — the
actual crash fix (a throwing store no longer cancels the merge) — is kept;
the anti-stall seed is dropped because real providers emit immediately, so
combine's first emission is already the full merge.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Opens the existing library options panel — sort by, app type, app status,
and the layout selector (List / Capsule / Hero / Carousel) — straight from
the system menu, so those options are reachable without hunting for the
options button. Non-destructive: it just toggles the panel that already
exists; the item sits directly under Controllers.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
From a 10-agent performance study of the emulation stack. This is the
low-risk, high-impact batch (config/default/guard/dedup changes); the
larger pipeline rewrites are tracked separately.

Renderer / X server hot path:
- Drawable.drawImage submitted every 32bpp PutImage to the native scanout
  TWICE (duplicate rewind+forceUpdate). PutImage is the hottest request in
  the server; now one submit per image — roughly halves JNI upload + present
  traffic on the dominant blit path.
- xconnector_epoll: the epoll events[] buffer was file-scope and shared by
  every connector thread (X, VirGL, Vortek, ALSA, SysV-SHM) running
  concurrently — a data race. Made it stack-local.

CPU emulation:
- Box64 default preset COMPATIBILITY -> INTERMEDIATE for new containers.
  COMPATIBILITY disables BIGBLOCK/CALLRET, forces SAFEFLAGS=2 and slow FP;
  INTERMEDIATE tracks box64 upstream defaults without the risky
  STRONGMEM/AVX levers — a large throughput win, per-game overridable.

Wine:
- Set WINEFSYNC=1 (futex sync) alongside esync; faster/lighter, and Wine
  auto-falls-back to esync on kernels without futex support, so it's safe.
- Disable winemenubuilder.exe (pure per-install/startup spawn overhead here).
- ShowCrashDialog=0 so a crashing game fails fast instead of hanging on a
  modal that a touch UI can never answer.

DXVK / VKD3D (anti-stutter):
- Enable the gplasync on-disk pipeline cache by default (ASYNC_CACHE=1); the
  shipped build is 2.6.1-gplasync whose whole point is this cache.
- Include async/asyncCache in DXVKHelper.DEFAULT_CONFIG so fallback containers
  aren't left with async disabled.
- Wire VKD3D_SHADER_CACHE_PATH so D3D12 titles stop recompiling every PSO on
  each launch.

Startup:
- extractComponentsWithVersionCheck now actually version-checks: a .version
  sentinel skips re-extracting pulseaudio (date-stamped asset) on every
  launch instead of delete+re-extract each time.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Two DXVK defaults applied at launch when the container doesn't set them:

- numCompilerThreads: capped to [1,4] (~cores/2-1) so pipeline compilation
  stops stealing the big cores from Box64/Wine — a common cause of frametime
  spikes on shader-heavy scenes. The gplasync on-disk cache (just enabled)
  softens the slightly slower first-run compile this trades for.

- maxDeviceMemory/maxSharedMemory: was unbounded (0), which on unified-memory
  Android makes DXVK report the full Vulkan heap so games size pools as if
  they had discrete VRAM and over-commit (paging/lowmemorykiller thrash or
  guest OOM). Now derived from physical RAM (~70%, floor 2 GB) — well above
  any mobile game's real need, so it only trims the pathological case.

Both remain overridable per container via the existing config fields.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
One action that fetches every entry in the remote manifest — all
Wine/Proton, DXVK, VKD3D, Box64/WoWBox64, FEXCore versions and GPU drivers
— instead of installing each version by hand.

- ManifestBulkInstaller: maps each manifest type key to its ContentType (or
  driver), then installs entries sequentially via the existing
  ManifestInstaller. Never throws — a failed entry is counted and the sweep
  continues, returning an installed/total tally.
- Settings > Emulation: a "Download all components" tile with a size warning
  confirmation (can be several GB), a live progress dialog, and a summary.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Adds a full-bleed wallpaper behind the library that the user configures
from the Layout options panel: a looping user-picked video (optionally
with sound) or a static image. Video takes priority when both are set.

- PrefManager: 4 new prefs (enabled / video URI / image URI / sound).
- LibraryBackground composable: ExoPlayer-backed looping video with
  optional audio (lifecycle-aware, releases on dispose, swallows
  playback errors to a no-op) or a Coil image; RESIZE_MODE_ZOOM for
  full-bleed. Caller draws a scrim over it for text legibility.
- LibraryScreen: renders the wallpaper + a 0.6-alpha scrim behind the
  content, drops the solid background colour when a wallpaper is active,
  and guards against @Preview (LocalInspectionMode).
- LibraryOptionsPanel: a Background section under Layout with enable and
  sound toggles, video/image pickers (OpenDocument +
  takePersistableUriPermission), a remove action, and a hint.
- strings.xml: wallpaper section strings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The strings added this session (Game Hub, LAN chat/invite, library
wallpaper, low graphics mode, download-all, animated login background,
Wine/Proton URL install, Layout menu) were only present in the English
base, so they showed in English for pt-rBR users. Add the missing
Brazilian-Portuguese translations for all of them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- XServerScreen: the in-game LAN chat overlay was passed
  visible = showLanChat && lanInRoom, which made the overlay's own
  self-close-on-room-end effect unreachable (visible already implied the
  room was up). showLanChat then stayed stuck true after a room closed —
  the overlay popped up unbidden on the next room, Back was silently
  swallowed, and pointer capture wasn't restored. Pass visible =
  showLanChat and gate the BackHandler on showLanChat && lanInRoom.
- LanRoomManager: after making serverSocket @volatile, the host
  accept-loop used serverSocket-nullness to tell a bind failure from a
  normal stop(); stop() nulls it cross-thread, so a normal leave was
  misclassified as 'port in use' and flipped the room to ERROR with a
  bogus chat line. Use a coroutine-local  flag instead.
- LanRoomManager: host sendChat ran the blocking broadcast() on the
  caller (UI) thread; a stalled peer could ANR it. Offload to scope like
  the guest path (local echo still immediate).
- SettingsGroupEmulation: wrap download-all in try/finally so the
  non-dismissible progress dialog is always cleared even if it throws
  before installAll returns.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
- MainViewModel.onWindowMapped: the process-walk read window.parent (the
  fixed param) each iteration instead of currentWindow.parent, so for any
  game window whose parent isn't explorer.exe the loop never advanced —
  infinite loop on the main thread (ANR) with an unbounded list. Walk up
  via currentWindow.parent.
- SteamService AppDownloadListener: depotCumulativeCompressedBytes was a
  plain HashMap mutated concurrently by parallel depot workers; use
  ConcurrentHashMap so concurrent structural writes can't corrupt it.
- IntentLaunchManager.mergeConfigurations rebuilt ContainerData from a
  hand-listed subset of fields, silently resetting every unlisted field
  (containerVariant, wineVersion, emulator, renderer, fexcore*, etc.) to
  constructor defaults. Build from base.copy(...) so unlisted fields keep
  the base container's value.
- DrawRequests.polyFillRectangle filled with the GC background pixel;
  X11 PolyFillRectangle uses the foreground pixel.
- GOG/Epic streaming download: a permanently-failed chunk never
  decremented pendingChunks and the stuck-detector re-emitted it forever,
  hanging the whole download. Record the failure and abort the wait loop
  with a clean failure instead of spinning.

Deferred (need on-device testing): ASurfaceRendererContext fence-fd
double-close and InputControlsView touch hot-path.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…Manager)

Adding an assignment to assemblyFailure inside the flow closure made
Kotlin treat it as 'mutated in a capturing closure', which disabled the
smart-cast on the pre-existing assemblyFailure.message read at line 978.
Bind the non-null value to a local val and use that for both the assign
and the log.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Actions storage hit 100%: every run uploads ~800 MB of APK artifacts and
they pile up. Releases don't count against the Actions quota, and the
artifacts only exist to hand APKs to the release job, so:
- retention-days: 1 on both uploads;
- new 'cleanup' job that runs at the start of every workflow (in
  parallel with build, so space is freed before the new upload) and
  deletes all artifacts except those of the 4 most recent runs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…aunch guard

New RuntimeCompatibility module:
- Internal compatibility matrix (Wine/Proton series ↔ container variant ↔
  minimum Box64), with version comparison and best-available pick.
- libc detection by scanning the Wine build's ELF binaries (glibc
  references libc.so.6/__libc_start_main; bionic references
  __libc_init/liblog.so).
- checkAndAutoFix(): pre-launch guard wired into XServerScreen BEFORE the
  appliedWineVersion mismatch markers, so a bionic-built Wine selected in
  a glibc container (or vice versa) is swapped to a known-good fallback
  instead of crashing the loader with 'Symbol __libc_init not found,
  cannot apply R_X86_64_JUMP_SLOT' — and the swap still triggers prefix
  re-extraction. Also bumps too-old Box64 for the Wine series. Every fix
  is persisted, shown as a snackbar, and appended to a friendly log at
  files/logs/runtime_compat.log (wine selected / reason / action).
- GeneralTab: selecting a Wine that needs a newer Box64 now auto-adjusts
  Box64 in the same config update and warns the user (e.g. Wine 11 +
  Box64 0.3.4 → Box64 0.3.6), instead of letting the game fail at boot.
- Strings in English base + pt-BR.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Real-time library (no more manual refresh):
- New CustomGameWatcher: one inotify FileObserver per custom-game folder
  (CREATE/DELETE/MOVED/CLOSE_WRITE, ignoring our own .extracted.ico
  artifacts to avoid scan loops).
- LibraryViewModel arms it at init and re-arms when a folder is added;
  events are debounced 1s (file copies emit storms), then the custom-game
  cache is invalidated and only the current page is re-filtered. Dropping
  a game folder, an exe, or a cover image on disk now updates the library
  live — including cover/name/metadata.

Cover manager (custom games):
- New CoverArtManager: sets a user-picked image as the game cover —
  decodes with subsampling (no OOM on huge photos), downscales to 1440px
  long edge, saves as optimized cover.jpg in the game folder (replacing
  any older cover.*), which takes priority over SteamGridDB art;
  removeCustomCover() restores the default resolution order.
- CustomGameAppScreen: new 'Change cover' (image picker) and 'Remove
  custom cover' options in the game options panel, refreshing the library
  through the existing CustomGameImagesFetched event.
- Strings in English base + pt-BR.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…, persistent Mesa shader cache

Improvements identified from the Winlator-Ludashi fork (v2.7-v3.1
changelogs), adapted to GameNative:

- Game screen now requests the panel's highest refresh-rate display mode
  (preferredDisplayModeId) while a game is on screen and restores the
  system policy on exit. Many devices pin unknown apps to 60 Hz on
  90/120 Hz panels, so the FPS limiter was targeting rates the display
  never reached (Ludashi's 'DeviceRefreshRate' feature).
- TU_DEBUG picker gains forcecb/nocb (Turnip concurrent-binning
  control), plus new FD_DEV_FEATURES and IR3_SHADER_DEBUG env vars for
  per-game Adreno tuning.
- Pin MESA_SHADER_CACHE_DIR to the persistent imagefs cache dir next to
  DXVK_STATE_CACHE_PATH: the cache was enabled but its directory was
  never pinned, so Zink/GL shader caches were rebuilt every session.

Already present in GameNative (no port needed): BCn emulation +
compute-shader path, mailbox/FIFO present modes, universal FPS limiter,
GPU spoofing, driver manifest manager, EXE icon/image scraper fallback,
appCategory=game (the honest version of the Ludashi package-name trick),
foreground service keeping sessions alive.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
… + DXVK semaphore toggle

Cloned StevenMXZ/Winlator-Ludashi and verified the remaining changelog
items against their actual code:

- Their v3.1 'crash prevention service' is a foreground service holding
  a PARTIAL_WAKE_LOCK. GameNative already keeps a foreground service and
  the screen on, but the CPU was still suspended if the user locked the
  screen or switched away mid-game, freezing/killing the guest. Port: a
  partial wakelock scoped strictly to the XServer game session (acquired
  on entry, released on dispose) + WAKE_LOCK permission.
- Expose DXVK_DISABLE_TIMELINE_SEMAPHORES in the per-game env picker
  (Ludashi ships it globally; broken timeline semaphores on Mali/older
  Adreno cause hangs with DXVK 2.x — per-game opt-in is safer).

Verified NOT worth porting: their imagefs extraction is byte-identical
to ours (same TarCompressorUtils/buffer; the '2x' was XZ→ZSTD which we
already use); BOX64_MMAP32 handling already present in our presets/RC;
VKD3D_SHADER_MODEL=6_6 / TU_DEBUG=sysmem global defaults skipped as
device-risky (both already available per-game).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…, gallery picker

The cover options shipped in bf13205 did not work as expected on device:

- Placement: they were grouped under Game Management while 'Fetch game
  images' lives in Help & Info, so they didn't appear below it as
  intended. Moved to the same section — the list order already puts
  Change cover / Remove custom cover directly after Fetch game images.
- The open detail screen never refreshed: its cover URLs are remembered
  keyed on the folder path, which doesn't change when the cover file
  inside it does. New coverRefreshTick companion state, bumped on
  set/remove and used as a remember key, so the screen updates the
  moment a cover is picked (the 'Remove custom cover' entry also
  appears/disappears reactively now).
- Coil cache: overwriting cover.jpg kept the same file:// URL, so the
  old image kept showing. Cover URLs now carry ?v=<lastModified> as a
  cache-busting key (Coil loads file URIs by path; the query only
  affects the cache key).
- Picker: switched OpenDocument → GetContent so the gallery/photo
  picker opens instead of the documents UI.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…Amazon path traversal, Epic crash

From the project-wide audit (verified by reading each site):

- SteamService.onLoggedOn reassigned picsChangesCheckerJob/
  picsGetProductInfoJob without cancelling the previous ones, leaking a
  checker + product-info coroutine on every Steam reconnect. Cancel
  before reassigning.
- SteamService.isLoggingOut / isWaitingForQRAuth are written and read
  across threads but lacked @volatile (unlike their sibling flags).
- SteamService.fetchFile used a fixed '<dest>.part' temp path, so two
  concurrent fetches of the same file clobbered each other's temp and
  corrupted the result. Use a per-call UUID temp name.
- ContentsManagerDialog / DriverManagerDialog set SteamService.isImporting
  before launching the file picker but never cleared it when the picker
  was cancelled (or, for Contents, when the import failed), permanently
  blocking SteamService shutdown. Clear on the null-uri path and on the
  failure path / via finally.
- StoreManager.unifiedLibrary didn't dedup by id; two providers emitting
  the same id crash the Game Hub LazyGrid (duplicate key). distinctBy id.
- AmazonSdkManager.ensureSdkFiles wrote manifest-supplied paths with no
  containment check — a '../' entry escaped the SDK cache dir. Added a
  canonical-path traversal guard.
- EpicDownloadManager preAlloc threw IOException from a root launch of a
  non-supervised scope with no handler, crashing the whole app on a
  preallocation failure. Record it as a download failure instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…ocket

- handleClient's finally called refreshPlayers() unconditionally, so a
  handler finishing after stop() could resurrect a phantom idle player or
  clobber a freshly created next room's roster. Only refresh while HOSTING.
- runDiscoveryResponder leaked the DatagramSocket fd when bind() failed
  (socket created before assignment to discoverySocket, catch had no
  handle). Declare it outside the try and close it in finally.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…matException + guest launch failure)

The version dropdowns built ids straight from the labels, so selecting
'0.3.6 (Default)' stored box64Version='0.3.6 (Default)'. That string then
flowed into getProfileByEntryName('box64-0.3.6 (Default)'), whose numeric
fallback did Integer.parseInt('0.3.6 (Default)') → NumberFormatException,
the box64 profile was never found, extraction fell back to a
non-existent asset name, and the guest launched with no box64 — surfacing
as 'wine: could not load kernel32.dll, status c0000135'.

- ManifestComponentHelper.buildVersionOptionList: key options by a clean
  id (trailing ' (Default)' stripped), so stored version ids are always
  clean; also de-dupes the default entry against its installed copy.
- ContentsManager.getProfileByEntryName: strip a ' (Default)' suffix up
  front (repairs already-saved containers) and skip the numeric fallback
  when the tail isn't all digits (e.g. 'wine-9.2-x86_64') instead of
  throwing/logging a NumberFormatException on every lookup.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
Rotation: the custom OrientationEventListener (startOrientator) is
disabled because it leaked/restarted the Activity, which left
currentOrientationChangeValue stuck at 0 — so setOrientationTo's manual
angle math locked every game to ONE fixed landscape. Map the allowed
orientation set to Android's own sensor constant
(SENSOR_LANDSCAPE / SENSOR_PORTRAIT / FULL_SENSOR) so the OS rotates the
game freely within it: holding the phone in either landscape now
auto-rotates to match, which is the requested behaviour, with no
listener to leak.

Box64 tags: expose the modern performance dynarec vars in the quick env
picker (WEAKBARRIER, PAUSE, ALIGNED_ATOMICS, BLEEDING_EDGE, SSE42,
SHAEXT, MMAP32, and a MAXCPU=0 auto option) and register the two that
were missing from RCField (WEAKBARRIER, PAUSE) so the RC editor persists
them instead of dropping them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
A proper on-device logging system (not unlimited):
- SessionLogger: a Timber tree mirroring logs into a rotating file under
  files/logs/session, hard-capped at ~4 MB (1 MB x 4 files) so it never
  grows without bound. Survives app kills (unlike logcat). Persists
  everything in debug, WARN+ in release to keep the hot path cheap.
  Planted at app start in PluviaApp.
- DiagnosticsAnalyzer: maps known guest/emulator failure signatures
  (__libc_init/libc mismatch, kernel32 c0000135 = box64 not loaded,
  Wwise/AkAudio init, box64 missing library, Vulkan device-lost, D3D
  feature level) to a friendly pt-BR explanation with the fix.
- XServerScreen: the guest-output callback now feeds error-ish lines to
  the analyzer and shows a one-shot snackbar with the diagnosis (deduped
  per session) so a cryptic loader dump becomes an actionable message,
  and mirrors those lines into the session log.
- Debug settings: 'Session log' entry to view/share the bounded log.
- Strings in EN base + pt-BR.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
The termination callback treated status 137 (128+9 = SIGKILL) the same
as any game error. While the app is backgrounded that status is almost
always Android's low-memory/power management killing the paused guest,
not a game bug. Detect status==137 while isOverlayPaused and log it as a
system background-kill (with the battery-optimization fix) instead of
firing the game-launch-error path — so telemetry and the user aren't
misled. A genuine in-game crash still takes the error path.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
…FPS)

When a container's graphicsDriverConfig has no presentMode, the code
still exported MESA_VK_WSI_PRESENT_MODE='' — an empty value that the
Turnip/Mesa WSI ignores or maps to a slow default, and which overrode the
'mailbox' default set earlier in setup, costing FPS. Guard both this and
the sibling WRAPPER_RESOURCE_TYPE so they're only set when non-empty.

Found by the FPS audit workflow.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwSSHzP8di8bFWR1xnX84Z
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The pull request adds a unified Game Hub, LAN rooms and chat, runtime diagnostics and telemetry, configuration improvements, download hardening, rendering and controller updates, CI workflows, localized resources, tests, and engineering documentation.

Changes

GameNative feature and reliability changes

Layer / File(s) Summary
Build, permissions, and concurrency foundations
.github/workflows/*, app/build.gradle.kts, app/src/main/AndroidManifest.xml, app/src/main/java/app/gamenative/data/*
Adds debug APK build/release automation, test defaults, LAN/wakelock permissions, Box64 options, sensor orientation handling, session logging startup, and thread-safe download/event state.
Unified Game Hub architecture
app/src/main/java/app/gamenative/gamehub/*, app/src/main/java/app/gamenative/ui/model/GameHubViewModel.kt, app/src/main/java/app/gamenative/ui/screen/gamehub/*, app/src/test/java/app/gamenative/gamehub/*
Introduces unified game models, store adapters, persistence, custom stores, provider registration, aggregation, filtering, sorting, navigation, and tests.
LAN rooms, chat, and application UI
app/src/main/java/app/gamenative/lan/*, app/src/main/java/app/gamenative/ui/*, app/src/main/java/app/gamenative/utils/CoverArtManager.kt
Adds LAN hosting/joining, discovery, chat overlays, controller dialogs, wallpapers, login video backgrounds, custom covers, and related navigation/settings.
Runtime compatibility, telemetry, and diagnostics
app/src/main/java/app/gamenative/utils/{RuntimeCompatibility,SessionLogger,TelemetryCollector,PerformanceGovernor}.kt, app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Adds Wine/Box64 compatibility checks, bounded logs, local telemetry, thermal FPS governance, diagnosis reporting, and launch-time integration.
Download, assembly, and protocol hardening
app/src/main/java/app/gamenative/service/{amazon,epic,gog}/*, app/src/main/java/app/gamenative/xserver/*, app/src/main/java/app/gamenative/ui/PluviaMain.kt
Adds path containment checks, rename/copy fallbacks, fail-fast chunk assembly, malformed-request recovery, and install failure feedback.
Container, rendering, and controller behavior
app/src/main/java/com/winlator/{container,core,renderer,widget,winhandler,xserver}/*, app/src/main/cpp/*
Updates container persistence and CPU selection, graphics configuration, rendering allocation, touch input reuse, multi-player controller routing, native buffer copying, and 16 KB linker alignment.
Resources and regression coverage
app/src/main/res/values*/strings.xml, app/src/test/java/*
Adds localized resources and regression tests for Game Hub mappings, store aggregation, thermal caps, presets, containers, and affinity masks.
Engineering documentation
docs/*
Adds delivery, review, server architecture, and XoDos analysis documents.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: utkarshdalal, phobos665, joshuatam

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague and generic to describe the main change in this large review-focused PR. Replace it with a concise, specific summary of the primary change, such as the new Game Hub and related UI/runtime fixes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (7)
app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt (1)

64-77: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use ConcurrentHashMap.compute() for atomic read-modify-write.

The setFavorite, setLastPlayed, and setConfigurationProfile methods perform a non-atomic read-modify-write: store[gameId] is read, then store[gameId] = ... is written. If two suspend functions execute concurrently on different dispatchers (e.g., setFavorite + setLastPlayed for the same game), one update can overwrite the other — a classic TOCTOU race. The class doc at line 51 claims "Correct and thread-safe," but ConcurrentHashMap only guarantees atomicity of individual get/put calls, not compound operations.

♻️ Proposed fix using atomic compute()
 override suspend fun setFavorite(gameId: String, favorite: Boolean) {
-    store[gameId] = (store[gameId] ?: GameHubMetadata(gameId)).copy(favorite = favorite)
+    store.compute(gameId) { _, existing -> (existing ?: GameHubMetadata(gameId)).copy(favorite = favorite) }
     publish()
 }

 override suspend fun setLastPlayed(gameId: String, epochMillis: Long) {
-    store[gameId] = (store[gameId] ?: GameHubMetadata(gameId)).copy(lastPlayedAt = epochMillis)
+    store.compute(gameId) { _, existing -> (existing ?: GameHubMetadata(gameId)).copy(lastPlayedAt = epochMillis) }
     publish()
 }

 override suspend fun setConfigurationProfile(gameId: String, profileId: String?) {
-    store[gameId] = (store[gameId] ?: GameHubMetadata(gameId)).copy(configurationProfileId = profileId)
+    store.compute(gameId) { _, existing -> (existing ?: GameHubMetadata(gameId)).copy(configurationProfileId = profileId) }
     publish()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt` around
lines 64 - 77, Replace the non-atomic read-modify-write logic in setFavorite,
setLastPlayed, and setConfigurationProfile with
ConcurrentHashMap.compute(gameId) so each update derives from the current value
and writes atomically, preserving concurrent changes to the same game’s metadata
while retaining the existing publish behavior.
app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt (1)

112-112: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Mask the auth token input.

The authToken field renders the API key in plain text via the generic Field composable. On a shared device or screen recording, this exposes the credential to shoulder-surfing.

🔒️ Proposed fix: add password masking to the token field
-            Field("Token / API key", authToken) { authToken = it }
+            OutlinedTextField(
+                value = authToken,
+                onValueChange = { authToken = it },
+                label = { Text("Token / API key") },
+                singleLine = true,
+                visualTransformation = PasswordVisualTransformation(),
+                modifier = Modifier.fillMaxWidth(),
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt` at
line 112, Mask the credential in the authToken field by replacing the generic
Field composable with the project’s password/secret input variant, or add
password visual transformation and appropriate keyboard options to Field if
supported. Update the field in CustomStoreDialog so entered API keys are
rendered as bullets while preserving authToken state updates.
app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt (1)

212-230: 📐 Maintainability & Code Quality | 🔵 Trivial

Optional: bail out or warn the user if persisting URI permission fails.

Currently a failed takePersistableUriPermission only logs a warning, yet the code still saves the URI and enables the feature — the video may silently stop working after the app restarts (permission not retained). Consider surfacing this to the user instead of only logging it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt`
around lines 212 - 230, Handle failure from takePersistableUriPermission in the
loginBgVideoPicker callback by warning the user and avoiding
persistence/enabling when permission retention fails; only update
loginBgVideoUri, PrefManager values, and show the success Snackbar after
permission succeeds, while retaining Timber logging for diagnostics.
app/src/main/java/app/gamenative/gamefixes/GameFixesRegistry.kt (1)

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exposing exeNameFixes for test override.

fixesProvider has a test-only override hook (setFixesProviderForTests), but exeNameFixes is hardcoded with no override mechanism. If custom-game fix matching needs testing, consider adding a similar provider pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/gamefixes/GameFixesRegistry.kt` around lines
57 - 62, Add a test-overridable provider for the hardcoded exeNameFixes map,
mirroring the existing setFixesProviderForTests pattern and fixesProvider usage
in GameFixesRegistry. Update custom-game executable matching to read through
this provider, while preserving the current map as the production default.
app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java (1)

226-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Local envVars shadows the field this.envVars.

Line 242 declares a new local EnvVars envVars, shadowing the class field mutated just above (lines 229-240). The current code is correct because the field writes explicitly use this., but this pattern is fragile — any future edit inside this method that drops the this. prefix will silently write to the wrong object instead of failing to compile.

Consider renaming the local to avoid the collision (e.g. launchEnvVars) for clarity.

♻️ Suggested rename to eliminate the shadowing
-        EnvVars envVars = new EnvVars();
+        EnvVars launchEnvVars = new EnvVars();

         // Use the ControllerManager's dynamic count for the environment variable
-        envVars.put("EVSHIM_MAX_PLAYERS", String.valueOf(enabledPlayerCount));
-        envVars.put("EVSHIM_SHM_ID", 1);
+        launchEnvVars.put("EVSHIM_MAX_PLAYERS", String.valueOf(enabledPlayerCount));
+        launchEnvVars.put("EVSHIM_SHM_ID", 1);

(and rename remaining envVars uses through the rest of the method accordingly)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java`
around lines 226 - 246, Rename the local EnvVars variable in the launcher method
from envVars to launchEnvVars, updating all subsequent references while
preserving the explicit this.envVars field writes. This removes the field/local
name collision and keeps environment construction behavior unchanged.
app/src/main/java/app/gamenative/ui/screen/settings/ContentsManagerDialog.kt (1)

78-96: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

refreshInstalled() runs blocking syncContents() on the main thread.

refreshInstalled calls mgr.syncContents() synchronously. It's invoked from LaunchedEffect (Line 95, right after the IO sync at Line 94 — a redundant second sync) and from coroutine continuations at Lines 168/241/368/391 that resume on the main dispatcher. That blocks the UI thread on disk I/O and risks an ANR. Consider wrapping the sync/listing in withContext(Dispatchers.IO) and dropping the duplicate call in the LaunchedEffect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/settings/ContentsManagerDialog.kt`
around lines 78 - 96, Move the blocking sync and profile listing performed by
refreshInstalled into withContext(Dispatchers.IO), preserving the existing
exception handling and updating installedProfiles safely after the background
work; then remove the redundant mgr.syncContents() call from
LaunchedEffect(currentType), leaving it to invoke refreshInstalled once.
app/src/main/java/app/gamenative/ui/screen/gamehub/GameHubScreen.kt (1)

473-479: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

sourceLabel hardcodes "Local" — it won't be translated in the Portuguese locale.

This PR adds Portuguese translations, but sourceLabel returns hardcoded English strings. Brand names (Steam, GOG, Epic, Amazon) are typically not localized, but "Local" is a generic word that should use a string resource for consistency with the rest of the localized UI.

♻️ Make sourceLabel composable and use string resources
-private fun sourceLabel(source: GameSource): String = when (source) {
-    GameSource.STEAM -> "Steam"
-    GameSource.CUSTOM_GAME -> "Local"
-    GameSource.GOG -> "GOG"
-    GameSource.EPIC -> "Epic"
-    GameSource.AMAZON -> "Amazon"
+@Composable
+private fun sourceLabel(source: GameSource): String = when (source) {
+    GameSource.STEAM -> "Steam"
+    GameSource.CUSTOM_GAME -> stringResource(R.string.game_hub_local)
+    GameSource.GOG -> "GOG"
+    GameSource.EPIC -> "Epic"
+    GameSource.AMAZON -> "Amazon"
 }

This requires adding game_hub_local to strings.xml and values-pt-rBR/strings.xml, and updating the two call sites (lines 227, 414) which are already inside @Composable scopes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/gamehub/GameHubScreen.kt` around
lines 473 - 479, Update sourceLabel to be composable and return the
game_hub_local string resource for GameSource.CUSTOM_GAME while preserving the
existing brand labels. Add game_hub_local to the default and Portuguese strings
resources, then update both sourceLabel call sites to invoke the composable
function within their existing `@Composable` scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-apk.yml:
- Around line 25-51: Update the cleanup job’s artifact query to process only
artifacts belonging to build-apk.yml runs, using the workflow-specific runs
endpoint or filtering each artifact’s workflow identity before deletion. Make
KEEP_RUNS retrieval fail safely: enable fail-fast behavior and explicitly abort
cleanup when the API call fails or returns an empty list, before iterating over
artifacts. Preserve deletion only for artifacts from non-retained build-apk.yml
runs, leaving artifacts from all other workflows untouched.

In `@app/src/main/java/app/gamenative/gamehub/StoreProvider.kt`:
- Around line 129-148: Change the default values of canInstall, canUpdate, and
canUninstall in StoreCapabilities to false, matching the unsupported default
StoreProvider operations; ensure concrete providers explicitly set these
capabilities to true only when implemented.

In `@app/src/main/java/app/gamenative/MainActivity.kt`:
- Around line 648-649: Update the orientation-selection logic in the relevant
method so ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR is returned only when
conformTo contains all four physical orientations; change the conformTo.size >=
3 branch to preserve partial mixed sets for the manual handling path.

In `@app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt`:
- Around line 1658-1673: Update createDirectoriesAndLinks() to resolve every
manifest-controlled directory and symlink output path through
resolveInsideInstallDir(installDir, relPath) before mkdirs() or
createSymbolicLink(). Skip entries when the helper returns null, and ensure both
the symlink location and any relevant target path cannot escape the install
directory.
- Around line 964-968: Make terminal chunk failure publication thread-safe in
the download assembly flow: replace the plain mutable assemblyFailure variable
with an AtomicReference or CompletableDeferred, and update all worker writes and
the waiting coroutine’s polling/check logic to use it. Preserve only the first
failure, including the fallback exception message, so concurrent Dispatchers.IO
workers cannot overwrite it and the wait loop reliably aborts.

In `@app/src/main/java/app/gamenative/ui/component/dialog/WhatsNewDialog.kt`:
- Around line 41-82: The WhatsNewDialog content is hardcoded in Portuguese and
must be localized. Move the section titles, item strings, and any other
user-facing text from WHATS_NEW_SECTIONS into string resources, keeping default
values in values and Portuguese translations in values-pt-rBR; update
WhatsNewDialog and WhatsNewSection construction to reference resource IDs or
resolved localized strings.

In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt`:
- Around line 739-758: The wallpaper preference values in LibraryScreen are
incorrectly cached by parameterless remember, so updates are not reflected while
the screen remains composed. Remove remember from libBgVideo, libBgImage,
libBgSound, and showLibWallpaper, or replace them with observable preference
state keyed to the relevant values; apply the same fix to the corresponding
wallpaper setup block referenced near the additional occurrence.

In `@app/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.kt`:
- Around line 46-56: Update the retained PlayerView whenever the ExoPlayer
instance is recreated by assigning its player to the newly created exoPlayer in
the relevant DisposableEffect or AndroidView update logic. Ensure the previous
player is detached before release, and the current player is attached after
creation, covering both the initialization and cleanup paths in
LoginBackgroundVideo.

In
`@app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupEmulation.kt`:
- Around line 104-120: Add a catch block to the bulkScope.launch
try/catch/finally surrounding ManifestBulkInstaller.installAll, preventing
exceptions from reaching the uncaught coroutine handler and crashing the app. In
the catch, log or report the failure and show a user-facing Snackbar error using
the existing bulkContext and SnackbarManager, while retaining the finally block
that resets downloadAllProgress.

In
`@app/src/main/java/app/gamenative/ui/screen/settings/WineProtonManagerDialog.kt`:
- Around line 577-723: Extract the duplicated extraction, profile validation,
GLIBC compatibility check, untrusted-file handling, and install-result/error
mapping from downloadAndInstallFromUrl, importLauncher, and
downloadAndInstallWineProton into one shared suspend helper accepting a Uri and
returning the mapped installation result/message. Update all three callers to
use this helper while preserving their download/import-specific setup and UI
state handling, so changes such as the GLIBC gate are maintained in one place.

In `@app/src/main/java/app/gamenative/utils/CoverArtManager.kt`:
- Around line 36-54: Update setCustomCover to preserve the existing cover until
the replacement is fully encoded: write the JPEG to a temporary file in the game
folder, verify compression succeeds, then delete old files and atomically rename
or move the temporary file to cover.jpg. Ensure all failure paths clean up the
temporary file and leave the previous cover untouched.

In `@app/src/main/java/app/gamenative/utils/IntentLaunchManager.kt`:
- Around line 260-264: Update mergeConfigurations to parse intent overrides into
a presence-aware patch rather than relying on default-valued fields. Apply
suspendPolicy and merge boolean fields such as launchRealSteam only when their
keys were explicitly supplied, preserving base values for omitted keys while
honoring explicit false values. Ensure all other existing base fields remain
unchanged.

In `@app/src/main/java/app/gamenative/utils/SessionLogger.kt`:
- Line 31: Make timestamp formatting in Tree.log() thread-safe: do not call the
shared timeFmt.format(nowDate()) outside synchronized(lock). Either move the
formatting call inside the existing lock or replace timeFmt with a
ThreadLocal<SimpleDateFormat> and use its formatter in Tree.log(), preserving
the current timestamp format and log output.

In `@app/src/main/java/com/winlator/container/ContainerManager.java`:
- Around line 210-225: Make config loading a required step in the container
duplication flow. In the duplication method around dstContainer.loadData, treat
null/blank config content, JSON parsing errors, and loadData failures as
duplication failures: log the error, remove the partially created destination
directory using the existing cleanup path, and return without calling saveData()
or adding dstContainer to containers. Preserve the existing successful path and
align failure handling with mkdirs() and FileUtils.copy failures.

In `@app/src/main/java/com/winlator/core/DXVKHelper.java`:
- Around line 118-127: Update deviceMemoryCapMb so its returned cap never
exceeds physical RAM: remove the 2048 MB floor and retain only the
70%-of-totalMb calculation, while preserving the existing zero-value handling
for unavailable or invalid memory data.

In `@app/src/main/res/values/strings.xml`:
- Around line 103-127: Replace the Portuguese literals in the default strings
resource for destination_game_hub, game_hub_title, game_hub_custom_stores,
game_hub_add_store, game_hub_remove_store, and tab_store with English
translations, and add the current Portuguese values to the corresponding keys in
values-pt-rBR/strings.xml.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/gamefixes/GameFixesRegistry.kt`:
- Around line 57-62: Add a test-overridable provider for the hardcoded
exeNameFixes map, mirroring the existing setFixesProviderForTests pattern and
fixesProvider usage in GameFixesRegistry. Update custom-game executable matching
to read through this provider, while preserving the current map as the
production default.

In `@app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt`:
- Around line 64-77: Replace the non-atomic read-modify-write logic in
setFavorite, setLastPlayed, and setConfigurationProfile with
ConcurrentHashMap.compute(gameId) so each update derives from the current value
and writes atomically, preserving concurrent changes to the same game’s metadata
while retaining the existing publish behavior.

In `@app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt`:
- Line 112: Mask the credential in the authToken field by replacing the generic
Field composable with the project’s password/secret input variant, or add
password visual transformation and appropriate keyboard options to Field if
supported. Update the field in CustomStoreDialog so entered API keys are
rendered as bullets while preserving authToken state updates.

In `@app/src/main/java/app/gamenative/ui/screen/gamehub/GameHubScreen.kt`:
- Around line 473-479: Update sourceLabel to be composable and return the
game_hub_local string resource for GameSource.CUSTOM_GAME while preserving the
existing brand labels. Add game_hub_local to the default and Portuguese strings
resources, then update both sourceLabel call sites to invoke the composable
function within their existing `@Composable` scopes.

In
`@app/src/main/java/app/gamenative/ui/screen/settings/ContentsManagerDialog.kt`:
- Around line 78-96: Move the blocking sync and profile listing performed by
refreshInstalled into withContext(Dispatchers.IO), preserving the existing
exception handling and updating installedProfiles safely after the background
work; then remove the redundant mgr.syncContents() call from
LaunchedEffect(currentType), leaving it to invoke refreshInstalled once.

In
`@app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt`:
- Around line 212-230: Handle failure from takePersistableUriPermission in the
loginBgVideoPicker callback by warning the user and avoiding
persistence/enabling when permission retention fails; only update
loginBgVideoUri, PrefManager values, and show the success Snackbar after
permission succeeds, while retaining Timber logging for diagnostics.

In
`@app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java`:
- Around line 226-246: Rename the local EnvVars variable in the launcher method
from envVars to launchEnvVars, updating all subsequent references while
preserving the explicit this.envVars field writes. This removes the field/local
name collision and keeps environment construction behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57d8f345-4cad-4ae0-b537-b5d0fcf93c33

📥 Commits

Reviewing files that changed from the base of the PR and between b6fd912 and 65d477f.

📒 Files selected for processing (124)
  • .github/workflows/app-release-signed.yml
  • .github/workflows/build-apk.yml
  • .github/workflows/pluvia-pr-check.yml
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/assets/box64_env_vars.json
  • app/src/main/cpp/asurfacerenderer/drawable.c
  • app/src/main/cpp/patchelf/CMakeLists.txt
  • app/src/main/cpp/proot/CMakeLists.txt
  • app/src/main/cpp/virglrenderer/CMakeLists.txt
  • app/src/main/cpp/winlator/xconnector_epoll.c
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/SteamBootstrap.kt
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/di/GameHubModule.kt
  • app/src/main/java/app/gamenative/events/EventDispatcher.kt
  • app/src/main/java/app/gamenative/gamefixes/GameFixesRegistry.kt
  • app/src/main/java/app/gamenative/gamefixes/STEAM_1888930.kt
  • app/src/main/java/app/gamenative/gamehub/DataStoreGameLibraryRepository.kt
  • app/src/main/java/app/gamenative/gamehub/DelegatingStoreProvider.kt
  • app/src/main/java/app/gamenative/gamehub/GameHubMappers.kt
  • app/src/main/java/app/gamenative/gamehub/GameHubRegistrar.kt
  • app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt
  • app/src/main/java/app/gamenative/gamehub/GameModel.kt
  • app/src/main/java/app/gamenative/gamehub/GameModelMapper.kt
  • app/src/main/java/app/gamenative/gamehub/README.md
  • app/src/main/java/app/gamenative/gamehub/StoreManager.kt
  • app/src/main/java/app/gamenative/gamehub/StoreProvider.kt
  • app/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.kt
  • app/src/main/java/app/gamenative/gamehub/custom/CustomStoreRepository.kt
  • app/src/main/java/app/gamenative/lan/InGameLanChatOverlay.kt
  • app/src/main/java/app/gamenative/lan/LanRoomDialog.kt
  • app/src/main/java/app/gamenative/lan/LanRoomManager.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/amazon/AmazonDownloadManager.kt
  • app/src/main/java/app/gamenative/service/amazon/AmazonSdkManager.kt
  • app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
  • app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ControllersDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/WhatsNewDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/WineTab.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/ui/enums/HomeDestination.kt
  • app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt
  • app/src/main/java/app/gamenative/ui/model/GameHubViewModel.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/HomeScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/gamehub/GameHubScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryBackground.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt
  • app/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.kt
  • app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/ContentsManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/DriverManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupDebug.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupEmulation.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/WineProtonManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/AssetUtils.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/CoverArtManager.kt
  • app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
  • app/src/main/java/app/gamenative/utils/CustomGameWatcher.kt
  • app/src/main/java/app/gamenative/utils/DiagnosticsAnalyzer.kt
  • app/src/main/java/app/gamenative/utils/IntentLaunchManager.kt
  • app/src/main/java/app/gamenative/utils/ManifestBulkInstaller.kt
  • app/src/main/java/app/gamenative/utils/ManifestComponentHelper.kt
  • app/src/main/java/app/gamenative/utils/PerformanceGovernor.kt
  • app/src/main/java/app/gamenative/utils/RuntimeCompatibility.kt
  • app/src/main/java/app/gamenative/utils/SessionLogger.kt
  • app/src/main/java/app/gamenative/utils/SteamGridDB.kt
  • app/src/main/java/app/gamenative/utils/TelemetryCollector.kt
  • app/src/main/java/com/winlator/box86_64/Box86_64PresetManager.java
  • app/src/main/java/com/winlator/box86_64/rc/RCField.java
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/container/ContainerManager.java
  • app/src/main/java/com/winlator/contents/ContentsManager.java
  • app/src/main/java/com/winlator/core/DXVKHelper.java
  • app/src/main/java/com/winlator/core/DefaultVersion.java
  • app/src/main/java/com/winlator/core/FileUtils.java
  • app/src/main/java/com/winlator/core/ProcessHelper.java
  • app/src/main/java/com/winlator/core/TarCompressorUtils.java
  • app/src/main/java/com/winlator/core/WineUtils.java
  • app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt
  • app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java
  • app/src/main/java/com/winlator/widget/TouchpadView.java
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/com/winlator/xconnector/XConnectorEpoll.java
  • app/src/main/java/com/winlator/xenvironment/ImageFs.java
  • app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java
  • app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java
  • app/src/main/java/com/winlator/xserver/Drawable.java
  • app/src/main/java/com/winlator/xserver/XClientRequestHandler.java
  • app/src/main/java/com/winlator/xserver/requests/DrawRequests.java
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/app/gamenative/gamehub/GameHubMappersTest.kt
  • app/src/test/java/app/gamenative/gamehub/GameModelMapperTest.kt
  • app/src/test/java/app/gamenative/gamehub/StoreManagerTest.kt
  • app/src/test/java/app/gamenative/utils/PerformanceGovernorTest.kt
  • app/src/test/java/com/winlator/box86_64/Box86_64PresetManagerTest.kt
  • app/src/test/java/com/winlator/container/ContainerPersistenceTest.kt
  • app/src/test/java/com/winlator/core/ProcessHelperAffinityTest.kt
  • docs/RELATORIO_ENGENHARIA_2026-07-09.md
  • docs/RELATORIO_REVISAO_2026-07.md
  • docs/SERVIDOR_GAMENATIVE_ANALISE.md
  • docs/XODOS_ANALISE.md

Comment on lines +25 to +51
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Cleanup job deletes artifacts from ALL workflows, not just build-apk.yml

Two issues in the cleanup job:

  1. Cross-workflow deletion (critical): Line 39 lists ALL repository artifacts via repos/$GH_REPO/actions/artifacts, but KEEP_RUNS (line 36) only contains run IDs from the build-apk.yml workflow. Artifacts from other workflows (e.g., app-release-signed.yml uploads signed APKs) have different run IDs, so KEEP stays false and they get deleted.

  2. Empty KEEP_RUNS on API failure (major): With set -uo pipefail but no -e (line 35), a gh api failure (rate limit, network) leaves KEEP_RUNS empty. The for K in $KEEP_RUNS loop never executes, so every artifact gets KEEP=false and is deleted — wiping the entire repository's artifact storage.

🔧 Proposed fix: filter by workflow and guard against empty keep list
 jobs:
   cleanup:
     runs-on: ubuntu-latest
     permissions:
       actions: write
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       GH_REPO: ${{ github.repository }}
     steps:
       - name: Delete artifacts older than the last 4 runs
         run: |
           set -uo pipefail
+
+          # Get the workflow ID so we only touch build-apk artifacts, not other workflows'.
+          WF_ID=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml" -q '.id')
+          if [ -z "$WF_ID" ]; then
+            echo "Could not resolve workflow ID, skipping cleanup"
+            exit 0
+          fi
+
           KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
             -q '.workflow_runs[].id' | tr '\n' ' ')
+          if [ -z "$KEEP_RUNS" ]; then
+            echo "Failed to fetch keep runs, skipping cleanup to avoid deleting everything"
+            exit 0
+          fi
           echo "Keeping artifacts of runs: $KEEP_RUNS"
-          gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-            -q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
+          gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
+            -q ".artifacts[] | select(.workflow_run.workflow_id == $WF_ID) | \"\(.id) \(.workflow_run.id) \(.size_in_bytes)\"" |
           while read -r ART_ID RUN_ID SIZE; do
             KEEP=false
             for K in $KEEP_RUNS; do
               if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
             done
             if [ "$KEEP" = "false" ]; then
               echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
               gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
             fi
           done
           echo "Cleanup done."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
# Get the workflow ID so we only touch build-apk artifacts, not other workflows'.
WF_ID=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml" -q '.id')
if [ -z "$WF_ID" ]; then
echo "Could not resolve workflow ID, skipping cleanup"
exit 0
fi
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
if [ -z "$KEEP_RUNS" ]; then
echo "Failed to fetch keep runs, skipping cleanup to avoid deleting everything"
exit 0
fi
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q ".artifacts[] | select(.workflow_run.workflow_id == $WF_ID) | \"\(.id) \(.workflow_run.id) \(.size_in_bytes)\"" |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-apk.yml around lines 25 - 51, Update the cleanup
job’s artifact query to process only artifacts belonging to build-apk.yml runs,
using the workflow-specific runs endpoint or filtering each artifact’s workflow
identity before deletion. Make KEEP_RUNS retrieval fail safely: enable fail-fast
behavior and explicitly abort cleanup when the API call fails or returns an
empty list, before iterating over artifacts. Preserve deletion only for
artifacts from non-retained build-apk.yml runs, leaving artifacts from all other
workflows untouched.

Comment on lines +129 to +148
/** Declares which optional operations a [StoreProvider] actually supports. */
data class StoreCapabilities(
val canSearch: Boolean = false,
val canInstall: Boolean = true,
val canUpdate: Boolean = true,
val canUninstall: Boolean = true,
val canImportExisting: Boolean = false,
val hasCloudSaves: Boolean = false,
val requiresAuth: Boolean = true,
/** Supports interactive login/logout (vs. auth handled elsewhere). */
val canLogin: Boolean = false,
/** Exposes a user profile (name/avatar). */
val hasProfile: Boolean = false,
/** Supports pause/resume/cancel of downloads. */
val canControlDownloads: Boolean = false,
/** Supports verifying installed files. */
val canVerify: Boolean = false,
/** Supports repairing installed files. */
val canRepair: Boolean = false,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not advertise unsupported operations by default.

StoreCapabilities() claims install/update/uninstall support, while the matching default provider methods are unsupported (or return a misleading UP_TO_DATE). Any provider using default capabilities can expose actions that cannot work. Default optional capabilities to false; concrete providers should opt in explicitly.

Proposed fix
 data class StoreCapabilities(
     val canSearch: Boolean = false,
-    val canInstall: Boolean = true,
-    val canUpdate: Boolean = true,
-    val canUninstall: Boolean = true,
+    val canInstall: Boolean = false,
+    val canUpdate: Boolean = false,
+    val canUninstall: Boolean = false,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Declares which optional operations a [StoreProvider] actually supports. */
data class StoreCapabilities(
val canSearch: Boolean = false,
val canInstall: Boolean = true,
val canUpdate: Boolean = true,
val canUninstall: Boolean = true,
val canImportExisting: Boolean = false,
val hasCloudSaves: Boolean = false,
val requiresAuth: Boolean = true,
/** Supports interactive login/logout (vs. auth handled elsewhere). */
val canLogin: Boolean = false,
/** Exposes a user profile (name/avatar). */
val hasProfile: Boolean = false,
/** Supports pause/resume/cancel of downloads. */
val canControlDownloads: Boolean = false,
/** Supports verifying installed files. */
val canVerify: Boolean = false,
/** Supports repairing installed files. */
val canRepair: Boolean = false,
)
/** Declares which optional operations a [StoreProvider] actually supports. */
data class StoreCapabilities(
val canSearch: Boolean = false,
val canInstall: Boolean = false,
val canUpdate: Boolean = false,
val canUninstall: Boolean = false,
val canImportExisting: Boolean = false,
val hasCloudSaves: Boolean = false,
val requiresAuth: Boolean = true,
/** Supports interactive login/logout (vs. auth handled elsewhere). */
val canLogin: Boolean = false,
/** Exposes a user profile (name/avatar). */
val hasProfile: Boolean = false,
/** Supports pause/resume/cancel of downloads. */
val canControlDownloads: Boolean = false,
/** Supports verifying installed files. */
val canVerify: Boolean = false,
/** Supports repairing installed files. */
val canRepair: Boolean = false,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/gamehub/StoreProvider.kt` around lines 129 -
148, Change the default values of canInstall, canUpdate, and canUninstall in
StoreCapabilities to false, matching the unsupported default StoreProvider
operations; ensure concrete providers explicitly set these capabilities to true
only when implemented.

Comment on lines +648 to +649
conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not widen three-orientation sets to FULL_SENSOR.

A set such as landscape + reverse-landscape + portrait excludes reverse-portrait, but FULL_SENSOR allows it. Only use FULL_SENSOR when all four physical orientations are allowed; leave partial mixed sets on the manual path.

Proposed fix
-            conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
+            conformTo.containsAll(
+                EnumSet.of(
+                    Orientation.LANDSCAPE,
+                    Orientation.REVERSE_LANDSCAPE,
+                    Orientation.PORTRAIT,
+                    Orientation.REVERSE_PORTRAIT,
+                ),
+            ) -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> null
conformTo.containsAll(
EnumSet.of(
Orientation.LANDSCAPE,
Orientation.REVERSE_LANDSCAPE,
Orientation.PORTRAIT,
Orientation.REVERSE_PORTRAIT,
),
) -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/MainActivity.kt` around lines 648 - 649,
Update the orientation-selection logic in the relevant method so
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR is returned only when conformTo
contains all four physical orientations; change the conformTo.size >= 3 branch
to preserve partial mixed sets for the manual handling path.

Comment on lines +964 to +968
// Record the failure so the wait loop below aborts instead of
// spinning forever: pendingChunks is never decremented for a
// permanently-failed chunk and the stuck-detector would re-emit it
// endlessly, hanging the whole download.
assemblyFailure = exception ?: Exception("Chunk $chunkMd5 failed permanently")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Publish terminal chunk failures safely across coroutines.

assemblyFailure is a plain mutable variable written by concurrent Dispatchers.IO workers and polled by the waiting coroutine. The poller may never observe the write, reintroducing the download hang this change intends to fix. Use an AtomicReference or CompletableDeferred and preserve the first failure.

Proposed fix
+import java.util.concurrent.atomic.AtomicReference
...
-            var assemblyFailure: Throwable? = null
+            val assemblyFailure = AtomicReference<Throwable?>(null)
...
-                                assemblyFailure = exception ?: Exception("Chunk $chunkMd5 failed permanently")
+                                assemblyFailure.compareAndSet(
+                                    null,
+                                    exception ?: Exception("Chunk $chunkMd5 failed permanently"),
+                                )
...
-                if (assemblyFailure != null) {
+                assemblyFailure.get()?.let { failure ->
                     networkChunkJob.cancel()
                     assembleJob.cancel()
-                    return@withContext Result.failure(assemblyFailure!!)
+                    return@withContext Result.failure(failure)
                 }

Also applies to: 1037-1043

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt` around
lines 964 - 968, Make terminal chunk failure publication thread-safe in the
download assembly flow: replace the plain mutable assemblyFailure variable with
an AtomicReference or CompletableDeferred, and update all worker writes and the
waiting coroutine’s polling/check logic to use it. Preserve only the first
failure, including the fallback exception message, so concurrent Dispatchers.IO
workers cannot overwrite it and the wait loop reliably aborts.

Comment on lines +1658 to +1673
/**
* Resolve [relativePath] (which comes from the untrusted GOG manifest) against [installDir]
* and verify the result stays inside [installDir]. A manifest entry like "../../../foo" would
* otherwise let a compromised/malicious depot write arbitrary files outside the game directory
* (path traversal / "zip slip"). Returns null if the path escapes, so callers can skip it.
*/
private fun resolveInsideInstallDir(installDir: File, relativePath: String): File? {
val installRoot = installDir.canonicalPath
val resolved = File(installDir, relativePath).canonicalFile
return if (resolved.path == installRoot || resolved.path.startsWith(installRoot + File.separator)) {
resolved
} else {
Timber.tag("GOG").e("Refusing manifest path outside install dir: %s", relativePath)
null
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply containment validation to manifest directories and symlink locations.

The helper protects assembled files, but createDirectoriesAndLinks() still constructs File(installDir, relPath) directly for manifest-controlled directory and link paths. A ../../ link or directory path can still escape the game directory. Resolve those output paths through this helper before mkdirs() or createSymbolicLink().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt` around
lines 1658 - 1673, Update createDirectoriesAndLinks() to resolve every
manifest-controlled directory and symlink output path through
resolveInsideInstallDir(installDir, relPath) before mkdirs() or
createSymbolicLink(). Skip entries when the helper returns null, and ensure both
the symlink location and any relevant target path cannot escape the install
directory.

Comment on lines +260 to +264
// Start from `base` so every field NOT explicitly merged below keeps the base container's
// real value. Building a fresh ContainerData(...) instead would silently reset every
// unlisted field (containerVariant, wineVersion, emulator, renderer, fexcore*, etc.) to the
// constructor default.
return base.copy(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve override field presence instead of inferring it from defaults.

A JSON suspendPolicy is parsed but never applied (mergeConfigurations always keeps the base value); explicit false values for fields such as launchRealSteam are likewise indistinguishable from omitted fields. Parse into a presence-aware patch and merge only keys actually supplied by the intent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/utils/IntentLaunchManager.kt` around lines
260 - 264, Update mergeConfigurations to parse intent overrides into a
presence-aware patch rather than relying on default-valued fields. Apply
suspendPolicy and merge boolean fields such as launchRealSteam only when their
keys were explicitly supplied, preserving base values for omitted keys while
honoring explicit false values. Ensure all other existing base fields remain
unchanged.

private const val MAX_FILES = 4 // session.log + .1 + .2 + .3 → ~4 MB ceiling

private val lock = Any()
private val timeFmt = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

SimpleDateFormat is not thread-safe; Tree.log() formats timestamps outside the lock.

timeFmt is a shared SimpleDateFormat instance. Tree.log() calls timeFmt.format(nowDate()) at line 109 inside buildString{} but outside synchronized(lock). Timber invokes log() from whatever thread logs the message, so concurrent calls can corrupt SimpleDateFormat's internal CalendarBuilder/DigitList state, causing ArrayIndexOutOfBoundsException, NumberFormatException, or garbled timestamps.

🔒 Minimal fix: synchronize the format call
     override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
         val line = buildString {
-            append(timeFmt.format(nowDate()))
+            append(synchronized(lock) { timeFmt.format(nowDate()) })
             append(' ')
             append(levelChar(priority))
             append('/')
             append(tag ?: "app")
             append(": ")
             append(message)
         }
         append(line)
         if (t != null) append(android.util.Log.getStackTraceString(t))
     }

Alternatively, replace timeFmt with a ThreadLocal<SimpleDateFormat> to avoid lock contention on the hot path.

Also applies to: 107-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/utils/SessionLogger.kt` at line 31, Make
timestamp formatting in Tree.log() thread-safe: do not call the shared
timeFmt.format(nowDate()) outside synchronized(lock). Either move the formatting
call inside the existing lock or replace timeFmt with a
ThreadLocal<SimpleDateFormat> and use its formatter in Tree.log(), preserving
the current timestamp format and log output.

Comment on lines +210 to 225

// The source root dir (including its .container config file) was copied above.
// Load the full copied config so every setting is preserved, instead of
// copying a hand-picked subset of fields.
try {
String configContent = FileUtils.readString(dstContainer.getConfigFile());
if (configContent != null && !configContent.trim().isEmpty()) {
JSONObject data = new JSONObject(configContent);
data.put("id", newId);
dstContainer.loadData(data);
}
} catch (Exception e) {
Log.w("ContainerManager", "Could not load config of duplicated container " + newId + ": " + e.getMessage());
}

dstContainer.setName(srcContainer.getName()+" ("+context.getString(R.string.copy)+")");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Duplication silently succeeds with an unconfigured container on config-load failure.

If the copied config is empty/null or fails to parse, the catch/empty-content branch just logs a warning and falls through — dstContainer keeps none of srcContainer's settings (wine version, box64 preset, drives, etc.), yet the method still calls saveData() and adds it to containers. Earlier failure points in this same method (mkdirs() failing, FileUtils.copy failing) instead abort and clean up the partial directory; this path breaks that consistency and can leave a broken, unusable duplicated container silently visible to the user.

🐛 Suggested fix: abort duplication on config-load failure
         try {
             String configContent = FileUtils.readString(dstContainer.getConfigFile());
-            if (configContent != null && !configContent.trim().isEmpty()) {
-                JSONObject data = new JSONObject(configContent);
-                data.put("id", newId);
-                dstContainer.loadData(data);
-            }
+            if (configContent == null || configContent.trim().isEmpty()) {
+                Log.w("ContainerManager", "Duplicated container config is empty for " + newId + ", aborting");
+                FileUtils.delete(dstDir);
+                return;
+            }
+            JSONObject data = new JSONObject(configContent);
+            data.put("id", newId);
+            dstContainer.loadData(data);
         } catch (Exception e) {
             Log.w("ContainerManager", "Could not load config of duplicated container " + newId + ": " + e.getMessage());
+            FileUtils.delete(dstDir);
+            return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The source root dir (including its .container config file) was copied above.
// Load the full copied config so every setting is preserved, instead of
// copying a hand-picked subset of fields.
try {
String configContent = FileUtils.readString(dstContainer.getConfigFile());
if (configContent != null && !configContent.trim().isEmpty()) {
JSONObject data = new JSONObject(configContent);
data.put("id", newId);
dstContainer.loadData(data);
}
} catch (Exception e) {
Log.w("ContainerManager", "Could not load config of duplicated container " + newId + ": " + e.getMessage());
}
dstContainer.setName(srcContainer.getName()+" ("+context.getString(R.string.copy)+")");
// The source root dir (including its .container config file) was copied above.
// Load the full copied config so every setting is preserved, instead of
// copying a hand-picked subset of fields.
try {
String configContent = FileUtils.readString(dstContainer.getConfigFile());
if (configContent == null || configContent.trim().isEmpty()) {
Log.w("ContainerManager", "Duplicated container config is empty for " + newId + ", aborting");
FileUtils.delete(dstDir);
return;
}
JSONObject data = new JSONObject(configContent);
data.put("id", newId);
dstContainer.loadData(data);
} catch (Exception e) {
Log.w("ContainerManager", "Could not load config of duplicated container " + newId + ": " + e.getMessage());
FileUtils.delete(dstDir);
return;
}
dstContainer.setName(srcContainer.getName()+" ("+context.getString(R.string.copy)+")");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/winlator/container/ContainerManager.java` around lines
210 - 225, Make config loading a required step in the container duplication
flow. In the duplication method around dstContainer.loadData, treat null/blank
config content, JSON parsing errors, and loadData failures as duplication
failures: log the error, remove the partially created destination directory
using the existing cleanup path, and return without calling saveData() or adding
dstContainer to containers. Preserve the existing successful path and align
failure handling with mkdirs() and FileUtils.copy failures.

Comment on lines +118 to +127
/** ~70% of physical RAM in MB (floor 2048), or 0 if it can't be read. */
private static long deviceMemoryCapMb(Context context) {
try {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) return 0;
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
long totalMb = mi.totalMem / (1024L * 1024L);
if (totalMb <= 0) return 0;
return Math.max(2048L, totalMb * 70 / 100);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Never report more GPU memory than physical RAM.

The 2048 MB floor makes a 1 GB device advertise 2 GB for both DXGI memory limits, encouraging guest allocations the device cannot back. Remove the floor or retain the 70% cap without exceeding totalMb.

Proposed fix
-            return Math.max(2048L, totalMb * 70 / 100);
+            return totalMb * 70 / 100;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** ~70% of physical RAM in MB (floor 2048), or 0 if it can't be read. */
private static long deviceMemoryCapMb(Context context) {
try {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) return 0;
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
long totalMb = mi.totalMem / (1024L * 1024L);
if (totalMb <= 0) return 0;
return Math.max(2048L, totalMb * 70 / 100);
/** ~70% of physical RAM in MB (floor 2048), or 0 if it can't be read. */
private static long deviceMemoryCapMb(Context context) {
try {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) return 0;
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
long totalMb = mi.totalMem / (1024L * 1024L);
if (totalMb <= 0) return 0;
return totalMb * 70 / 100;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/winlator/core/DXVKHelper.java` around lines 118 - 127,
Update deviceMemoryCapMb so its returned cap never exceeds physical RAM: remove
the 2048 MB floor and retain only the 70%-of-totalMb calculation, while
preserving the existing zero-value handling for unavailable or invalid memory
data.

Comment on lines +103 to +127
<string name="destination_game_hub">Loja</string>
<string name="game_hub_title">Loja</string>
<string name="game_hub_all_sources">All stores</string>
<string name="game_hub_filter_all">All</string>
<string name="game_hub_filter_installed">Installed</string>
<string name="game_hub_filter_not_installed">Not installed</string>
<string name="game_hub_search">Search games</string>
<string name="game_hub_empty">No games yet. Connect a store or refresh to load your library.</string>
<string name="game_hub_refresh">Refresh libraries</string>
<string name="game_hub_game_count">%1$d games</string>
<string name="game_hub_tab_library">Library</string>
<string name="game_hub_tab_stores">Stores</string>
<string name="game_hub_store_connected">Connected</string>
<string name="game_hub_store_disconnected">Not connected</string>
<string name="game_hub_store_connecting">Connecting…</string>
<string name="game_hub_sort_name">Name</string>
<string name="game_hub_sort_store">Store</string>
<string name="game_hub_sort_recent">Recent</string>
<string name="game_hub_count">%1$d of %2$d</string>
<string name="game_hub_favorites">Favorites</string>
<string name="game_hub_favorite_add">Add to favorites</string>
<string name="game_hub_favorite_remove">Remove from favorites</string>
<string name="game_hub_custom_stores">Lojas personalizadas</string>
<string name="game_hub_add_store">Adicionar loja</string>
<string name="game_hub_remove_store">Remover loja</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Portuguese literals in the default (English) resource file.

Several new default values are Portuguese rather than English: destination_game_hub/game_hub_title = "Loja" (Lines 103‑104), game_hub_custom_stores = "Lojas personalizadas", game_hub_add_store = "Adicionar loja", game_hub_remove_store = "Remover loja" (Lines 125‑127), and tab_store = "Loja" (Line 135). Users on any non‑pt‑BR locale will see Portuguese here. Put English in the default file and the Portuguese in values-pt-rBR/strings.xml.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values/strings.xml` around lines 103 - 127, Replace the
Portuguese literals in the default strings resource for destination_game_hub,
game_hub_title, game_hub_custom_stores, game_hub_add_store,
game_hub_remove_store, and tab_store with English translations, and add the
current Portuguese values to the corresponding keys in
values-pt-rBR/strings.xml.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 issues found across 124 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/res/values/strings.xml">

<violation number="1" location="app/src/main/res/values/strings.xml:103">
P0: Several new strings use Portuguese values in the default locale (`res/values/strings.xml`), which is the fallback locale for all languages. This means users of any language other than pt-BR will see "Loja", "Lojas personalizadas", "Adicionar loja", "Remover loja" instead of English text. The existing pt-BR file at `values-pt-rBR/strings.xml` already contains translations for other strings — these should use English here and be added to the pt-BR file instead.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.kt:66">
P1: API tokens are persisted in plaintext as part of the config blob, so a local data extraction exposes credentials for third-party store accounts. Keeping `authToken` out of the JSON payload and storing it via encrypted storage (Keystore-backed) would reduce credential leakage risk.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.kt:113">
P2: Malformed config JSON is silently treated as an empty list, which can turn a transient parse issue into permanent data loss on the next save/remove. Propagating or surfacing parse errors (instead of defaulting to `emptyList`) would prevent accidental wipe of user store configurations.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/ManifestBulkInstaller.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/ManifestBulkInstaller.kt:68">
P1: Bulk install can ignore coroutine cancellation and keep downloading/installing entries after the caller is cancelled. This comes from wrapping `installManifestEntry` in `runCatching` without rethrowing `CancellationException`; consider rethrowing cancellation before counting it as a normal failure.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.kt:97">
P1: Changing videoUri or soundOn can leave the background view attached to a stale/released ExoPlayer, so the new media state may not render or play. This happens because player assignment is done only in AndroidView.factory; adding an update block to rebind `player` each recomposition would keep PlayerView in sync.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt:65">
P1: Concurrent metadata updates can drop previously written fields for the same game. The current `ConcurrentHashMap` usage is thread-safe per operation, but each setter performs a non-atomic read-modify-write; using `compute` (or another atomic update path) for each setter would preserve all fields under races.</violation>
</file>

<file name="app/src/main/java/com/winlator/contents/ContentsManager.java">

<violation number="1" location="app/src/main/java/com/winlator/contents/ContentsManager.java:574">
P1: If entryName is null the method NPEs on entryName.toLowerCase() despite the new null guard. The guard only covers the (Default) stripping step, not the rest of the method. Add an early return when entryName is null to make the guard complete.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/DelegatingStoreProvider.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/DelegatingStoreProvider.kt:47">
P1: Authentication cancellation can be misreported as an auth error because `runCatching` also catches coroutine cancellation. Using explicit `try/catch` and rethrowing `CancellationException` keeps structured concurrency and avoids false `StoreConnectionState.Error` updates.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupEmulation.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupEmulation.kt:109">
P1: Bulk-download can crash instead of showing a failure state when manifest loading throws, because the new `try/finally` clears UI state but does not catch exceptions from `ManifestBulkInstaller.installAll`. Handling the failure path locally (and returning early) would keep Settings responsive and provide user feedback.</violation>
</file>

<file name="app/src/main/java/app/gamenative/MainActivity.kt">

<violation number="1" location="app/src/main/java/app/gamenative/MainActivity.kt:648">
P1: Orientation constraints can be broadened incorrectly for mixed 3-orientation sets, so the app may rotate into a disallowed orientation. This comes from mapping any `conformTo.size >= 3` to `SCREEN_ORIENTATION_FULL_SENSOR` before the manual allowlist-based selection runs.</violation>
</file>

<file name="app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt">

<violation number="1" location="app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt:231">
P2: The new `FD_DEV_FEATURES` entry will not be used because a second `FD_DEV_FEATURES` key exists later in the same `KNOWN_ENV_VARS` map and overrides it. This can mislead users and future maintainers about which selection type/value set is actually active; consider keeping only one definition.</violation>

<violation number="2" location="app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt:274">
P2: `VKD3D_CONFIG` is defined twice in the same map, so the later existing entry overrides this newly added one. The UI/validation behavior from this block won’t apply, which can silently break the intended per-flag tuning.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt:42">
P2: The API token is persisted in saveable UI state, so sensitive auth data survives process recreation in plain saved-state flow. Using non-saveable state for `authToken` avoids persisting secrets beyond the current session.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.kt:137">
P2: Extra headers cannot be entered one-per-line as the UI text describes, because the shared `Field` forces a single-line text box for all inputs. Consider supporting multiline for `extraHeaders` (e.g., a `singleLine = false` path) so this config format is actually editable.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/DataStoreGameLibraryRepository.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/DataStoreGameLibraryRepository.kt:24">
P2: Metadata reads can fail hard on transient DataStore read errors because `data` is consumed without `catch`, so the flow may terminate and `get()` may throw instead of returning fallback state. Consider handling `IOException` on reads (emit empty preferences, rethrow others) before parsing.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/gamehub/DataStoreGameLibraryRepository.kt:77">
P1: A parse failure currently collapses to `emptyMap()`, which silently drops all favorites/last-played/profile metadata instead of isolating the bad data. This makes one malformed payload a full metadata reset risk; consider per-entry recovery or explicit corruption handling instead of blanket empty fallback.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/TelemetryCollector.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/TelemetryCollector.kt:62">
P2: A quick start/stop can leave a false `.running` marker because marker creation happens after session state is already cleared. Guarding marker write with the current session check avoids recording crashes for sessions that already stopped.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/utils/TelemetryCollector.kt:104">
P1: Crash telemetry can overcount because `.running` deletion is asynchronous and may not execute before process teardown. Moving marker cleanup to a synchronous step in `stop` would keep crash detection aligned with real unclean exits.</violation>
</file>

<file name="app/src/main/java/com/winlator/xserver/XClientRequestHandler.java">

<violation number="1" location="app/src/main/java/com/winlator/xserver/XClientRequestHandler.java:479">
P1: When malformed-request recovery fails, this path still reports the request as handled, so the epoll loop can keep retrying the same broken input instead of terminating that client cleanly. Propagating an `IOException` here would let `XConnectorEpoll` kill only the offending connection and avoid repeated failure loops.</violation>
</file>

<file name="app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/gog/GOGDownloadManager.kt:1664">
P1: The new `resolveInsideInstallDir` method guards file writes against path traversal, but `createDirectoriesAndLinks` (which also processes untrusted manifest paths) still uses `File(installDir, relPath)` directly for both directories and symlinks. A malicious manifest with `../../malicious` as a directory or link path bypasses this protection. Consider applying `resolveInsideInstallDir` consistently to all manifest-derived paths, including in `createDirectoriesAndLinks`.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt:742">
P2: Library wallpaper settings can appear stuck after users change them in Layout options, because the new `PrefManager.libraryBackground*` reads are wrapped in `remember` and stop updating during recomposition. Reading these values directly each recomposition (or from observable state) keeps wallpaper visibility/media/sound in sync immediately.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/GameHubRegistrar.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/GameHubRegistrar.kt:126">
P2: Refreshing stores can leave Local games stale in the unified library because the local provider’s refresh lambda does not update the provider flow. Using a mutable flow/state-backed source for `libraryItems` and updating it inside `onRefresh` would make refresh actually propagate new local scan results.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/StoreProvider.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/StoreProvider.kt:132">
P2: Stores wired with `StoreCapabilities(canSearch = ..., requiresAuth = true)` will now advertise install/uninstall support even though `StoreProvider.install`/`uninstall` default to `notSupported`, so capability-gated UI can expose actions that always fail at runtime. Default these capability flags to `false` so support is opt-in per provider.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/SessionLogger.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/SessionLogger.kt:109">
P2: Concurrent logging can intermittently corrupt timestamps or crash because `Tree.log` formats dates through a shared `SimpleDateFormat` without synchronization. Using a synchronized access (or per-call formatter) keeps log formatting thread-safe.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/CoverArtManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/CoverArtManager.kt:41">
P2: Changing cover can permanently clear the existing custom image when writing the new JPEG fails, because old `cover*` files are deleted first. Writing to a temp file and only replacing old files after a successful encode would preserve the previous cover on failure.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt:248">
P2: Wine selection can revert other settings changed after this composable rendered, especially when install completes asynchronously, because the update copies from a stale captured `config` snapshot. Using the latest `state.config.value` inside `applyWineSelection` avoids clobbering unrelated fields.</violation>
</file>

<file name="app/src/main/java/com/winlator/core/DXVKHelper.java">

<violation number="1" location="app/src/main/java/com/winlator/core/DXVKHelper.java:29">
P2: Mesa shader caching will be pinned to the wrong directory in `release-gold` builds because this line hardcodes the base package data path. Using `imageFs.cache_path` keeps the cache path aligned with the current `applicationId` and the already discovered `ImageFs` root.</violation>

<violation number="2" location="app/src/main/java/com/winlator/core/DXVKHelper.java:115">
P2: VKD3D shader caching will be pinned to the base app's private directory in `release-gold` builds, so D3D12 titles won't use the intended cache. Deriving the path from `ImageFs.find(context)` avoids package-name drift across build variants.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/GameHubMappers.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/GameHubMappers.kt:49">
P2: Epic game identity can drift across syncs because the mapper uses the local Room `id` instead of Epic’s stable external key. Using `catalogId` for `GameModel.id` would keep favorites/metadata associations stable when rows are recreated.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/gamehub/GameHubMappers.kt:65">
P2: Amazon game identity is currently tied to local DB `appId`, which can change when rows are recreated during library refresh. Building the hub ID from stable `productId` avoids metadata/favorite mismatches for the same entitlement.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/StoreManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/StoreManager.kt:49">
P2: `register()` can ignore coroutine cancellation during provider initialization, so startup/shutdown cancellation may not stop registration work. This happens because `runCatching` catches `CancellationException` and the result is dropped; rethrow cancellation and only handle non-cancellation failures.</violation>
</file>

<file name="app/src/main/java/app/gamenative/gamehub/GameModel.kt">

<violation number="1" location="app/src/main/java/app/gamenative/gamehub/GameModel.kt:49">
P2: Installed games with pending updates can be treated as not installed, which can break installed filters and UI state during migration. This comes from `isInstalled` only checking `InstallState.INSTALLED`; considering `UPDATE_AVAILABLE` as installed would keep behavior consistent.</violation>
</file>

<file name="app/src/main/java/app/gamenative/service/amazon/AmazonSdkManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/amazon/AmazonSdkManager.kt:92">
P2: The traversal guard still accepts paths that resolve to the SDK root itself, so a malformed manifest entry can target the cache directory as a file destination. Consider rejecting anything that is not a strict child of `sdkRoot`, and handling canonicalization failures per-file so one bad path does not abort the whole download.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryBackground.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryBackground.kt:102">
P2: Background video updates can silently break after settings/content changes because the reused `PlayerView` is never rebound to the new `ExoPlayer` instance. Adding an `update` lambda keeps `PlayerView.player` in sync with recomposed `exoPlayer` values.</violation>
</file>

<file name=".github/workflows/build-apk.yml">

<violation number="1" location=".github/workflows/build-apk.yml:19">
P2: This configuration does not preserve a full per-commit build queue on active branches; intermediate pending runs can be dropped under bursty pushes. If every commit must build, the concurrency strategy should be changed to one that does not rely on default pending-slot behavior.</violation>

<violation number="2" location=".github/workflows/build-apk.yml:104">
P2: Manual runs on non-`claude/*` branches still enter the release job, which can create/update debug releases for mainline branches. Tightening the condition to branch scope keeps behavior aligned with the stated release isolation.</violation>
</file>

<file name="app/src/main/java/com/winlator/container/ContainerManager.java">

<violation number="1" location="app/src/main/java/com/winlator/container/ContainerManager.java:221">
P2: A failed config read during duplication now produces a partially default container instead of preserving source settings, because the exception path logs and continues to `saveData()`. Consider aborting duplication (and cleaning `dstDir`) when config load fails so users don't get a silently misconfigured copy.</violation>
</file>

<file name="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt:1309">
P2: Canonical path resolution is unguarded, so filesystem canonicalization errors can escape before allocation error handling and terminate this coroutine unexpectedly. Catching canonicalization failures inside `resolveInsideInstallDir` and returning null keeps this path in controlled failure handling.</violation>
</file>

<file name="app/src/main/java/app/gamenative/lan/LanRoomManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/lan/LanRoomManager.kt:489">
P2: After a join is denied, fails, or the host closes the room, the multicast lock can remain held and keep Wi‑Fi resources active until a manual stop. Releasing the lock on non-room terminal transitions in `joinRoom` avoids this battery/network leak.</violation>
</file>

<file name="app/src/main/cpp/virglrenderer/CMakeLists.txt">

<violation number="1" location="app/src/main/cpp/virglrenderer/CMakeLists.txt:57">
P2: Missing -Wl,-z,common-page-size=16384 alongside max-page-size. The official Android docs for 16 KB page size support recommend both flags for NDK r27 and lower. Without common-page-size, the ELF program header p_align field may not be set to 16 KB, which can cause the loader to fail on 16 KB page size devices. Several targets in this repo (asurfacerenderer, evshim, steambootstrap) already set both flags.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

@@ -100,13 +100,39 @@
<string name="confirm_delete">Confirm Deletion</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Several new strings use Portuguese values in the default locale (res/values/strings.xml), which is the fallback locale for all languages. This means users of any language other than pt-BR will see "Loja", "Lojas personalizadas", "Adicionar loja", "Remover loja" instead of English text. The existing pt-BR file at values-pt-rBR/strings.xml already contains translations for other strings — these should use English here and be added to the pt-BR file instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/res/values/strings.xml, line 103:

<comment>Several new strings use Portuguese values in the default locale (`res/values/strings.xml`), which is the fallback locale for all languages. This means users of any language other than pt-BR will see "Loja", "Lojas personalizadas", "Adicionar loja", "Remover loja" instead of English text. The existing pt-BR file at `values-pt-rBR/strings.xml` already contains translations for other strings — these should use English here and be added to the pt-BR file instead.</comment>

<file context>
@@ -100,13 +100,39 @@
     <string name="confirm_delete">Confirm Deletion</string>
     <string name="destination_library">Library</string>
     <string name="destination_downloads">Downloads &amp; Storage</string>
+    <string name="destination_game_hub">Loja</string>
+    <string name="game_hub_title">Loja</string>
+    <string name="game_hub_all_sources">All stores</string>
</file context>

.put("authType", authType.name)
.put("authHeaderName", authHeaderName)
.put("authScheme", authScheme)
.put("authToken", authToken)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: API tokens are persisted in plaintext as part of the config blob, so a local data extraction exposes credentials for third-party store accounts. Keeping authToken out of the JSON payload and storing it via encrypted storage (Keystore-backed) would reduce credential leakage risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.kt, line 66:

<comment>API tokens are persisted in plaintext as part of the config blob, so a local data extraction exposes credentials for third-party store accounts. Keeping `authToken` out of the JSON payload and storing it via encrypted storage (Keystore-backed) would reduce credential leakage risk.</comment>

<file context>
@@ -0,0 +1,128 @@
+        .put("authType", authType.name)
+        .put("authHeaderName", authHeaderName)
+        .put("authScheme", authScheme)
+        .put("authToken", authToken)
+        .put("httpMethod", httpMethod)
+        .put("libraryEndpoint", libraryEndpoint)
</file context>

var failed = 0
jobs.forEachIndexed { i, (entry, isDriver, type) ->
onProgress(Progress(entry.name, i + 1, jobs.size, 0f))
val result = runCatching {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Bulk install can ignore coroutine cancellation and keep downloading/installing entries after the caller is cancelled. This comes from wrapping installManifestEntry in runCatching without rethrowing CancellationException; consider rethrowing cancellation before counting it as a normal failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/utils/ManifestBulkInstaller.kt, line 68:

<comment>Bulk install can ignore coroutine cancellation and keep downloading/installing entries after the caller is cancelled. This comes from wrapping `installManifestEntry` in `runCatching` without rethrowing `CancellationException`; consider rethrowing cancellation before counting it as a normal failure.</comment>

<file context>
@@ -0,0 +1,80 @@
+        var failed = 0
+        jobs.forEachIndexed { i, (entry, isDriver, type) ->
+            onProgress(Progress(entry.name, i + 1, jobs.size, 0f))
+            val result = runCatching {
+                ManifestInstaller.installManifestEntry(context, entry, isDriver, type) { f ->
+                    onProgress(Progress(entry.name, i + 1, jobs.size, f))
</file context>

)
}
},
modifier = modifier,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Changing videoUri or soundOn can leave the background view attached to a stale/released ExoPlayer, so the new media state may not render or play. This happens because player assignment is done only in AndroidView.factory; adding an update block to rebind player each recomposition would keep PlayerView in sync.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.kt, line 97:

<comment>Changing videoUri or soundOn can leave the background view attached to a stale/released ExoPlayer, so the new media state may not render or play. This happens because player assignment is done only in AndroidView.factory; adding an update block to rebind `player` each recomposition would keep PlayerView in sync.</comment>

<file context>
@@ -0,0 +1,99 @@
+                )
+            }
+        },
+        modifier = modifier,
+    )
+}
</file context>

override suspend fun get(gameId: String): GameHubMetadata? = store[gameId]

override suspend fun setFavorite(gameId: String, favorite: Boolean) {
store[gameId] = (store[gameId] ?: GameHubMetadata(gameId)).copy(favorite = favorite)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Concurrent metadata updates can drop previously written fields for the same game. The current ConcurrentHashMap usage is thread-safe per operation, but each setter performs a non-atomic read-modify-write; using compute (or another atomic update path) for each setter would preserve all fields under races.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt, line 65:

<comment>Concurrent metadata updates can drop previously written fields for the same game. The current `ConcurrentHashMap` usage is thread-safe per operation, but each setter performs a non-atomic read-modify-write; using `compute` (or another atomic update path) for each setter would preserve all fields under races.</comment>

<file context>
@@ -0,0 +1,82 @@
+    override suspend fun get(gameId: String): GameHubMetadata? = store[gameId]
+
+    override suspend fun setFavorite(gameId: String, favorite: Boolean) {
+        store[gameId] = (store[gameId] ?: GameHubMetadata(gameId)).copy(favorite = favorite)
+        publish()
+    }
</file context>

)

fun EpicGame.toGameModel(): GameModel = GameModel(
id = GameModel.buildId(GameSource.EPIC, id.toString()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Epic game identity can drift across syncs because the mapper uses the local Room id instead of Epic’s stable external key. Using catalogId for GameModel.id would keep favorites/metadata associations stable when rows are recreated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/gamehub/GameHubMappers.kt, line 49:

<comment>Epic game identity can drift across syncs because the mapper uses the local Room `id` instead of Epic’s stable external key. Using `catalogId` for `GameModel.id` would keep favorites/metadata associations stable when rows are recreated.</comment>

<file context>
@@ -0,0 +1,76 @@
+)
+
+fun EpicGame.toGameModel(): GameModel = GameModel(
+    id = GameModel.buildId(GameSource.EPIC, id.toString()),
+    name = title.ifEmpty { appName },
+    source = GameSource.EPIC,
</file context>
Suggested change
id = GameModel.buildId(GameSource.EPIC, id.toString()),
id = GameModel.buildId(GameSource.EPIC, catalogId.ifEmpty { id.toString() }),

* directory (path traversal / "zip slip"). Returns null if the path escapes.
*/
private fun resolveInsideInstallDir(installDir: File, relativePath: String): File? {
val installRoot = installDir.canonicalPath

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Canonical path resolution is unguarded, so filesystem canonicalization errors can escape before allocation error handling and terminate this coroutine unexpectedly. Catching canonicalization failures inside resolveInsideInstallDir and returning null keeps this path in controlled failure handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt, line 1309:

<comment>Canonical path resolution is unguarded, so filesystem canonicalization errors can escape before allocation error handling and terminate this coroutine unexpectedly. Catching canonicalization failures inside `resolveInsideInstallDir` and returning null keeps this path in controlled failure handling.</comment>

<file context>
@@ -1276,6 +1299,23 @@ class EpicDownloadManager @Inject constructor(
+     * directory (path traversal / "zip slip"). Returns null if the path escapes.
+     */
+    private fun resolveInsideInstallDir(installDir: File, relativePath: String): File? {
+        val installRoot = installDir.canonicalPath
+        val resolved = File(installDir, relativePath).canonicalFile
+        return if (resolved.path == installRoot || resolved.path.startsWith(installRoot + File.separator)) {
</file context>

}
}
}
if (_status.value == Status.JOINED) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After a join is denied, fails, or the host closes the room, the multicast lock can remain held and keep Wi‑Fi resources active until a manual stop. Releasing the lock on non-room terminal transitions in joinRoom avoids this battery/network leak.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/lan/LanRoomManager.kt, line 489:

<comment>After a join is denied, fails, or the host closes the room, the multicast lock can remain held and keep Wi‑Fi resources active until a manual stop. Releasing the lock on non-room terminal transitions in `joinRoom` avoids this battery/network leak.</comment>

<file context>
@@ -0,0 +1,599 @@
+                                }
+                            }
+                        }
+                        if (_status.value == Status.JOINED) {
+                            _status.value = Status.IDLE
+                            _players.value = emptyList()
</file context>

# runs so it never touches main's releases.
release:
needs: build
if: startsWith(github.ref_name, 'claude/') || github.event_name == 'workflow_dispatch'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Manual runs on non-claude/* branches still enter the release job, which can create/update debug releases for mainline branches. Tightening the condition to branch scope keeps behavior aligned with the stated release isolation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/build-apk.yml, line 104:

<comment>Manual runs on non-`claude/*` branches still enter the release job, which can create/update debug releases for mainline branches. Tightening the condition to branch scope keeps behavior aligned with the stated release isolation.</comment>

<file context>
@@ -0,0 +1,150 @@
+  # runs so it never touches main's releases.
+  release:
+    needs: build
+    if: startsWith(github.ref_name, 'claude/') || github.event_name == 'workflow_dispatch'
+    runs-on: ubuntu-latest
+    permissions:
</file context>

GLESv3) No newline at end of file
GLESv3)
# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing -Wl,-z,common-page-size=16384 alongside max-page-size. The official Android docs for 16 KB page size support recommend both flags for NDK r27 and lower. Without common-page-size, the ELF program header p_align field may not be set to 16 KB, which can cause the loader to fail on 16 KB page size devices. Several targets in this repo (asurfacerenderer, evshim, steambootstrap) already set both flags.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/cpp/virglrenderer/CMakeLists.txt, line 57:

<comment>Missing -Wl,-z,common-page-size=16384 alongside max-page-size. The official Android docs for 16 KB page size support recommend both flags for NDK r27 and lower. Without common-page-size, the ELF program header p_align field may not be set to 16 KB, which can cause the loader to fail on 16 KB page size devices. Several targets in this repo (asurfacerenderer, evshim, steambootstrap) already set both flags.</comment>

<file context>
@@ -52,4 +52,6 @@ target_link_libraries(virglrenderer
\ No newline at end of file
+                      GLESv3)
+# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
+target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)
</file context>
Suggested change
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384)

@phobos665

Copy link
Copy Markdown
Contributor

What on earth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants