Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/src/main/java/app/gamenative/enums/SpecialGameSaveMapping.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app.gamenative.enums

/**
* Configuration for game-specific save location mappings
*
* This data class defines how to create symlinks for games that store saves
* in non-standard locations or need compatibility mappings.
*
* @property appId The Steam app ID
* @property pathType Which PathType to use as the base path (e.g., WinAppDataLocal, WinMyDocuments)
* @property sourceRelativePath Relative path from the base path to the actual save location
* @property targetRelativePath Relative path where the symlink should be created
* @property description Human-readable game name for logging
*
* Supports placeholders in paths:
* - {64BitSteamID} - Replaced with the user's 64-bit Steam ID
* - {Steam3AccountID} - Replaced with the user's Steam3 account ID
*/
data class SpecialGameSaveMapping(
val appId: Int,
val pathType: PathType,
val sourceRelativePath: String,
val targetRelativePath: String,
val description: String
) {
companion object {
/**
* Registry of game-specific save location mappings
* Add new entries here for games that need save folder symlinks
*/
val registry = listOf(
SpecialGameSaveMapping(
appId = 2680010,
pathType = PathType.WinAppDataLocal,
sourceRelativePath = "The First Berserker Khazan/Saved/SaveGames/{64BitSteamID}",
targetRelativePath = "BBQ/Saved/SaveGames",
description = "The First Berserker Khazan"
)
// Add more game mappings here as needed
)
}
}
72 changes: 66 additions & 6 deletions app/src/main/java/app/gamenative/utils/SteamUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ package app.gamenative.utils
import android.annotation.SuppressLint
import android.content.Context
import android.provider.Settings
import androidx.navigation.ActivityNavigator
import app.gamenative.PrefManager
import app.gamenative.data.DepotInfo
import app.gamenative.data.LibraryItem
import app.gamenative.data.SaveFilePattern
import app.gamenative.data.SteamApp
import app.gamenative.enums.Marker
import app.gamenative.enums.PathType
import app.gamenative.enums.SpecialGameSaveMapping
import app.gamenative.service.SteamService
import app.gamenative.service.SteamService.Companion.getAppDirName
import app.gamenative.service.SteamService.Companion.getAppInfoOf
import com.winlator.container.Container
import com.winlator.container.ContainerManager
import com.winlator.core.TarCompressorUtils
import com.winlator.core.WineRegistryEditor
import com.winlator.xenvironment.ImageFs
Expand All @@ -33,7 +29,6 @@ import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
import kotlin.io.path.name
import timber.log.Timber
import okhttp3.*
Expand Down Expand Up @@ -221,6 +216,10 @@ object SteamUtils {

// Create Steam ACF manifest for real Steam compatibility
createAppManifest(context, steamAppId)

// Game-specific Handling
ensureSaveLocationsForGames(context, steamAppId)

MarkerUtils.addMarker(appDirPath, Marker.STEAM_DLL_REPLACED)
}

Expand Down Expand Up @@ -262,6 +261,9 @@ object SteamUtils {
val ticketBase64 = SteamService.instance?.getEncryptedAppTicketBase64(steamAppId)
ensureSteamSettings(context, File(container.getRootDir(), ".wine/drive_c/Program Files (x86)/Steam/steamclient.dll").toPath(), appId, ticketBase64)

// Game-specific Handling
ensureSaveLocationsForGames(context, steamAppId)

MarkerUtils.addMarker(appDirPath, Marker.STEAM_COLDCLIENT_USED)
}

Expand Down Expand Up @@ -727,6 +729,10 @@ object SteamUtils {

// Create Steam ACF manifest for real Steam compatibility
createAppManifest(context, steamAppId)

// Game-specific Handling
ensureSaveLocationsForGames(context, steamAppId)

MarkerUtils.addMarker(appDirPath, Marker.STEAM_DLL_RESTORED)
}

