Claude/gamenative comprehensive review egflnd - #1694
Conversation
…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
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
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
📝 WalkthroughWalkthroughThe 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. ChangesGameNative feature and reliability changes
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (7)
app/src/main/java/app/gamenative/gamehub/GameLibraryRepository.kt (1)
64-77: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUse
ConcurrentHashMap.compute()for atomic read-modify-write.The
setFavorite,setLastPlayed, andsetConfigurationProfilemethods perform a non-atomic read-modify-write:store[gameId]is read, thenstore[gameId] = ...is written. If two suspend functions execute concurrently on different dispatchers (e.g.,setFavorite+setLastPlayedfor the same game), one update can overwrite the other — a classic TOCTOU race. The class doc at line 51 claims "Correct and thread-safe," butConcurrentHashMaponly guarantees atomicity of individualget/putcalls, 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 winMask the auth token input.
The
authTokenfield renders the API key in plain text via the genericFieldcomposable. 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 | 🔵 TrivialOptional: bail out or warn the user if persisting URI permission fails.
Currently a failed
takePersistableUriPermissiononly 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 valueConsider exposing
exeNameFixesfor test override.
fixesProviderhas a test-only override hook (setFixesProviderForTests), butexeNameFixesis 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 winLocal
envVarsshadows the fieldthis.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 usethis., but this pattern is fragile — any future edit inside this method that drops thethis.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
envVarsuses 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 blockingsyncContents()on the main thread.
refreshInstalledcallsmgr.syncContents()synchronously. It's invoked fromLaunchedEffect(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 inwithContext(Dispatchers.IO)and dropping the duplicate call in theLaunchedEffect.🤖 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
sourceLabelhardcodes "Local" — it won't be translated in the Portuguese locale.This PR adds Portuguese translations, but
sourceLabelreturns 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
sourceLabelcomposable 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_localtostrings.xmlandvalues-pt-rBR/strings.xml, and updating the two call sites (lines 227, 414) which are already inside@Composablescopes.🤖 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
📒 Files selected for processing (124)
.github/workflows/app-release-signed.yml.github/workflows/build-apk.yml.github/workflows/pluvia-pr-check.ymlapp/build.gradle.ktsapp/src/main/AndroidManifest.xmlapp/src/main/assets/box64_env_vars.jsonapp/src/main/cpp/asurfacerenderer/drawable.capp/src/main/cpp/patchelf/CMakeLists.txtapp/src/main/cpp/proot/CMakeLists.txtapp/src/main/cpp/virglrenderer/CMakeLists.txtapp/src/main/cpp/winlator/xconnector_epoll.capp/src/main/java/app/gamenative/MainActivity.ktapp/src/main/java/app/gamenative/PluviaApp.ktapp/src/main/java/app/gamenative/PrefManager.ktapp/src/main/java/app/gamenative/SteamBootstrap.ktapp/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/di/GameHubModule.ktapp/src/main/java/app/gamenative/events/EventDispatcher.ktapp/src/main/java/app/gamenative/gamefixes/GameFixesRegistry.ktapp/src/main/java/app/gamenative/gamefixes/STEAM_1888930.ktapp/src/main/java/app/gamenative/gamehub/DataStoreGameLibraryRepository.ktapp/src/main/java/app/gamenative/gamehub/DelegatingStoreProvider.ktapp/src/main/java/app/gamenative/gamehub/GameHubMappers.ktapp/src/main/java/app/gamenative/gamehub/GameHubRegistrar.ktapp/src/main/java/app/gamenative/gamehub/GameLibraryRepository.ktapp/src/main/java/app/gamenative/gamehub/GameModel.ktapp/src/main/java/app/gamenative/gamehub/GameModelMapper.ktapp/src/main/java/app/gamenative/gamehub/README.mdapp/src/main/java/app/gamenative/gamehub/StoreManager.ktapp/src/main/java/app/gamenative/gamehub/StoreProvider.ktapp/src/main/java/app/gamenative/gamehub/custom/CustomStoreConfig.ktapp/src/main/java/app/gamenative/gamehub/custom/CustomStoreRepository.ktapp/src/main/java/app/gamenative/lan/InGameLanChatOverlay.ktapp/src/main/java/app/gamenative/lan/LanRoomDialog.ktapp/src/main/java/app/gamenative/lan/LanRoomManager.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/service/amazon/AmazonDownloadManager.ktapp/src/main/java/app/gamenative/service/amazon/AmazonSdkManager.ktapp/src/main/java/app/gamenative/service/epic/EpicDownloadManager.ktapp/src/main/java/app/gamenative/service/gog/GOGDownloadManager.ktapp/src/main/java/app/gamenative/ui/PluviaMain.ktapp/src/main/java/app/gamenative/ui/component/QuickMenu.ktapp/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/ControllersDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.ktapp/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.ktapp/src/main/java/app/gamenative/ui/component/dialog/WhatsNewDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/WineTab.ktapp/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.ktapp/src/main/java/app/gamenative/ui/enums/HomeDestination.ktapp/src/main/java/app/gamenative/ui/enums/LibraryTab.ktapp/src/main/java/app/gamenative/ui/model/GameHubViewModel.ktapp/src/main/java/app/gamenative/ui/model/LibraryViewModel.ktapp/src/main/java/app/gamenative/ui/model/MainViewModel.ktapp/src/main/java/app/gamenative/ui/screen/HomeScreen.ktapp/src/main/java/app/gamenative/ui/screen/gamehub/CustomStoreDialog.ktapp/src/main/java/app/gamenative/ui/screen/gamehub/GameHubScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryBackground.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.ktapp/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.ktapp/src/main/java/app/gamenative/ui/screen/login/LoginBackgroundVideo.ktapp/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.ktapp/src/main/java/app/gamenative/ui/screen/settings/ContentsManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/settings/DriverManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupDebug.ktapp/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupEmulation.ktapp/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.ktapp/src/main/java/app/gamenative/ui/screen/settings/WineProtonManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.ktapp/src/main/java/app/gamenative/utils/AssetUtils.ktapp/src/main/java/app/gamenative/utils/ContainerUtils.ktapp/src/main/java/app/gamenative/utils/CoverArtManager.ktapp/src/main/java/app/gamenative/utils/CustomGameScanner.ktapp/src/main/java/app/gamenative/utils/CustomGameWatcher.ktapp/src/main/java/app/gamenative/utils/DiagnosticsAnalyzer.ktapp/src/main/java/app/gamenative/utils/IntentLaunchManager.ktapp/src/main/java/app/gamenative/utils/ManifestBulkInstaller.ktapp/src/main/java/app/gamenative/utils/ManifestComponentHelper.ktapp/src/main/java/app/gamenative/utils/PerformanceGovernor.ktapp/src/main/java/app/gamenative/utils/RuntimeCompatibility.ktapp/src/main/java/app/gamenative/utils/SessionLogger.ktapp/src/main/java/app/gamenative/utils/SteamGridDB.ktapp/src/main/java/app/gamenative/utils/TelemetryCollector.ktapp/src/main/java/com/winlator/box86_64/Box86_64PresetManager.javaapp/src/main/java/com/winlator/box86_64/rc/RCField.javaapp/src/main/java/com/winlator/container/Container.javaapp/src/main/java/com/winlator/container/ContainerData.ktapp/src/main/java/com/winlator/container/ContainerManager.javaapp/src/main/java/com/winlator/contents/ContentsManager.javaapp/src/main/java/com/winlator/core/DXVKHelper.javaapp/src/main/java/com/winlator/core/DefaultVersion.javaapp/src/main/java/com/winlator/core/FileUtils.javaapp/src/main/java/com/winlator/core/ProcessHelper.javaapp/src/main/java/com/winlator/core/TarCompressorUtils.javaapp/src/main/java/com/winlator/core/WineUtils.javaapp/src/main/java/com/winlator/core/envvars/EnvVarInfo.ktapp/src/main/java/com/winlator/renderer/ASurfaceRenderer.javaapp/src/main/java/com/winlator/widget/TouchpadView.javaapp/src/main/java/com/winlator/winhandler/WinHandler.javaapp/src/main/java/com/winlator/xconnector/XConnectorEpoll.javaapp/src/main/java/com/winlator/xenvironment/ImageFs.javaapp/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.javaapp/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.javaapp/src/main/java/com/winlator/xserver/Drawable.javaapp/src/main/java/com/winlator/xserver/XClientRequestHandler.javaapp/src/main/java/com/winlator/xserver/requests/DrawRequests.javaapp/src/main/res/values-pt-rBR/strings.xmlapp/src/main/res/values/strings.xmlapp/src/test/java/app/gamenative/gamehub/GameHubMappersTest.ktapp/src/test/java/app/gamenative/gamehub/GameModelMapperTest.ktapp/src/test/java/app/gamenative/gamehub/StoreManagerTest.ktapp/src/test/java/app/gamenative/utils/PerformanceGovernorTest.ktapp/src/test/java/com/winlator/box86_64/Box86_64PresetManagerTest.ktapp/src/test/java/com/winlator/container/ContainerPersistenceTest.ktapp/src/test/java/com/winlator/core/ProcessHelperAffinityTest.ktdocs/RELATORIO_ENGENHARIA_2026-07-09.mddocs/RELATORIO_REVISAO_2026-07.mddocs/SERVIDOR_GAMENATIVE_ANALISE.mddocs/XODOS_ANALISE.md
| 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." |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
Cleanup job deletes artifacts from ALL workflows, not just build-apk.yml
Two issues in the cleanup job:
-
Cross-workflow deletion (critical): Line 39 lists ALL repository artifacts via
repos/$GH_REPO/actions/artifacts, butKEEP_RUNS(line 36) only contains run IDs from thebuild-apk.ymlworkflow. Artifacts from other workflows (e.g.,app-release-signed.ymluploads signed APKs) have different run IDs, soKEEPstaysfalseand they get deleted. -
Empty
KEEP_RUNSon API failure (major): Withset -uo pipefailbut no-e(line 35), agh apifailure (rate limit, network) leavesKEEP_RUNSempty. Thefor K in $KEEP_RUNSloop never executes, so every artifact getsKEEP=falseand 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.
| 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.
| /** 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, | ||
| ) |
There was a problem hiding this comment.
🎯 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.
| /** 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.
| conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR | ||
| else -> null |
There was a problem hiding this comment.
🎯 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.
| 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.
| // 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") |
There was a problem hiding this comment.
🩺 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.
| /** | ||
| * 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 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.
| // 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( |
There was a problem hiding this comment.
🎯 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) |
There was a problem hiding this comment.
🩺 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.
|
|
||
| // 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)+")"); |
There was a problem hiding this comment.
🗄️ 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.
| // 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.
| /** ~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); |
There was a problem hiding this comment.
🩺 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.
| /** ~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.
| <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> |
There was a problem hiding this comment.
📐 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.
There was a problem hiding this comment.
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> | |||
There was a problem hiding this comment.
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 & 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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()), |
There was a problem hiding this comment.
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>
| 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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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>
| 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) |
|
What on earth. |
Description
Recording
Type of Change
Checklist
#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.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
app.gamenative.gamehub): universalStoreProvider+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..github/workflows/build-apk.ymlbuilds and publishes debug APKs; release workflow gated to upstream repo.Bug Fixes
SteamServiceflags@Volatile; atomic download counters and listener lists; fix duplicate keys and error isolation inStoreManager.Written for commit 65d477f. Summary will update on new commits.
Summary by CodeRabbit