Skip to content

Conversation

@joshuatam
Copy link
Contributor

@joshuatam joshuatam commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Automatic Steam login setup during container launch
    • Two-phase Steam token setup and management with token obfuscation utilities
    • Automatic download of steam-token files when missing
    • Steam client file backup and restore utilities
  • Bug Fixes & Improvements

    • Improved Steam authentication and login persistence across flows
    • Better cross-platform Steam integration and more robust config handling

✏️ Tip: You can customize this high-level summary in your review settings.

@joshuatam joshuatam changed the title Feat: Implement steam auto login Feat: Implement steam client auto login Jan 13, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

Adds a Steam token-based login orchestration and helpers, integrates token setup into launch flow, adds token obfuscation utilities, expands Steam utilities (DLL backup/restore), and updates Steam-related launch and unpacking logic and a JWT dependency.

Changes

Cohort / File(s) Summary
Steam token core
app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt, app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
New SteamTokenHelper (obfuscate/deobfuscate with checksum, hex I/O) and SteamTokenLogin (two-phase config.vdf/local.vdf orchestration, JWT checks, wine-based encrypt/decrypt, file writes/permissions, public setup/phase APIs).
Steam integration & service tweaks
app/src/main/java/app/gamenative/service/SteamService.kt, app/src/main/java/app/gamenative/utils/SteamUtils.kt
SteamService: removed currentuser in OAuth VDF output and added clientOSType/persistentSession to auth session details. SteamUtils: made autoLoginUserChanges public, added personaName to OAuth call, added steamClientFiles, backupSteamclientFiles, restoreSteamclientFiles, removed RememberPassword write, and added updateOrModifyLocalConfig.
Launch flow & UI integration
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, app/src/main/java/app/gamenative/ui/PluviaMain.kt
XServerScreen: invoke SteamTokenLogin.setupSteamFiles() at container launch when isLaunchRealSteam, switch Steam checks to container.isLaunchRealSteam, and add enableSteamless param to unpackExecutableFile signature. PluviaMain: download steam-token.tzst when needed during pre-launch.
Build config
app/build.gradle.kts
Added dependency com.auth0.android:jwtdecode:2.0.2 for JWT validation.

Sequence Diagram(s)

sequenceDiagram
    participant XServer as XServerScreen
    participant TokenLogin as SteamTokenLogin
    participant Service as SteamService
    participant Wine as Wine/Box64
    participant FS as FileSystem
    participant Helper as SteamTokenHelper

    XServer->>TokenLogin: setupSteamFiles(container, imageFs, ...)
    TokenLogin->>FS: check/read config.vdf
    alt config.vdf present
        TokenLogin->>Helper: deobfuscateToken(ConnectCache)
        Helper-->>TokenLogin: token bytes
        TokenLogin->>Service: validate JWT / check expiration
        alt JWT valid
            TokenLogin->>FS: ensure local Steam dir & extract steam-token.tzst
            TokenLogin->>Wine: run steam-token.exe (decrypt/encrypt)
            Wine-->>TokenLogin: encrypted/decrypted token
            TokenLogin->>FS: write/update local.vdf
        else JWT expired
            TokenLogin->>TokenLogin: createConfigVdf() (obfuscateToken)
            TokenLogin->>FS: write config.vdf and remove stale local.vdf
            TokenLogin->>FS: proceed to local.vdf creation (extract & run steam-token.exe)
            Wine-->>TokenLogin: encrypted token
            TokenLogin->>FS: write local.vdf
        end
    else config.vdf missing
        TokenLogin->>TokenLogin: createConfigVdf() and createLocalVdf()
        TokenLogin->>FS: write config.vdf & local.vdf
    end
    TokenLogin-->>XServer: setup complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Exagear steam #118 — Changes to SteamService.getLoginUsersVdfOauth affecting OAuth VDF fields (direct overlap).
  • Latest gbe #379 — Modifies unpackExecutableFile signature and XServer Steam launch flows (overlapping launch/unpack logic).
  • Emuready #121 — Changes PluviaMain.preLaunchApp related to steam-token download and pre-launch handling.

Poem

🐰
A rabbit nudges tokens, hops through VDF and file,
Phase one hides the secret, phase two makes it compile.
Wine stirs the magic, hex and checksum hum,
Steam wakes to login — welcome, runner, run! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main focus of the changeset—implementing Steam client auto-login functionality through new token handling, OAuth login modifications, and Steam file setup integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

279-306: Avoid forced unwraps + “null” strings in autoLoginUserChanges (can crash launch / corrupt VDF).
personaName = ...!! can NPE, and steamId64 = SteamService.userSteamId?.…​.toString() will write "null" when unset.