Expand Down Expand Up @@ -1209,5 +1215,59 @@ object SteamUtils {
fun getSteam3AccountId(): Long? {
return SteamService.userSteamId?.accountID?.toLong()
}

/**
* Ensures save locations for games that require special handling (e.g., symlinks)
* This function checks if the current game needs any save location mappings
* and applies them automatically.
*
* Supports placeholders in paths:
* - {64BitSteamID} - Replaced with the user's 64-bit Steam ID
* - {Steam3AccountID} - Replaced with the user's Steam3 account ID
*/
fun ensureSaveLocationsForGames(context: Context, steamAppId: Int) {
val mapping = SpecialGameSaveMapping.registry.find { it.appId == steamAppId } ?: return

try {
val accountId = SteamService.userSteamId?.accountID?.toLong() ?: 0L
val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString() ?: "0"
Comment on lines +1232 to +1233
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 24, 2026

Choose a reason for hiding this comment

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

P2: Missing PrefManager fallback for Steam ID retrieval. Other functions in this file (e.g., ensureSteamSettings) check PrefManager.steamUserSteamId64 and PrefManager.steamUserAccountId as fallbacks before defaulting to "0". Without this fallback, symlinks could be created with "0" in the path when the Steam ID is temporarily unavailable from SteamService.

(Based on your team's feedback about adding fallbacks when reading optional data.)

View Feedback

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

<comment>Missing `PrefManager` fallback for Steam ID retrieval. Other functions in this file (e.g., `ensureSteamSettings`) check `PrefManager.steamUserSteamId64` and `PrefManager.steamUserAccountId` as fallbacks before defaulting to "0". Without this fallback, symlinks could be created with "0" in the path when the Steam ID is temporarily unavailable from `SteamService`.

(Based on your team's feedback about adding fallbacks when reading optional data.) </comment>

<file context>
@@ -1209,5 +1221,59 @@ object SteamUtils {
+        val workaround = SaveLocationWorkaround.registry.find { it.appId == steamAppId } ?: return
+
+        try {
+            val accountId = SteamService.userSteamId?.accountID?.toLong() ?: 0L
+            val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString() ?: "0"
+            val steam3AccountId = accountId.toString()
</file context>
Suggested change
val accountId = SteamService.userSteamId?.accountID?.toLong() ?: 0L
val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString() ?: "0"
val accountId = SteamService.userSteamId?.accountID?.toLong()
?: PrefManager.steamUserAccountId.takeIf { it != 0 }?.toLong()
?: 0L
val steamId64 = SteamService.userSteamId?.convertToUInt64()?.toString()
?: PrefManager.steamUserSteamId64.takeIf { it != 0L }?.toString()
?: "0"
Fix with Cubic

val steam3AccountId = accountId.toString()

val basePath = mapping.pathType.toAbsPath(context, steamAppId, accountId)

// Substitute placeholders in paths
val sourceRelativePath = mapping.sourceRelativePath
.replace("{64BitSteamID}", steamId64)
.replace("{Steam3AccountID}", steam3AccountId)
val targetRelativePath = mapping.targetRelativePath
.replace("{64BitSteamID}", steamId64)
.replace("{Steam3AccountID}", steam3AccountId)

val sourcePath = File(basePath, sourceRelativePath)
val targetPath = File(basePath, targetRelativePath)

if (!sourcePath.exists()) {
Timber.i("[${mapping.description}] Source save folder does not exist yet: ${sourcePath.absolutePath}")
return
}

if (targetPath.exists()) {
if (Files.isSymbolicLink(targetPath.toPath())) {
Timber.i("[${mapping.description}] Symlink already exists: ${targetPath.absolutePath}")
return
} else {
Timber.w("[${mapping.description}] Target path exists but is not a symlink: ${targetPath.absolutePath}")
return
}
}

targetPath.parentFile?.mkdirs()

Files.createSymbolicLink(targetPath.toPath(), sourcePath.toPath())
Timber.i("[${mapping.description}] Created symlink: ${targetPath.absolutePath} -> ${sourcePath.absolutePath}")
} catch (e: Exception) {
Timber.e(e, "[${mapping.description}] Failed to create save location symlink")
}
}
}