Proposed fix
 fun autoLoginUserChanges(imageFs: ImageFs) {
+    val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString()
+        ?: PrefManager.steamUserSteamId64.takeIf { it != 0L }?.toString()
+        ?: run {
+            Timber.w("Steam userSteamId not available; skipping autoLoginUserChanges")
+            return
+        }
+    val personaNameSafe =
+        SteamService.instance?.localPersona?.value?.name
+            ?: PrefManager.username
+
     val vdfFileText = SteamService.getLoginUsersVdfOauth(
-        steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
+        steamId64 = steamId64,
         account = PrefManager.username,
         refreshToken = PrefManager.refreshToken,
         accessToken = PrefManager.accessToken,      // may be blank
-        personaName = SteamService.instance?.localPersona?.value?.name!!
+        personaName = personaNameSafe
     )
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt:
- Around line 64-122: The deobfuscate flow and hexStringToByteArray lack input
validation and can crash on malformed hex (odd length or non-hex chars) or
too-short data; update hexStringToByteArray to validate even length and only hex
digits and throw a clear exception on invalid input, then in deobfuscate
validate data string conversion result and lengths before indexing (ensure
dataBytes.size >= 8 at least to contain header + checksum and handle cases where
ctext becomes empty) and check that csumData and ctext slices use safe bounds
(protect uses of sliceArray(4 until dataBytes.size - 4) and
ctext.sliceArray(0..3) by confirming remaining length >= 4), returning/throwing
descriptive errors if checks fail; reference functions: deobfuscate,
hexStringToByteArray and variables: dataBytes, csumData, ctext, ptext, k1, k2.

In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt:
- Around line 85-113: createLocalVdf currently only reads process.inputStream
which can deadlock if steam-token.exe writes to stderr; replace
Runtime.getRuntime().exec(...) usage with a ProcessBuilder built from the same
command and env (use ProcessHelper.splitCommand(command) for args), call
redirectErrorStream(true) (or spawn a separate thread to consume
process.errorStream) before starting the process, then read from the merged
input stream and waitFor(); ensure you still collect output and check exitCode
to throw the same RuntimeException on non-zero exit.
🧹 Nitpick comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

1809-1809: Remove commented-out code or add explanatory comment.

If the currentuser entry is no longer needed for the auto-login flow, delete this line entirely. If it's being kept for potential future use or debugging, add a brief comment explaining why.

🔧 Suggested fix
-//                appendLine("    \"currentuser\"              \"$steamId64\"")
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

1503-1513: Guard SteamTokenLogin.setupSteamFiles inputs + handle failures (launch path).
steamId/login/token appear to come from PrefManager; if token is blank/steamId is 0, setup likely writes invalid configs or fails silently.

Concrete hardening idea
 if (container.isLaunchRealSteam) {
+    val steamId64 = PrefManager.steamUserSteamId64
+    val login = PrefManager.username
+    val token = PrefManager.refreshToken
+    if (steamId64 == 0L || login.isBlank() || token.isBlank()) {
+        Timber.w("Skipping SteamTokenLogin.setupSteamFiles: missing steam credentials (steamId64=%s, loginBlank=%s, tokenBlank=%s)",
+            steamId64, login.isBlank(), token.isBlank()
+        )
+    } else {
         SteamTokenLogin.setupSteamFiles(
             context = context,
-            steamId = PrefManager.steamUserSteamId64.toString(),
-            login = PrefManager.username,
-            token = PrefManager.refreshToken,
+            steamId = steamId64.toString(),
+            login = login,
+            token = token,
             imageFs = imageFs,
             container = container,
             isArm64EC = xServerState.value.wineInfo.isArm64EC,
         )
+    }
 }
app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt (1)

16-62: Consider replacing repeated sliceArray/ByteBuffer allocations (hot-path perf).
Current implementation does O(n²) copying for large payloads.

Also applies to: 84-123

app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (2)

145-193: phase1SteamConfig: avoid chmod on missing files (and double-reads).
You chmod loginusers.vdf regardless of existence, and read config.vdf multiple times.


199-294: phase2LocalConfig should report failure to caller (don’t println/swallow).
Right now setup continues even if local.vdf generation fails.

Small shape change idea
-fun phase2LocalConfig(... ) {
+fun phase2LocalConfig(... ): Boolean {
     try {
@@
-    } catch (e: Exception) {
-        println("An unexpected error occurred: ${e.message}")
-        e.printStackTrace()
-    }
+        return true
+    } catch (e: Exception) {
+        Timber.e(e, "phase2LocalConfig failed")
+        return false
+    }
 }
@@
-    if (!handled) {
-        phase2LocalConfig(...)
-    }
+    if (!handled) {
+        val ok = phase2LocalConfig(...)
+        if (!ok) Timber.w("Steam token local.vdf setup failed")
+    }

Also applies to: 296-325

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd26d48 and 9acb66a.

📒 Files selected for processing (6)
  • app/src/main/assets/steam-token.tzst
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.

Applied to files:

  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (2)
app/src/main/python/gogdl/dl/objects/v1.py (1)
  • File (36-49)
app/src/main/java/com/winlator/core/envvars/EnvVars.java (1)
  • EnvVars (8-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
app/src/main/java/app/gamenative/service/SteamService.kt (2)

1889-1896: Verify Windows OS type emulation is intentional for auto-login.

Setting clientOSType = EOSType.WinUnknown on an Android app suggests this is emulating a Windows client to enable Steam auto-login. Please confirm this is the intended behavior and that it doesn't cause issues with Steam's client detection or violate any terms.


1943-1947: LGTM - QR login now configured for auto-login support.

The additions align QR login with credential login (deviceFriendlyName, clientOSType). Hardcoding persistentSession = true makes sense for QR authentication flows, as users scanning a QR code typically expect the session to persist.

app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)

279-306: Confirm the intended behavior of commenting out RememberPassword / CompletedOOBEStage1.
If Steam relies on these flags for persistence/first-run suppression in your Wine setup, auto-login may regress.


746-754: Account ID fallback logic looks fine; verify accountId=0 behavior is acceptable.
If both SteamService and PrefManager fallbacks are absent, this writes paths under userdata\0.

app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (2)

1552-1563: Signature change: ensure all unpackExecutableFile call sites were updated.
This file is updated; verify there are no other invocations elsewhere (compile break).

Also applies to: 2123-2133


2215-2288: Steamless gating looks correct; verify the z:\tmp\... mapping matches where the batch files are written.
You write the bat to ${imageFs.rootDir}/tmp/... but execute wine z:\\tmp\\.... If Z: doesn’t map to imageFs.rootDir, this will fail intermittently.

Comment on lines 64 to 122
fun deobfuscate(data: String, key: Long): ByteArray {
val dataBytes = hexStringToByteArray(data)

// Check header
if (dataBytes.size < 4 ||
dataBytes[0] != 0x02.toByte() ||
dataBytes[1] != 0x00.toByte() ||
dataBytes[2] != 0x00.toByte() ||
dataBytes[3] != 0x00.toByte()) {
throw Exception("wrong type of data")
}

val csumData = dataBytes.sliceArray(dataBytes.size - 4 until dataBytes.size)
var ctext = dataBytes.sliceArray(4 until dataBytes.size - 4)
val ptext = mutableListOf<Byte>()

var k1 = (key shr 0x1f).toInt()
var k2 = key.toInt()
var csum = 0

while (ctext.size >= 4) {
k1 = (k1 + 0x25fe6761) and 0xffffffff.toInt()
k2 = (k2 + 1) and 0xffffffff.toInt()

val d = ByteBuffer.wrap(ctext.sliceArray(0..3))
.order(ByteOrder.LITTLE_ENDIAN)
.int

val t = obf[k2 % 0x11] xor k1 xor d
csum = (csum + t) and 0xffffffff.toInt()

val tBytes = ByteBuffer.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(t)
.array()

ptext.addAll(tBytes.toList())

ctext = ctext.sliceArray(4 until ctext.size)
}

// Add remaining bytes
ptext.addAll(ctext.toList())

k1 = (k1 + 0x25fe6761) and 0xffffffff.toInt()
k2 = (k2 + 1) and 0xffffffff.toInt()
val t = obf[k2 % 0x11] xor k1 xor csum

val tBytes = ByteBuffer.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(t)
.array()

if (!tBytes.contentEquals(csumData)) {
throw Exception("bad checksum! ${t.toString(16)}")
}

return ptext.toByteArray()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix input-length/hex validation in deobfuscate/hexStringToByteArray (can crash on malformed input).

Proposed fix
 fun deobfuscate(data: String, key: Long): ByteArray {
     val dataBytes = hexStringToByteArray(data)
 
-    // Check header
-    if (dataBytes.size < 4 ||
+    // Need at least 4-byte header + 4-byte checksum
+    if (dataBytes.size < 8 ||
         dataBytes[0] != 0x02.toByte() ||
         dataBytes[1] != 0x00.toByte() ||
         dataBytes[2] != 0x00.toByte() ||
         dataBytes[3] != 0x00.toByte()) {
         throw Exception("wrong type of data")
     }
@@
 private fun hexStringToByteArray(hex: String): ByteArray {
     val cleanHex = hex.replace(" ", "")
+    require(cleanHex.length % 2 == 0) { "invalid hex length" }
     val len = cleanHex.length
     val data = ByteArray(len / 2)
 
     for (i in 0 until len step 2) {
-        data[i / 2] = ((Character.digit(cleanHex[i], 16) shl 4) +
-                Character.digit(cleanHex[i + 1], 16)).toByte()
+        val hi = Character.digit(cleanHex[i], 16)
+        val lo = Character.digit(cleanHex[i + 1], 16)
+        require(hi >= 0 && lo >= 0) { "invalid hex character" }
+        data[i / 2] = ((hi shl 4) + lo).toByte()
     }
     return data
 }

Also applies to: 124-134

🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt around lines 64 -
122, The deobfuscate flow and hexStringToByteArray lack input validation and can
crash on malformed hex (odd length or non-hex chars) or too-short data; update
hexStringToByteArray to validate even length and only hex digits and throw a
clear exception on invalid input, then in deobfuscate validate data string
conversion result and lengths before indexing (ensure dataBytes.size >= 8 at
least to contain header + checksum and handle cases where ctext becomes empty)
and check that csumData and ctext slices use safe bounds (protect uses of
sliceArray(4 until dataBytes.size - 4) and ctext.sliceArray(0..3) by confirming
remaining length >= 4), returning/throwing descriptive errors if checks fail;
reference functions: deobfuscate, hexStringToByteArray and variables: dataBytes,
csumData, ctext, ptext, k1, k2.

Comment on lines 24 to 82
private fun createConfigVdf(
steamId: String,
login: String,
token: String
): String {
// Simple hash-based encryption for cross-platform compatibility
val crc = CRC32()
crc.update(login.toByteArray())

val hdr = "${crc.value.toString(16)}1"

// This is the key to make config.vdf work
val nullChar = '\u0000'

// e.g. 1329969238
val minMTBF = 1000000000L
val maxMTBF = 2000000000L
var mtbf = kotlin.random.Random.nextLong(minMTBF, maxMTBF)
var encoded = ""

// Try to encode the token until it get a value
do {
try {
encoded = SteamTokenHelper.obfuscate("$token$nullChar".toByteArray(), mtbf)
} catch (_: Exception) {
mtbf = kotlin.random.Random.nextLong(minMTBF, maxMTBF)
}
} while (encoded == "")

Timber.tag("SteamTokenLogin").d("MTBF: $mtbf")
Timber.tag("SteamTokenLogin").d("Encoded: $encoded")

return """
"InstallConfigStore"
{
"Software"
{
"Valve"
{
"Steam"
{
"MTBF" "$mtbf"
"ConnectCache"
{
"$hdr" "$encoded$nullChar"
}
"Accounts"
{
"$login"
{
"SteamID" "$steamId"
}
}
}
}
}
}
""".trimIndent()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not log token-derived secrets (encoded) or command args containing the token.

Proposed fix
- Timber.tag("SteamTokenLogin").d("MTBF: $mtbf")
- Timber.tag("SteamTokenLogin").d("Encoded: $encoded")
+ Timber.tag("SteamTokenLogin").d("Generated config.vdf (mtbf=%d, encodedLen=%d)", mtbf, encoded.length)
@@
- Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(command).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)
+ Timber.tag("SteamTokenLogin").d("Executing steam-token encrypt (login=%s)", login)

Also applies to: 95-96

Comment on lines 85 to 113
private fun createLocalVdf(winePath: String, envVars: EnvVars, imageFs: ImageFs, login: String, token: String): String {
// Simple hash-based encryption for cross-platform compatibility
val crc = CRC32()
crc.update(login.toByteArray())
val hdr = "${crc.value.toString(16)}1"

// Simple encoding (not as secure as Windows CryptProtectData, but cross-platform)
// run steam-token.exe from extractDir and return the result
val command = "$winePath ${imageFs.rootDir}/opt/apps/steam-token.exe encrypt $login $token"

Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(command).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)

val process = Runtime.getRuntime().exec(command, envVars.toStringArray(), imageFs.rootDir)
val reader = BufferedReader(process.inputStream.reader())

val output = StringBuilder()

var line: String?
while ((reader.readLine().also { line = it }) != null) {
output.append(line).append("\n")
}

val exitCode = process.waitFor()

// Check if command succeeded
if (exitCode != 0) {
throw RuntimeException("Steam token encryption failed with exit code: $exitCode. Output: $output")
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

createLocalVdf: consume stderr or redirect it (process can hang).
Reading only process.inputStream risks deadlock if steam-token.exe writes enough to stderr.

Proposed fix (ProcessBuilder + redirectErrorStream)
- val process = Runtime.getRuntime().exec(command, envVars.toStringArray(), imageFs.rootDir)
- val reader = BufferedReader(process.inputStream.reader())
+ val args = ProcessHelper.splitCommand(command).toList()
+ val pb = ProcessBuilder(args)
+     .directory(imageFs.rootDir)
+     .redirectErrorStream(true)
+ val env = pb.environment()
+ for (k in envVars) env[k] = envVars.get(k)
+ val process = pb.start()
+ val reader = BufferedReader(process.inputStream.reader())
 
 val output = StringBuilder()
 
 var line: String?
- while ((reader.readLine().also { line = it }) != null) {
-     output.append(line).append("\n")
- }
+ reader.use {
+     while ((it.readLine().also { line = it }) != null) {
+         output.append(line).append("\n")
+     }
+ }
 
 val exitCode = process.waitFor()
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt around lines 85 -
113, createLocalVdf currently only reads process.inputStream which can deadlock
if steam-token.exe writes to stderr; replace Runtime.getRuntime().exec(...)
usage with a ProcessBuilder built from the same command and env (use
ProcessHelper.splitCommand(command) for args), call redirectErrorStream(true)
(or spawn a separate thread to consume process.errorStream) before starting the
process, then read from the merged input stream and waitFor(); ensure you still
collect output and check exitCode to throw the same RuntimeException on non-zero
exit.

Copy link
Owner

Choose a reason for hiding this comment

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

I have uploaded this to https://downloads.gamenative.app/steam-token.tzst. Can we download and extract it like we do for steam.tzst?

Copy link
Owner

Choose a reason for hiding this comment

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

Additionally, let's fix the case you mentioned:
image

Maybe we can detect if the goldberg dlls are used, if they are, we extract steam.tzst (or overwrite the dlls, we can store a backup or a copy in assets/)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me check for this later tonight

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@utkarshdalal updated, it will now backup / restore the steamclient to avoid conflicts with experimental drm files

@joshuatam joshuatam force-pushed the feat/steam-autologin branch from 9acb66a to b3586eb Compare January 13, 2026 11:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:
- Around line 1503-1513: SteamTokenLogin.setupSteamFiles() can throw runtime
exceptions (e.g., from execCommand) and is currently called without protection;
wrap the call in a try-catch around the block where container.isLaunchRealSteam
is checked (the SteamTokenLogin(...) .setupSteamFiles() invocation) to catch
Throwable or RuntimeException, log the error via your logger or
PrefManager/appropriate logger with context (include container or steamId), and
gracefully continue startup (e.g., skip steam setup) to avoid crashing the app.

In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt:
- Around line 134-144: encryptToken and decryptToken pass sensitive tokens to
execCommand which currently logs the full command; stop logging raw tokens by
either masking or omitting them before logging. Update execCommand to accept an
optional maskedCommand string or a boolean suppressSensitiveLog flag and use
that when called from encryptToken/decryptToken (or build a sanitized command
string there) so logs contain only non-sensitive parts (e.g., "$winePath ...
steam-token.exe encrypt <REDACTED>" or no token), while still executing the full
command with the original token; reference the encryptToken, decryptToken, and
execCommand symbols when making the change.
- Around line 109-132: The method in SteamTokenLogin that executes external
commands (uses ProcessHelper.splitCommand and Runtime.getRuntime().exec) leaks
resources because the BufferedReader and the process streams aren’t closed and
stderr isn’t consumed; fix it by wrapping the process streams in
try-with-resources / Kotlin use blocks (e.g., reader =
BufferedReader(process.inputStream.reader()).use { ... }) and also read/consume
the process.errorStream (either synchronously into a String or on a background
thread) so the process cannot block, ensure process.outputStream is closed, and
only call process.waitFor() after streams are consumed and all readers are
closed; include logging of stderr and ensure process.destroy() /
destroyForcibly() is used on exceptional paths.

In @app/src/main/java/app/gamenative/utils/SteamUtils.kt:
- Around line 279-286: In autoLoginUserChanges, avoid the unsafe force-unwrap of
SteamService.instance?.localPersona?.value?.name!! when calling
SteamService.getLoginUsersVdfOauth; instead first read and validate the persona
name (e.g., val personaName = SteamService.instance?.localPersona?.value?.name)
and if null either provide a safe fallback (empty string or a default like
"Unknown"), or return early/log and throw a descriptive exception; then pass
that validated personaName into SteamService.getLoginUsersVdfOauth so you never
use !! on the nullable chain.

In @app/src/main/java/app/gamenative/utils/VdfStringParser.kt:
- Around line 27-33: The code consumes the next line with iterator.next() while
checking for "{" and drops it when the check fails, skipping data; fix by
avoiding consumption when peeking — e.g., change the iterator type to a
ListIterator or switch to an index-based loop so you can peek the next token
without advancing, or if you must use ListIterator call previous() to unread
when the peeked value != "{"; update the code around the section header handling
(the sectionName extraction and the call to parseSection(iterator)) so that a
non-"{" next line is not discarded but remains available for normal parsing.
🧹 Nitpick comments (5)
app/src/main/java/app/gamenative/utils/VdfStringParser.kt (2)

18-26: Double-tab delimiter (\t\t) is fragile for VDF parsing.

VDF format typically uses quoted key-value pairs with flexible whitespace. The strict \t\t requirement will fail to parse valid VDF content that uses different spacing conventions (single tab, multiple spaces, etc.).

Consider using a regex pattern to handle quoted key-value pairs more robustly:

♻️ More robust key-value parsing
// Match: "key"<whitespace>"value" or "key"<whitespace>value
private val keyValuePattern = Regex("""^"([^"]+)"\s+"?([^"]*)"?$""")

// In parseSection:
keyValuePattern.matchEntire(line)?.let { match ->
    val (key, value) = match.destructured
    result[key] = value
}

4-7: Trimming lines before parsing loses indentation context.

VDF parsing often relies on indentation to determine nesting depth. By trimming all lines upfront (line 5), the parser loses this structural information and must rely solely on brace matching, which is less robust.

app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (3)

302-303: Incorrect file permission value: 505 (octal 0771) is unusual.

FileUtils.chmod(file, 505) passes decimal 505, but the comment says 0771. In octal, 0771 = 505 decimal is incorrect. Octal 0771 = 505 is actually correct mathematically, but the permission 771 (rwxrwx--x) is unusual for config files. Consider using 0644 (420 decimal) for readable config files or 0600 (384 decimal) if restricting access.

💡 Clarification
// If you want rwxrwx--x (octal 0771 = 505 decimal), this is correct but unusual
// For config files, typical permissions are:
// - 0644 (rw-r--r--) = 420 decimal - readable by all
// - 0600 (rw-------) = 384 decimal - owner only
FileUtils.chmod(file, 420) // 0644 - more typical for config files

Also applies to: 366-366


321-371: Silent exception swallowing hides debugging information.

In phase2LocalConfig(), the catch block at line 353 silently swallows exceptions during JWT parsing. While the outer catch at line 367 prints to stdout, it should use Timber for consistency with the rest of the codebase.

♻️ Suggested improvement
                     try {
                         val dToken = decryptToken(connectCacheValue.trimEnd(NULL_CHAR))
                         val savedJWT = JWT(dToken)
 
                         // If the saved JWT is not expired, do not override it
                         if (!savedJWT.isExpired(TOKEN_EXPIRE_TIME)) {
                             Timber.tag("SteamTokenLogin").d("Saved JWT is not expired, do not override local.vdf")
                             return
                         }
-                    } catch (_: Exception) {
+                    } catch (e: Exception) {
+                        Timber.tag("SteamTokenLogin").d(e, "Failed to parse saved JWT, will override local.vdf")
                     }
                 }
             }
             // ...
-        } catch (e: Exception) {
-            println("An unexpected error occurred: ${e.message}")
-            e.printStackTrace()
+        } catch (e: Exception) {
+            Timber.tag("SteamTokenLogin").e(e, "An unexpected error occurred in phase2LocalConfig")
         }

322-326: Asset extraction in phase2 should be conditional or moved to initialization.

steam-token.tzst is extracted every time phase2LocalConfig() is called. Consider checking if the file already exists or moving extraction to setupSteamFiles() initialization.

♻️ Suggested improvement
+    private fun ensureSteamTokenExtracted() {
+        val steamTokenExe = File(imageFs.rootDir, "/opt/apps/steam-token.exe")
+        if (!steamTokenExe.exists()) {
+            val extractDir = File(imageFs.rootDir, "/opt/apps/")
+            Files.createDirectories(extractDir.toPath())
+            TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, context.assets, "steam-token.tzst", extractDir)
+        }
+    }
+
     fun phase2LocalConfig() {
         try {
-            // Extract steam-token.tzst
-            val extractDir = File(imageFs.rootDir, "/opt/apps/")
-            Files.createDirectories(extractDir.toPath())
-            TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, context.assets, "steam-token.tzst", extractDir)
+            ensureSteamTokenExtracted()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9acb66a and b3586eb.

📒 Files selected for processing (8)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/VdfStringParser.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.

Applied to files:

  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (1)
app/src/main/java/com/winlator/core/envvars/EnvVars.java (1)
  • EnvVars (8-84)
🔇 Additional comments (14)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

746-751: Formatting cleanup looks good.

These are cosmetic whitespace changes with no behavioral impact.

app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)

1111-1119: LGTM!

The steam-token download logic follows the same established pattern as the steam.tzst download. The conditional check, progress tracking, and await behavior are consistent with the surrounding code.

app/src/main/java/app/gamenative/service/SteamService.kt (3)

1786-1813: VDF output change removes the currentuser entry.

The currentuser block was removed from the VDF generation. Verify this is intentional for the token-based login flow, as it may affect how Steam identifies the active user in multi-user scenarios.


1894-1895: Verify EOSType.WinUnknown is appropriate for Steam auto-login.

Setting clientOSType = EOSType.WinUnknown presents this client as a Windows system to Steam servers. This appears intentional for the native Steam client auto-login feature, but ensure this doesn't violate Steam's ToS or cause unexpected behavior with account security features.


1942-1946: QR login now sets Windows OS type and persistent session.

The additions of clientOSType = EOSType.WinUnknown and persistentSession = true align with the credentials-based login changes. This ensures consistent behavior across both authentication methods for the token-based auto-login flow.

app/build.gradle.kts (1)

309-310: Dependency addition is appropriate — version 2.0.2 is the latest stable release.

The jwtdecode library is well-suited for decoding JWT tokens in the Steam OAuth login flow, and the pinned version is current as of January 2026.

app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (5)

1551-1562: LGTM!

The enableSteamless parameter is correctly propagated with !container.isLaunchRealSteam, ensuring Steamless DRM processing is disabled when launching real Steam (which handles its own DRM).


1660-1660: LGTM!

Adding !container.isLaunchRealSteam correctly prevents requesting encrypted app tickets when using real Steam, as the Steam client handles authentication itself.


1796-1796: LGTM!

The property access pattern container.isLaunchRealSteam is consistently used throughout the file for conditional Steam launch behavior.


2131-2131: LGTM!

The new enableSteamless parameter is correctly added to the function signature.


2215-2288: LGTM!

The Steamless processing block is properly gated behind enableSteamless. The logic correctly:

  • Scans executables only when enabled
  • Filters out CommonRedist and already-processed files
  • Handles per-file errors gracefully with continue semantics
  • Cleans up batch files in the finally block
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (3)

26-34: LGTM!

The class design is clean with clear constructor parameters and conditional wine path setup based on container variant and architecture.


41-45: LGTM!

The setupSteamFiles() entry point properly orchestrates the two-phase login flow.


246-290: VdfStringParser class exists but handles malformed VDF silently.

The VdfStringParser class is defined in app/src/main/java/app/gamenative/utils/VdfStringParser.kt. However, the parser silently skips invalid lines without throwing exceptions (e.g., when a line doesn't contain the expected "\t\t" delimiter). This means malformed VDF input could result in incomplete data being parsed without alerting the caller. The parse() call in this code is not wrapped in a try-catch block, unlike the operations that follow it, so incomplete parsing could go undetected.

Comment on lines 109 to 132
Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(command).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)

val process = Runtime.getRuntime().exec(command, envVars.toStringArray(), imageFs.rootDir)
val reader = BufferedReader(process.inputStream.reader())

val output = StringBuilder()

var line: String?
while ((reader.readLine().also { line = it }) != null) {
output.append(line).append("\n")
}

val exitCode = process.waitFor()

// Check if command succeeded
if (exitCode != 0) {
throw RuntimeException("Command execution failed with exit code: $exitCode. Output: $output")
}

// Filter out Wine debug messages and get the actual result
return output.lines()
.joinToString("\n")
.trim()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource leak: Process streams not properly closed.

The BufferedReader and process streams are not closed in a finally block or try-with-resources. Additionally, the error stream is not consumed, which can cause the process to hang if it writes to stderr.

🔧 Suggested fix
     private fun execCommand(command: String) : String {
         val envVars = EnvVars()
         // ... env setup ...
 
         Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(command).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)
 
         val process = Runtime.getRuntime().exec(command, envVars.toStringArray(), imageFs.rootDir)
-        val reader = BufferedReader(process.inputStream.reader())
-
-        val output = StringBuilder()
-
-        var line: String?
-        while ((reader.readLine().also { line = it }) != null) {
-            output.append(line).append("\n")
-        }
-
-        val exitCode = process.waitFor()
-
-        // Check if command succeeded
-        if (exitCode != 0) {
-            throw RuntimeException("Command execution failed with exit code: $exitCode. Output: $output")
+        
+        return try {
+            // Consume both streams to prevent blocking
+            val output = process.inputStream.bufferedReader().use { it.readText() }
+            val errorOutput = process.errorStream.bufferedReader().use { it.readText() }
+            
+            val exitCode = process.waitFor()
+            
+            if (exitCode != 0) {
+                Timber.tag("SteamTokenLogin").e("Command stderr: $errorOutput")
+                throw RuntimeException("Command execution failed with exit code: $exitCode. Output: $output")
+            }
+            
+            output.trim()
+        } finally {
+            process.destroy()
         }
-
-        // Filter out Wine debug messages and get the actual result
-        return output.lines()
-            .joinToString("\n")
-            .trim()
     }
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt around lines 109 -
132, The method in SteamTokenLogin that executes external commands (uses
ProcessHelper.splitCommand and Runtime.getRuntime().exec) leaks resources
because the BufferedReader and the process streams aren’t closed and stderr
isn’t consumed; fix it by wrapping the process streams in try-with-resources /
Kotlin use blocks (e.g., reader =
BufferedReader(process.inputStream.reader()).use { ... }) and also read/consume
the process.errorStream (either synchronously into a String or on a background
thread) so the process cannot block, ensure process.outputStream is closed, and
only call process.waitFor() after streams are consumed and all readers are
closed; include logging of stderr and ensure process.destroy() /
destroyForcibly() is used on exceptional paths.

Comment on lines +166 to +209
do {
try {
encoded = obfuscateToken("$token$NULL_CHAR", mtbf)
} catch (_: Exception) {
mtbf = kotlin.random.Random.nextLong(minMTBF, maxMTBF)
}
} while (encoded == "")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential infinite loop if obfuscation consistently fails.

The do-while loop retries indefinitely when obfuscateToken throws an exception. If there's a systematic issue (e.g., invalid input), this will hang. Consider adding a retry limit.

🔧 Suggested fix
+        val maxRetries = 10
+        var retryCount = 0
         // Try to encode the token until it get a value
         do {
             try {
                 encoded = obfuscateToken("$token$NULL_CHAR", mtbf)
             } catch (_: Exception) {
                 mtbf = kotlin.random.Random.nextLong(minMTBF, maxMTBF)
+                retryCount++
+                if (retryCount >= maxRetries) {
+                    throw RuntimeException("Failed to obfuscate token after $maxRetries attempts")
+                }
             }
         } while (encoded == "")

Comment on lines +279 to 349
fun autoLoginUserChanges(imageFs: ImageFs) {
val vdfFileText = SteamService.getLoginUsersVdfOauth(
steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
account = PrefManager.username,
refreshToken = PrefManager.refreshToken,
accessToken = PrefManager.accessToken, // may be blank
personaName = SteamService.instance?.localPersona?.value?.name!!
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Potential NullPointerException from !! on nullable chain.

The use of !! at line 285 on SteamService.instance?.localPersona?.value?.name!! is unsafe. If any part of this chain is null (e.g., SteamService.instance not initialized, localPersona not yet loaded, or name missing), the app will crash with a NullPointerException.

This risk is amplified now that autoLoginUserChanges is public and may be called from additional contexts.

Suggested fix: Use a safe fallback instead of force-unwrap
     fun autoLoginUserChanges(imageFs: ImageFs) {
+        val personaName = SteamService.instance?.localPersona?.value?.name
+            ?: PrefManager.username  // fallback to username if persona unavailable
         val vdfFileText = SteamService.getLoginUsersVdfOauth(
             steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
             account = PrefManager.username,
             refreshToken = PrefManager.refreshToken,
             accessToken = PrefManager.accessToken,      // may be blank
-            personaName = SteamService.instance?.localPersona?.value?.name!!
+            personaName = personaName
         )

Alternatively, if a persona name is strictly required, consider throwing a descriptive exception or returning early with logging when unavailable.

📝 Committable suggestion

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

Suggested change
fun autoLoginUserChanges(imageFs: ImageFs) {
val vdfFileText = SteamService.getLoginUsersVdfOauth(
steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
account = PrefManager.username,
refreshToken = PrefManager.refreshToken,
accessToken = PrefManager.accessToken, // may be blank
personaName = SteamService.instance?.localPersona?.value?.name!!
)
fun autoLoginUserChanges(imageFs: ImageFs) {
val personaName = SteamService.instance?.localPersona?.value?.name
?: PrefManager.username // fallback to username if persona unavailable
val vdfFileText = SteamService.getLoginUsersVdfOauth(
steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
account = PrefManager.username,
refreshToken = PrefManager.refreshToken,
accessToken = PrefManager.accessToken, // may be blank
personaName = personaName
)
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamUtils.kt around lines 279 - 286,
In autoLoginUserChanges, avoid the unsafe force-unwrap of
SteamService.instance?.localPersona?.value?.name!! when calling
SteamService.getLoginUsersVdfOauth; instead first read and validate the persona
name (e.g., val personaName = SteamService.instance?.localPersona?.value?.name)
and if null either provide a safe fallback (empty string or a default like
"Unknown"), or return early/log and throw a descriptive exception; then pass
that validated personaName into SteamService.getLoginUsersVdfOauth so you never
use !! on the nullable chain.

Comment on lines 27 to 33
else -> {
// Section header
val sectionName = line.trim('"')
if (iterator.hasNext() && iterator.next() == "{") {
result[sectionName] = parseSection(iterator)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unconsumed line when { check fails causes parsing errors.

When the next line is not {, it's consumed by iterator.next() but never processed. This silently corrupts the parse result by skipping data.

🐛 Proposed fix to handle non-brace lines correctly
             else -> {
                 // Section header
                 val sectionName = line.trim('"')
-                if (iterator.hasNext() && iterator.next() == "{") {
-                    result[sectionName] = parseSection(iterator)
+                if (iterator.hasNext()) {
+                    val nextLine = iterator.next()
+                    if (nextLine == "{") {
+                        result[sectionName] = parseSection(iterator)
+                    } else {
+                        // Not a section, treat sectionName as a standalone key
+                        // and re-process nextLine (requires using a peekable iterator or list index)
+                    }
                 }
             }

Consider using a ListIterator or index-based iteration to allow peeking without consuming, or restructure to handle the case where a section header isn't followed by {.

🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/VdfStringParser.kt around lines 27 -
33, The code consumes the next line with iterator.next() while checking for "{"
and drops it when the check fails, skipping data; fix by avoiding consumption
when peeking — e.g., change the iterator type to a ListIterator or switch to an
index-based loop so you can peek the next token without advancing, or if you
must use ListIterator call previous() to unread when the peeked value != "{";
update the code around the section header handling (the sectionName extraction
and the call to parseSection(iterator)) so that a non-"{" next line is not
discarded but remains available for normal parsing.

Adds Steam auto-login using steam-token for automated configuration.

This implementation introduces a two-phase process to manage
`config.vdf` and `local.vdf` files, handling token encryption,
decryption, and JWT validation to ensure secure and persistent login.

Disables Steamless integration and game ticket requests when
launching with Real Steam to avoid conflicts and ensure proper operation.

Implements a mechanism to backup and restore Steam client files before applying modifications. This ensures that the original files can be restored, facilitating a smoother experience.
@joshuatam joshuatam force-pushed the feat/steam-autologin branch from b3586eb to 9414975 Compare January 13, 2026 18:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

342-366: Avoid hard crash in autoLoginUserChanges (personaName !!) and don’t write "null" steamId64.
SteamService.instance?.localPersona?.value?.name!! will crash if persona isn’t loaded yet, and ?.toString() on a nullable can emit "null" into VDF.

Proposed fix
 fun autoLoginUserChanges(imageFs: ImageFs) {
+    val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString()
+        ?: PrefManager.steamUserSteamId64.takeIf { it != 0L }?.toString()
+        ?: "0"
+    val personaName = SteamService.instance?.localPersona?.value?.name
+        ?: PrefManager.username
+
     val vdfFileText = SteamService.getLoginUsersVdfOauth(
-        steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
+        steamId64 = steamId64,
         account = PrefManager.username,
         refreshToken = PrefManager.refreshToken,
         accessToken = PrefManager.accessToken,      // may be blank
-        personaName = SteamService.instance?.localPersona?.value?.name!!
+        personaName = personaName
     )
     val steamConfigDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/config")
     try {
         File(steamConfigDir, "loginusers.vdf").writeText(vdfFileText)
         ...
     } catch (e: Exception) {
         Timber.w("Could not add steam config options: $e")
     }
 }
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt:
- Around line 64-122: The deobfuscate function calls hexStringToByteArray
without validating the input; add pre-checks in hexStringToByteArray (and
similarly where hex decoding is used around lines 124-134) to reject odd-length
strings and any non-hex characters (Character.digit(...) == -1) and throw a
clear IllegalArgumentException (or custom ParseException) before attempting to
decode; update deobfuscate to rely on that validated helper and ensure error
messages include the offending input or its length to aid debugging.

In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt:
- Around line 171-181: encryptToken and decryptToken currently pass commands
prefixed with "wine" into execCommand which itself may add a "wine" prefix on
arm64ec, causing "wine wine ..." failures; change both to call execCommand
without the leading "wine " (e.g. "${imageFs.rootDir}/opt/apps/steam-token.exe
encrypt $login $token" and similarly for decrypt) so execCommand can apply any
necessary prefixing consistently, referencing encryptToken, decryptToken,
execCommand, imageFs.rootDir and login.
- Around line 59-161: execCommand currently reads stdout then stderr which can
deadlock and logs the full finalCommand (risking token leakage); fix by using
ProcessBuilder in execCommand to set command from finalCommand (or
ProcessHelper.splitCommand(finalCommand)), pass environment via
envVars.toStringArray(), set directory to imageFs.rootDir, call
redirectErrorStream(true) to merge streams, read the single combined stream
fully into output, then waitFor and check exitCode; also redact sensitive data
before logging by masking token-like substrings in the finalCommand used in
Timber.tag("SteamTokenLogin").d(...) so logs do not contain refresh/access
tokens.

In @app/src/main/java/app/gamenative/utils/SteamUtils.kt:
- Around line 239-242: The current backup/restore flows use Files.copy(...)
which can throw and bubble up, causing the app to fail to launch; update
backupSteamclientFiles and the corresponding restore/replace flows (the code
around lines 271-314) to wrap file I/O operations in try/catch for IOException
(and general IOException subclasses), log the error with context via the
existing logger rather than rethrowing, and proceed without aborting launch;
ensure any temporary/partial files are handled (delete partial files on failure)
and avoid letting exceptions escape these methods so launch remains resilient.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt (1)

16-62: Avoid per-iteration allocations in obfuscate().
Repeated sliceArray(...) in a loop is quadratic-ish on allocations; use an index pointer over the original ByteArray.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3586eb and 9414975.

📒 Files selected for processing (8)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/VdfStringParser.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/utils/VdfStringParser.kt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.

Applied to files:

  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Applied to files:

  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.

Applied to files:

  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (1)
app/src/main/java/com/winlator/core/envvars/EnvVars.java (1)
  • EnvVars (8-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)

218-220: Confirm it’s OK to always delete extra_dlls on restore.
restoreSteamclientFiles() unconditionally deletes .../Steam/extra_dlls which could remove user overrides.

Also applies to: 699-701


812-817: Account ID fallback logic looks good.
Nice to see account_steamid and accountId fallbacks avoid writing empty values.

app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (3)

1010-1075: Verify the “addController('*') to force controllersLoaded” behavior.
If * becomes a persisted controller entry, it may pollute user profiles.


1698-1711: Ticket fetch gating looks correct.
Avoiding encrypted ticket fetch in real-Steam mode makes sense (Steam itself should handle it).


1834-1838: Check Steam launch flags compatibility with your target Steam build.
The steam.exe -silent ... -applaunch string is plausible, but flag support varies by Steam version and platform.

app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (1)

171-190: Verify the token-helper executable path + JWT expiry semantics.

  • Pathing: you’re invoking steam-token.exe via a Unix path; if this is flaky under Wine, prefer Z:\\opt\\apps\\steam-token.exe.
  • JWT.isExpired(leewaySeconds) semantics must match your intended “expire within 1 day” logic.

Also applies to: 242-266

Comment on lines +1111 to +1119
if (container.isLaunchRealSteam && !SteamService.isFileInstallable(context, "steam-token.tzst")) {
setLoadingMessage("Downloading steam-token")
SteamService.downloadFile(
onDownloadProgress = { setLoadingProgress(it / 1.0f) },
this,
context = context,
"steam-token.tzst"
).await()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prefer string resources for “Downloading steam-token” and ensure failures are surfaced.
This path is good, but the UI string is hard-coded and download errors should map to a clear dialog (vs silent failure/exception).

Comment on lines +1541 to +1552
if (container.isLaunchRealSteam) {
SteamTokenLogin(
context = context,
steamId = PrefManager.steamUserSteamId64.toString(),
login = PrefManager.username,
token = PrefManager.refreshToken,
imageFs = imageFs,
container = container,
isArm64EC = xServerState.value.wineInfo.isArm64EC,
wineProfile = contentsManager.getProfileByEntryName(container.wineVersion),
).setupSteamFiles()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard SteamTokenLogin setup against missing creds and upstream crashes.
This will run on real-Steam launches, so it should no-op (or error clearly) when steamUserSteamId64 == 0 / token empty, and it should not be able to crash the whole setup thread (especially given current autoLoginUserChanges !!).

Comment on lines +64 to +122
fun deobfuscate(data: String, key: Long): String {
val dataBytes = hexStringToByteArray(data)

// Check header
if (dataBytes.size < 4 ||
dataBytes[0] != 0x02.toByte() ||
dataBytes[1] != 0x00.toByte() ||
dataBytes[2] != 0x00.toByte() ||
dataBytes[3] != 0x00.toByte()) {
throw Exception("wrong type of data")
}

val csumData = dataBytes.sliceArray(dataBytes.size - 4 until dataBytes.size)
var ctext = dataBytes.sliceArray(4 until dataBytes.size - 4)
val ptext = mutableListOf<Byte>()

var k1 = (key shr 0x1f).toInt()
var k2 = key.toInt()
var csum = 0

while (ctext.size >= 4) {
k1 = (k1 + 0x25fe6761) and 0xffffffff.toInt()
k2 = (k2 + 1) and 0xffffffff.toInt()

val d = ByteBuffer.wrap(ctext.sliceArray(0..3))
.order(ByteOrder.LITTLE_ENDIAN)
.int

val t = obf[k2 % 0x11] xor k1 xor d
csum = (csum + t) and 0xffffffff.toInt()

val tBytes = ByteBuffer.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(t)
.array()

ptext.addAll(tBytes.toList())

ctext = ctext.sliceArray(4 until ctext.size)
}

// Add remaining bytes
ptext.addAll(ctext.toList())

k1 = (k1 + 0x25fe6761) and 0xffffffff.toInt()
k2 = (k2 + 1) and 0xffffffff.toInt()
val t = obf[k2 % 0x11] xor k1 xor csum

val tBytes = ByteBuffer.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(t)
.array()

if (!tBytes.contentEquals(csumData)) {
throw Exception("bad checksum! ${t.toString(16)}")
}

return ptext.toByteArray().decodeToString()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate hex input (odd length / non-hex chars) before decoding.
hexStringToByteArray() can crash on odd length and accepts invalid digits (Character.digit(...) == -1).

Proposed fix
 private fun hexStringToByteArray(hex: String): ByteArray {
     val cleanHex = hex.replace(" ", "")
     val len = cleanHex.length
+    require(len % 2 == 0) { "hex string must have even length" }
     val data = ByteArray(len / 2)

     for (i in 0 until len step 2) {
-        data[i / 2] = ((Character.digit(cleanHex[i], 16) shl 4) +
-                Character.digit(cleanHex[i + 1], 16)).toByte()
+        val hi = Character.digit(cleanHex[i], 16)
+        val lo = Character.digit(cleanHex[i + 1], 16)
+        require(hi >= 0 && lo >= 0) { "invalid hex char at index $i" }
+        data[i / 2] = ((hi shl 4) + lo).toByte()
     }
     return data
 }

Also applies to: 124-134

🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenHelper.kt around lines 64 -
122, The deobfuscate function calls hexStringToByteArray without validating the
input; add pre-checks in hexStringToByteArray (and similarly where hex decoding
is used around lines 124-134) to reject odd-length strings and any non-hex
characters (Character.digit(...) == -1) and throw a clear
IllegalArgumentException (or custom ParseException) before attempting to decode;
update deobfuscate to rely on that validated helper and ensure error messages
include the offending input or its length to aid debugging.

Comment on lines +59 to +161
private fun execCommand(command: String) : String {
val envVars = EnvVars()
// Common environment variables for both container types
envVars.put("WINEDEBUG", "-all")
envVars.put("WINEPREFIX", imageFs.wineprefix)

if (container.containerVariant == Container.BIONIC) {
envVars.put("HOME", imageFs.home_path)
envVars.put("USER", ImageFs.USER)
envVars.put("TMPDIR", imageFs.rootDir.path + "/tmp")
envVars.put("DISPLAY", ":0")
envVars.put("PATH", winePath + ":" + imageFs.rootDir.path + "/usr/bin")
envVars.put("LD_LIBRARY_PATH", imageFs.rootDir.path + "/usr/lib" + ":" + "/system/lib64")
envVars.put("FONTCONFIG_PATH", imageFs.rootDir.path + "/usr/etc/fonts")
envVars.put("XDG_DATA_DIRS", imageFs.rootDir.path + "/usr/share")
envVars.put("XDG_CONFIG_DIRS", imageFs.rootDir.path + "/usr/etc/xdg")
envVars.put("ANDROID_SYSVSHM_SERVER", imageFs.rootDir.path + UnixSocketConfig.SYSVSHM_SERVER_PATH)
envVars.put("PREFIX", imageFs.rootDir.path + "/usr")
envVars.put("WINE_NO_DUPLICATE_EXPLORER", "1")
envVars.put("WINE_DISABLE_FULLSCREEN_HACK", "1")

if (!isArm64EC) {
// Set execute permissions.
val box64File: File = File(imageFs.rootDir, "usr/bin/box64")
if (box64File.exists()) {
FileUtils.chmod(box64File, 493) // 0755
}
}
} else {
if (!File(imageFs.rootDir.path, "/usr/local/bin/box64").exists()) {
// Extract box64 if it doesn't exist yet
TarCompressorUtils.extract(
TarCompressorUtils.Type.ZSTD,
context.assets,
"box86_64/box64-" + container.box64Version + ".tzst",
imageFs.rootDir,
)
}

envVars.put("HOME", imageFs.home_path)
envVars.put("USER", ImageFs.USER)
envVars.put("TMPDIR", imageFs.rootDir.path + "/tmp")
envVars.put("DISPLAY", ":0")
envVars.put(
"PATH",
winePath + ":" +
imageFs.rootDir.path + "/usr/bin:" +
imageFs.rootDir.path + "/usr/local/bin",
)
envVars.put("LD_LIBRARY_PATH", imageFs.rootDir.path + "/usr/lib")
envVars.put("BOX64_LD_LIBRARY_PATH", imageFs.rootDir.path + "/usr/lib/x86_64-linux-gnu")
envVars.put("ANDROID_SYSVSHM_SERVER", imageFs.rootDir.path + UnixSocketConfig.SYSVSHM_SERVER_PATH)
envVars.put("FONTCONFIG_PATH", imageFs.rootDir.path + "/usr/etc/fonts")

if (File(imageFs.glibc64Dir, "libandroid-sysvshm.so").exists() ||
File(imageFs.glibc32Dir, "libandroid-sysvshm.so").exists()) {
envVars.put("LD_PRELOAD", "libredirect.so libandroid-sysvshm.so")
}

envVars.put("WINEESYNC_WINLATOR", "1")
}

val finalCommand = if (container.containerVariant == Container.BIONIC) {
if (isArm64EC) {
winePath + "/" + command
} else {
imageFs.binDir.path + "/box64" + " " + command
}
} else {
imageFs.rootDir.path + "/usr/local/bin/box64" + " " + command
}

Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(finalCommand).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)

val process = Runtime.getRuntime().exec(finalCommand, envVars.toStringArray(), imageFs.rootDir)
val reader = BufferedReader(process.inputStream.reader())
val errorReader = BufferedReader(process.errorStream.reader())

val output = StringBuilder()
val errorOutput = StringBuilder()

var line: String?
while ((reader.readLine().also { line = it }) != null) {
output.append(line).append("\n")
}

var errorLine: String?
while ((errorReader.readLine().also { errorLine = it }) != null) {
errorOutput.append(errorLine).append("\n")
}

val exitCode = process.waitFor()

// Check if command succeeded
if (exitCode != 0) {
throw RuntimeException("Command execution failed with exit code: $exitCode. Output: $output, Error Output: $errorOutput")
}

// Filter out Wine debug messages and get the actual result
return output.lines()
.joinToString("\n")
.trim()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix execCommand deadlock risk + redact secrets from logs.
Reading stdout then stderr can hang if the other stream’s buffer fills; plus finalCommand logging can leak refresh tokens.

Proposed fix (ProcessBuilder + merged streams + redacted logging)
 private fun execCommand(command: String) : String {
     val envVars = EnvVars()
     // Common environment variables for both container types
     envVars.put("WINEDEBUG", "-all")
     envVars.put("WINEPREFIX", imageFs.wineprefix)
@@
-    val finalCommand = if (container.containerVariant == Container.BIONIC) {
-        if (isArm64EC) {
-            winePath + "/" + command
-        } else {
-            imageFs.binDir.path + "/box64" + " " + command
-        }
-    } else {
-        imageFs.rootDir.path + "/usr/local/bin/box64" + " " + command
-    }
-
-    Timber.tag("SteamTokenLogin").d("Executing: " + ProcessHelper.splitCommand(finalCommand).contentToString() + ", " + envVars.toStringArray().contentToString() + ", " + imageFs.rootDir)
-
-    val process = Runtime.getRuntime().exec(finalCommand, envVars.toStringArray(), imageFs.rootDir)
-    val reader = BufferedReader(process.inputStream.reader())
-    val errorReader = BufferedReader(process.errorStream.reader())
-
-    val output = StringBuilder()
-    val errorOutput = StringBuilder()
-
-    var line: String?
-    while ((reader.readLine().also { line = it }) != null) {
-        output.append(line).append("\n")
-    }
-
-    var errorLine: String?
-    while ((errorReader.readLine().also { errorLine = it }) != null) {
-        errorOutput.append(errorLine).append("\n")
-    }
-
-    val exitCode = process.waitFor()
-
-    // Check if command succeeded
-    if (exitCode != 0) {
-        throw RuntimeException("Command execution failed with exit code: $exitCode. Output: $output, Error Output: $errorOutput")
-    }
-
-    // Filter out Wine debug messages and get the actual result
-    return output.lines()
-        .joinToString("\n")
-        .trim()
+    val finalCommand = when {
+        container.containerVariant == Container.BIONIC && isArm64EC -> command
+        container.containerVariant == Container.BIONIC -> "${imageFs.binDir.path}/box64 $command"
+        else -> "${imageFs.rootDir.path}/usr/local/bin/box64 $command"
+    }
+
+    // Don't log secrets (token may be in args)
+    Timber.tag("SteamTokenLogin").d("Executing steam-token helper (redacted)")
+
+    val argv = ProcessHelper.splitCommand(finalCommand).toList()
+    val pb = ProcessBuilder(argv)
+        .directory(imageFs.rootDir)
+        .redirectErrorStream(true)
+
+    // EnvVars exposes keys via iterator()
+    for (k in envVars) pb.environment()[k] = envVars.get(k)
+
+    val process = pb.start()
+    val output = process.inputStream.bufferedReader().readText().trim()
+    val exitCode = process.waitFor()
+    if (exitCode != 0) {
+        throw RuntimeException("Command failed with exit code=$exitCode. Output: $output")
+    }
+    return output
 }
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt around lines 59 -
161, execCommand currently reads stdout then stderr which can deadlock and logs
the full finalCommand (risking token leakage); fix by using ProcessBuilder in
execCommand to set command from finalCommand (or
ProcessHelper.splitCommand(finalCommand)), pass environment via
envVars.toStringArray(), set directory to imageFs.rootDir, call
redirectErrorStream(true) to merge streams, read the single combined stream
fully into output, then waitFor and check exitCode; also redact sensitive data
before logging by masking token-like substrings in the finalCommand used in
Timber.tag("SteamTokenLogin").d(...) so logs do not contain refresh/access
tokens.

Comment on lines +171 to +181
private fun encryptToken(token: String) : String {
// Simple encoding (not as secure as Windows CryptProtectData, but cross-platform)
// run steam-token.exe from extractDir and return the result
return execCommand("wine ${imageFs.rootDir}/opt/apps/steam-token.exe encrypt $login $token")
}

private fun decryptToken(vdfValue: String) : String {
// Simple decoding (not as secure as Windows CryptProtectData, but cross-platform)
// run steam-token.exe from extractDir and return the result
return execCommand("wine ${imageFs.rootDir}/opt/apps/steam-token.exe decrypt $login $vdfValue")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

arm64ec path likely runs wine wine ... today (breaks encrypt/decrypt).
Because execCommand prefixes the command on arm64ec, passing a command that already starts with wine becomes double-wine.

Minimal fix option (keep “wine …” commands; avoid prefixing on arm64ec)
-    val finalCommand = if (container.containerVariant == Container.BIONIC) {
-        if (isArm64EC) {
-            winePath + "/" + command
-        } else {
-            imageFs.binDir.path + "/box64" + " " + command
-        }
-    } else {
-        imageFs.rootDir.path + "/usr/local/bin/box64" + " " + command
-    }
+    val finalCommand = if (container.containerVariant == Container.BIONIC) {
+        if (isArm64EC) {
+            // rely on PATH (already includes winePath) so we don't produce "wine wine ..."
+            command
+        } else {
+            imageFs.binDir.path + "/box64" + " " + command
+        }
+    } else {
+        imageFs.rootDir.path + "/usr/local/bin/box64" + " " + command
+    }
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt around lines 171 -
181, encryptToken and decryptToken currently pass commands prefixed with "wine"
into execCommand which itself may add a "wine" prefix on arm64ec, causing "wine
wine ..." failures; change both to call execCommand without the leading "wine "
(e.g. "${imageFs.rootDir}/opt/apps/steam-token.exe encrypt $login $token" and
similarly for decrypt) so execCommand can apply any necessary prefixing
consistently, referencing encryptToken, decryptToken, execCommand,
imageFs.rootDir and login.

Comment on lines 358 to 412
fun phase2LocalConfig() {
try {
// Extract steam-token.tzst
val extractDir = File(imageFs.rootDir, "/opt/apps/")
Files.createDirectories(extractDir.toPath())
TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, File(imageFs.filesDir, "steam-token.tzst"), extractDir)

val localSteamDir = File(imageFs.wineprefix, "drive_c/users/${ImageFs.USER}/AppData/Local/Steam").toPath()
Files.createDirectories(localSteamDir)

// Remove local.vdf
if (localSteamDir.resolve("local.vdf").exists()) {
val vdfContent = FileUtils.readString(localSteamDir.resolve("local.vdf").toFile())
val vdfData = VdfStringParser().parse(vdfContent)
val machineUserConfigStore = vdfData["MachineUserConfigStore"] as? Map<String, Any>
val software = machineUserConfigStore?.get("Software") as? Map<String, Any>
val valve = software?.get("Valve") as? Map<String, Any>
val steam = valve?.get("Steam") as? Map<String, Any>
val connectCache = steam?.get("ConnectCache") as? Map<String, Any>

// Get the first key-value pair from ConnectCache (the encrypted token)
val connectCacheValue = connectCache?.get(hdr()) as? String
if (connectCacheValue != null) {
try {
val dToken = decryptToken(connectCacheValue.trimEnd(NULL_CHAR))
val savedJWT = JWT(dToken)

// If the saved JWT is not expired, do not override it
if (!savedJWT.isExpired(TOKEN_EXPIRE_TIME)) {
Timber.tag("SteamTokenLogin").d("Saved JWT is not expired, do not override local.vdf")
return
}
} catch (e: Exception) {
Timber.tag("SteamTokenLogin").d("An unexpected error occurred: ${e.message}")
e.printStackTrace()
}
}
}

Timber.tag("SteamTokenLogin").d("Overriding local.vdf")

Files.write(
localSteamDir.resolve("local.vdf"),
createLocalVdf().toByteArray(),
)

killWineServer()

// Set permissions
FileUtils.chmod(File(localSteamDir.absolutePathString(), "local.vdf"), 505) // 0771
} catch (e: Exception) {
Timber.tag("SteamTokenLogin").d("An unexpected error occurred: ${e.message}")
e.printStackTrace()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Phase2: handle missing steam-token.tzst explicitly (don’t just throw/printStackTrace).
If this file isn’t present (alternate launch paths, partial downloads), you should fail with a clear error and skip modifying VDFs.

Proposed fix
 fun phase2LocalConfig() {
     try {
         // Extract steam-token.tzst
         val extractDir = File(imageFs.rootDir, "/opt/apps/")
         Files.createDirectories(extractDir.toPath())
-        TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, File(imageFs.filesDir, "steam-token.tzst"), extractDir)
+        val tokenArchive = File(imageFs.filesDir, "steam-token.tzst")
+        require(tokenArchive.exists()) { "steam-token.tzst missing; cannot configure Steam token login" }
+        TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, tokenArchive, extractDir)
@@
     } catch (e: Exception) {
-        Timber.tag("SteamTokenLogin").d("An unexpected error occurred: ${e.message}")
-        e.printStackTrace()
+        Timber.tag("SteamTokenLogin").e(e, "Phase2 local.vdf setup failed")
     }
 }

Comment on lines +239 to +242

// Make a backup before extracting
backupSteamclientFiles(context, steamAppId)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make steamclient backup/restore resilient to I/O failures (don’t fail launch).
Files.copy(...) can throw; today that propagates out of replace/restore flows.

Proposed fix
 fun backupSteamclientFiles(context: Context, steamAppId: Int) {
     val imageFs = ImageFs.find(context)
     var backupCount = 0
     val backupDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steamclient_backup")
     backupDir.mkdirs()

     steamClientFiles().forEach { file ->
         val dll = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/$file")
         if (dll.exists()) {
-            Files.copy(dll.toPath(), File(backupDir, "$file.orig").toPath(), StandardCopyOption.REPLACE_EXISTING)
-            backupCount++
+            runCatching {
+                Files.copy(
+                    dll.toPath(),
+                    File(backupDir, "$file.orig").toPath(),
+                    StandardCopyOption.REPLACE_EXISTING
+                )
+                backupCount++
+            }.onFailure { e ->
+                Timber.w(e, "Failed to back up steam client file: $file")
+            }
         }
     }

     Timber.i("Finished backupSteamclientFiles for appId: $steamAppId. Backed up $backupCount file(s)")
 }

 fun restoreSteamclientFiles(context: Context, steamAppId: Int) {
     val imageFs = ImageFs.find(context)
     var restoredCount = 0
     val origDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")

     val backupDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steamclient_backup")
     if (backupDir.exists()) {
         steamClientFiles().forEach { file ->
             val dll = File(backupDir, "$file.orig")
             if (dll.exists()) {
-                Files.copy(dll.toPath(), File(origDir, file).toPath(), StandardCopyOption.REPLACE_EXISTING)
-                restoredCount++
+                runCatching {
+                    Files.copy(
+                        dll.toPath(),
+                        File(origDir, file).toPath(),
+                        StandardCopyOption.REPLACE_EXISTING
+                    )
+                    restoredCount++
+                }.onFailure { e ->
+                    Timber.w(e, "Failed to restore steam client file: $file")
+                }
             }
         }
     }

     val extraDllDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/extra_dlls")
     if (extraDllDir.exists()) {
         extraDllDir.deleteRecursively()
     }

     Timber.i("Finished restoreSteamclientFiles for appId: $steamAppId. Restored $restoredCount file(s)")
 }

Also applies to: 271-314

🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/utils/SteamUtils.kt around lines 239 - 242,
The current backup/restore flows use Files.copy(...) which can throw and bubble
up, causing the app to fail to launch; update backupSteamclientFiles and the
corresponding restore/replace flows (the code around lines 271-314) to wrap file
I/O operations in try/catch for IOException (and general IOException
subclasses), log the error with context via the existing logger rather than
rethrowing, and proceed without aborting launch; ensure any temporary/partial
files are handled (delete partial files on failure) and avoid letting exceptions
escape these methods so launch remains resilient.

Replaces custom VDF parsing logic with `javasteam` library's `KeyValue` class.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt`:
- Around line 283-316: The VDF parsing/lookup uses force-unwraps and chained
access (KeyValue.loadFromString(vdfContent)!!,
vdfData["Software"]["Valve"]["Steam"]["MTBF"].value,
vdfData["Software"]["Valve"]["Steam"]["ConnectCache"][hdr()].value) which can
throw on malformed/missing data; wrap the KeyValue.loadFromString and subsequent
lookups in a try-catch, avoid !! by treating loadFromString as nullable,
null-check vdfData and each chained key before accessing .value, and on any
exception or missing keys set safe fallbacks (e.g., shouldWriteConfig = true or
shouldProcessPhase2 as appropriate) and log the error with
Timber.tag("SteamTokenLogin") so the app doesn't crash.
♻️ Duplicate comments (4)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (4)

132-162: Process handling issues previously identified.

The issues with this method (potential deadlock from sequential stream reading, resource leak from unclosed streams, and sensitive data in logs at line 132) were flagged in previous reviews. Please address those comments—using ProcessBuilder with redirectErrorStream(true) and use {} blocks for stream handling would resolve all three concerns.


172-182: Verify arm64ec command construction (previously flagged).

The past review raised concerns about potential double-wine issues on arm64ec. Please verify that the command construction in execCommand (line 124: winePath + "/" + command) produces the correct executable path when command already starts with "wine". If winePath is /path/bin, the result "/path/bin/wine /opt/apps/steam-token.exe..." should work, but this warrants explicit testing on arm64ec.


204-213: Previously flagged: infinite loop risk and secret logging.

Lines 204-210: The unbounded retry loop was flagged in a past review—add a retry limit. Lines 212-213: Logging mtbf and encoded values exposes token-derived secrets—redact or remove these logs.


352-399: Previously flagged: validate steam-token.tzst existence.

The past review noted that line 357 should check if steam-token.tzst exists before extraction, failing with a clear error message rather than letting TarCompressorUtils.extract throw. Additionally, consider using Timber.e(e, message) instead of e.printStackTrace() for consistent logging (lines 379, 397).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9414975 and bed0bbc.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (1)
app/src/main/java/com/winlator/core/envvars/EnvVars.java (1)
  • EnvVars (8-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt (5)

29-58: LGTM on constructor and initialization.

The constructor properly handles different container variants for wine path resolution, and the hdr() function correctly generates a CRC32-based identifier for ConnectCache keys.


164-170: LGTM on killWineServer.

Appropriate error handling for a cleanup operation that shouldn't halt execution on failure.


184-190: LGTM on token obfuscation wrappers.

Clean delegation to SteamTokenHelper.


243-267: LGTM on createLocalVdf.

The VDF structure is correctly formatted, and the dependency on encryptToken is satisfied by phase2LocalConfig extracting the token archive first.


324-346: Verify file permissions are intentional.

The chmod 505 (octal 0771 = rwxrwx--x) grants group execute permission on config files. If this is intentional for Wine compatibility, consider adding a brief comment explaining why.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +283 to +316
if (Files.exists(configVdfPath)) {
val vdfContent = FileUtils.readString(configVdfPath.toFile())
if (vdfContent.contains("ConnectCache")) {
// Find the value of ConnectCache
// Use structured parsing:
val vdfData = KeyValue.loadFromString(vdfContent)!!
val mtbf = vdfData["Software"]["Valve"]["Steam"]["MTBF"].value
val connectCacheValue = vdfData["Software"]["Valve"]["Steam"]["ConnectCache"][hdr()].value

if (mtbf != null && connectCacheValue != null) {
try {
val dToken = deobfuscateToken(connectCacheValue.trimEnd(NULL_CHAR), mtbf.toLong()).trimEnd(NULL_CHAR)
if (JWT(dToken).isExpired(TOKEN_EXPIRE_TIME)) {
Timber.tag("SteamTokenLogin").d("Saved JWT expired, overriding config.vdf")
// If the saved JWT is expired, override it
shouldWriteConfig = true
} else {
Timber.tag("SteamTokenLogin").d("Saved JWT is not expired, do not override config.vdf")
shouldWriteConfig = false
}
} catch (_: Exception) {
Timber.tag("SteamTokenLogin").d("Cannot parse saved JWT, overriding config.vdf")
shouldWriteConfig = true
}
} else {
if (mtbf == null && connectCacheValue == null) {
Timber.tag("SteamTokenLogin").d("MTBF and ConnectCache not found, overriding config.vdf")
shouldWriteConfig = true
} else if (mtbf != null) {
Timber.tag("SteamTokenLogin").d("MTBF exists but ConnectCache not found, it is an updated steam client, processing phase 2")
shouldWriteConfig = false
shouldProcessPhase2 = true
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap VDF parsing in try-catch to prevent crashes.

Lines 288-290 perform force-unwrap (!!) and chained KeyValue access outside any try-catch. If the VDF file is malformed or missing expected keys (possible after Steam updates or file corruption), this will throw and crash the app.

Proposed fix
         if (Files.exists(configVdfPath)) {
             val vdfContent = FileUtils.readString(configVdfPath.toFile())
             if (vdfContent.contains("ConnectCache")) {
-                // Find the value of ConnectCache
-                // Use structured parsing:
-                val vdfData = KeyValue.loadFromString(vdfContent)!!
-                val mtbf = vdfData["Software"]["Valve"]["Steam"]["MTBF"].value
-                val connectCacheValue = vdfData["Software"]["Valve"]["Steam"]["ConnectCache"][hdr()].value
-
-                if (mtbf != null && connectCacheValue != null) {
-                    try {
+                try {
+                    // Use structured parsing:
+                    val vdfData = KeyValue.loadFromString(vdfContent)
+                        ?: throw IllegalStateException("Failed to parse config.vdf")
+                    val mtbf = vdfData["Software"]["Valve"]["Steam"]["MTBF"].value
+                    val connectCacheValue = vdfData["Software"]["Valve"]["Steam"]["ConnectCache"][hdr()].value
+
+                    if (mtbf != null && connectCacheValue != null) {
                         val dToken = deobfuscateToken(connectCacheValue.trimEnd(NULL_CHAR), mtbf.toLong()).trimEnd(NULL_CHAR)
                         if (JWT(dToken).isExpired(TOKEN_EXPIRE_TIME)) {
                             Timber.tag("SteamTokenLogin").d("Saved JWT expired, overriding config.vdf")
-                            // If the saved JWT is expired, override it
                             shouldWriteConfig = true
                         } else {
                             Timber.tag("SteamTokenLogin").d("Saved JWT is not expired, do not override config.vdf")
                             shouldWriteConfig = false
                         }
-                    } catch (_: Exception) {
-                        Timber.tag("SteamTokenLogin").d("Cannot parse saved JWT, overriding config.vdf")
+                    } else if (mtbf != null) {
+                        Timber.tag("SteamTokenLogin").d("MTBF exists but ConnectCache not found, processing phase 2")
+                        shouldWriteConfig = false
+                        shouldProcessPhase2 = true
+                    } else {
+                        Timber.tag("SteamTokenLogin").d("MTBF and/or ConnectCache not found, overriding config.vdf")
                         shouldWriteConfig = true
                     }
-                } else {
-                    // ... rest of the else block
-                }
+                } catch (e: Exception) {
+                    Timber.tag("SteamTokenLogin").d("Failed to parse config.vdf: ${e.message}, overriding")
+                    shouldWriteConfig = true
+                }
             } else if (vdfContent.contains("MTBF")) {
🤖 Prompt for AI Agents
In `@app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt` around lines 283 -
316, The VDF parsing/lookup uses force-unwraps and chained access
(KeyValue.loadFromString(vdfContent)!!,
vdfData["Software"]["Valve"]["Steam"]["MTBF"].value,
vdfData["Software"]["Valve"]["Steam"]["ConnectCache"][hdr()].value) which can
throw on malformed/missing data; wrap the KeyValue.loadFromString and subsequent
lookups in a try-catch, avoid !! by treating loadFromString as nullable,
null-check vdfData and each chained key before accessing .value, and on any
exception or missing keys set safe fallbacks (e.g., shouldWriteConfig = true or
shouldProcessPhase2 as appropriate) and log the error with
Timber.tag("SteamTokenLogin") so the app doesn't crash.

Dynamically updates or creates the localconfig.vdf file to inject command-line arguments into Steam launch options.
This allows the application to pass container-specific parameters to the launched game, enabling autologin functionality.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 681-684: The call to updateOrModifyLocalConfig is force-unwrapping
SteamService.userSteamId (SteamService.userSteamId!!.accountID) which can throw
an NPE; change this to safely access userSteamId (e.g., guard/early return if
null or use a fallback value) before calling updateOrModifyLocalConfig, and pass
a non-null account ID (or skip the call) so updateOrModifyLocalConfig(imageFs,
container, steamAppId.toString(), /* safe account id */) never receives a
forced-null access.
♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)

272-315: I/O failures in backup/restore can crash the launch flow.

The Files.copy() calls at lines 283 and 303 can throw IOException, which will propagate up and potentially abort the game launch. Wrap each copy operation in runCatching and log failures instead of rethrowing.

Suggested resilient pattern
 steamClientFiles().forEach { file ->
     val dll = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/$file")
     if (dll.exists()) {
-        Files.copy(dll.toPath(), File(backupDir, "$file.orig").toPath(), StandardCopyOption.REPLACE_EXISTING)
-        backupCount++
+        runCatching {
+            Files.copy(dll.toPath(), File(backupDir, "$file.orig").toPath(), StandardCopyOption.REPLACE_EXISTING)
+            backupCount++
+        }.onFailure { e ->
+            Timber.w(e, "Failed to back up steam client file: $file")
+        }
     }
 }

Apply the same pattern to restoreSteamclientFiles.


343-350: Potential NullPointerException from force-unwrap on nullable chain.

Line 349 uses !! on SteamService.instance?.localPersona?.value?.name, which will crash if any part of the chain is null. This risk is amplified now that autoLoginUserChanges is public.

Suggested safe fallback
 fun autoLoginUserChanges(imageFs: ImageFs) {
+    val personaName = SteamService.instance?.localPersona?.value?.name
+        ?: PrefManager.username  // fallback if persona unavailable
     val vdfFileText = SteamService.getLoginUsersVdfOauth(
         steamId64 = SteamService.userSteamId?.convertToUInt64().toString(),
         account = PrefManager.username,
         refreshToken = PrefManager.refreshToken,
         accessToken = PrefManager.accessToken,
-        personaName = SteamService.instance?.localPersona?.value?.name!!
+        personaName = personaName
     )
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

1128-1139: Consider explicit null checks instead of chained !! for better error diagnostics.

While the outer try-catch prevents crashes, the chained force-unwraps at lines 1129-1130 (vdfContent!!, loadFromString(...)!!) make it harder to diagnose which step failed. Additionally, the VDF navigation at line 1131 assumes a specific structure that may not exist.

Suggested improvement
         if (localConfigFile.exists()) {
-            val vdfContent = FileUtils.readFileAsString(localConfigFile.absolutePath)
-            val vdfData = KeyValue.loadFromString(vdfContent!!)!!
+            val vdfContent = FileUtils.readFileAsString(localConfigFile.absolutePath)
+            if (vdfContent == null) {
+                Timber.w("Failed to read localconfig.vdf")
+                return
+            }
+            val vdfData = KeyValue.loadFromString(vdfContent)
+            if (vdfData == null) {
+                Timber.w("Failed to parse localconfig.vdf")
+                return
+            }
             val app = vdfData["Software"]["Valve"]["Steam"]["apps"][appId]
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bed0bbc and eae3c52.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)

817-822: LGTM!

The safe access pattern with fallbacks for accountSteamId and accountId is correctly implemented.


261-270: LGTM!

Clean utility function returning the list of Steam client files.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +681 to +684

// Update or modify localconfig.vdf
updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), SteamService.userSteamId!!.accountID.toString())

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential NullPointerException from force-unwrap on userSteamId.

SteamService.userSteamId!! at line 683 will throw NPE if userSteamId is null. Use safe access with a fallback or early return.

Suggested fix
-    // Update or modify localconfig.vdf
-    updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), SteamService.userSteamId!!.accountID.toString())
+    // Update or modify localconfig.vdf
+    val steamAccountId = SteamService.userSteamId?.accountID?.toString()
+    if (steamAccountId != null) {
+        updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), steamAccountId)
+    } else {
+        Timber.w("Cannot update localconfig.vdf: userSteamId is null")
+    }
📝 Committable suggestion

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

Suggested change
// Update or modify localconfig.vdf
updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), SteamService.userSteamId!!.accountID.toString())
// Update or modify localconfig.vdf
val steamAccountId = SteamService.userSteamId?.accountID?.toString()
if (steamAccountId != null) {
updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), steamAccountId)
} else {
Timber.w("Cannot update localconfig.vdf: userSteamId is null")
}
🤖 Prompt for AI Agents
In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt` around lines 681 - 684,
The call to updateOrModifyLocalConfig is force-unwrapping
SteamService.userSteamId (SteamService.userSteamId!!.accountID) which can throw
an NPE; change this to safely access userSteamId (e.g., guard/early return if
null or use a fallback value) before calling updateOrModifyLocalConfig, and pass
a non-null account ID (or skip the call) so updateOrModifyLocalConfig(imageFs,
container, steamAppId.toString(), /* safe account id */) never receives a
forced-null access.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants