Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object GameFixesRegistry {
STEAM_Fix_22300,
STEAM_Fix_22380,
STEAM_Fix_22330,
STEAM_Fix_752580,
STEAM_Fix_400,
STEAM_Fix_413150,
STEAM_Fix_3373660,
Expand Down
66 changes: 66 additions & 0 deletions app/src/main/java/app/gamenative/gamefixes/IniFileFix.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package app.gamenative.gamefixes

import android.content.Context
import app.gamenative.data.GameSource
import com.winlator.container.Container
import java.io.File
import java.nio.charset.StandardCharsets
import timber.log.Timber

private fun updateIniValue(content: String, key: String, value: String): String {
val regex = Regex("(?im)^(${Regex.escape(key)}\\s*=\\s*).*$")
return if (regex.containsMatchIn(content)) {
content.replace(regex, "$1$value")
} else {
val suffix = if (content.endsWith("\n") || content.isEmpty()) "" else System.lineSeparator()
content + suffix + "$key=$value" + System.lineSeparator()
}
}

class IniFileFix(
private val relativePath: String,
private val defaultValues: Map<String, String>,
) : GameFix {
override fun apply(
context: Context,
gameId: String,
installPath: String,
installPathWindows: String,
container: Container,
): Boolean {
val iniFile = File(installPath, relativePath)
if (!iniFile.isFile) {
return false
}

return runCatching {
val original = iniFile.readText(StandardCharsets.UTF_8)
var updated = original
for ((key, value) in defaultValues) {
updated = updateIniValue(updated, key, value)
}

val fileChanged = updated != original

if (fileChanged) {
iniFile.writeText(updated, StandardCharsets.UTF_8)
}

if (fileChanged) {
Timber.tag("GameFixes").i("Updated $relativePath for game $gameId")
}

fileChanged
}.getOrElse { error ->
Timber.tag("GameFixes").w(error, "Failed to update $relativePath for game $gameId")
false
}
}
}

class KeyedIniFileFix(
override val gameSource: GameSource,
override val gameId: String,
relativePath: String,
defaultValues: Map<String, String>,
) : KeyedGameFix, GameFix by IniFileFix(relativePath, defaultValues)
14 changes: 14 additions & 0 deletions app/src/main/java/app/gamenative/gamefixes/STEAM_752580.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.gamenative.gamefixes

import app.gamenative.data.GameSource

val STEAM_Fix_752580: KeyedGameFix = KeyedIniFileFix(
gameSource = GameSource.STEAM,
gameId = "752580",
relativePath = "Settings.ini",
defaultValues = linkedMapOf(
"Music" to "0",
"SoundFX" to "1",
"Speech" to "1",
),
)
10 changes: 8 additions & 2 deletions app/src/main/java/app/gamenative/service/DownloadService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import timber.log.Timber
import java.io.File

object DownloadService {
private var lastUpdateTime: Long = 0
private var downloadDirectoryApps: MutableList<String>? = null
@Volatile private var lastUpdateTime: Long = 0
@Volatile private var downloadDirectoryApps: MutableList<String>? = null
var baseDataDirPath: String = ""
private set(value) {
field = value
Expand Down Expand Up @@ -44,6 +44,12 @@ object DownloadService {
.map { it.absolutePath }
}

@Synchronized
fun invalidateCache() {
lastUpdateTime = 0
}

@Synchronized
fun getDownloadDirectoryApps (): MutableList<String> {
// What apps have folders in the download area?
// Isn't checking for "complete" marker - incomplete is accepted
Expand Down
98 changes: 72 additions & 26 deletions app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,15 @@ object SteamAutoCloud {
val getLocalUserFilesAsPrefixMap: () -> Map<String, List<UserFileInfo>> = {
val savePatterns = appInfo.ufs.saveFilePatterns.filter { userFile -> userFile.root.isWindows }

if (savePatterns.isNotEmpty()) {
val result = mutableMapOf<String, MutableList<UserFileInfo>>()
val result = mutableMapOf<String, MutableList<UserFileInfo>>()

if (savePatterns.isNotEmpty()) {
savePatterns.forEach { userFile ->
if (userFile.root == PathType.SteamUserData) {
// skip handling, use the logic below to scan SteamUserData
return@forEach
}

val basePath = Paths.get(prefixToPath(userFile.root.toString()), userFile.substitutedPath)

Timber.i("Looking for saves in $basePath with pattern ${userFile.pattern} (prefix ${userFile.prefix})")
Expand All @@ -278,42 +283,63 @@ object SteamAutoCloud {

val relativePath = basePath.relativize(it).pathString

UserFileInfo(userFile.root, userFile.substitutedPath, relativePath, Files.getLastModifiedTime(it).toMillis(), sha, cloudRoot = userFile.uploadRoot, cloudPath = userFile.uploadPath)
UserFileInfo(
root = userFile.root,
path = userFile.substitutedPath,
filename = relativePath,
timestamp = Files.getLastModifiedTime(it).toMillis(),
sha = sha,
cloudRoot = userFile.uploadRoot,
cloudPath = userFile.uploadPath
)
}.collect(Collectors.toList())

Timber.i("Found ${files.size} file(s) in $basePath for pattern ${userFile.pattern}")

val prefixKey = Paths.get(userFile.prefix).pathString
result.getOrPut(prefixKey) { mutableListOf() }.addAll(files)
}
}

result
} else {
// Fallback: no UFS patterns; scan SteamUserData root recursively (depth 5)
val rootType = PathType.SteamUserData
val basePath = Paths.get(prefixToPath(rootType.toString()))
// Scan SteamUserData root recursively (depth 5)
val rootType = PathType.SteamUserData
val basePath = Paths.get(prefixToPath(rootType.toString()))

Timber.i("Scanning $basePath recursively (depth 5) under ${rootType.name}")

Timber.i("No UFS patterns; scanning $basePath recursively (depth 5) under ${rootType.name}")
val files = FileUtils.findFilesRecursive(
rootPath = basePath,
pattern = "*",
maxDepth = 5,
).map {
val sha = streamingShaHash(it)

val files = FileUtils.findFilesRecursive(
rootPath = basePath,
pattern = "*",
maxDepth = 5,
).map {
val sha = streamingShaHash(it)
val relativePath = basePath.relativize(it).pathString

val relativePath = basePath.relativize(it).pathString
Timber.i("Found ${it.pathString}\n\tin %${rootType.name}%\n\twith sha [${sha.joinToString(", ")}]")

Timber.i("Found ${it.pathString}\n\tin %${rootType.name}%\n\twith sha [${sha.joinToString(", ")}]")
// Store relative path in filename; empty path component
UserFileInfo(
root = rootType,
path = "",
filename = relativePath,
timestamp = Files.getLastModifiedTime(it).toMillis(),
sha = sha,
cloudRoot = rootType,
cloudPath = ""
)
}.collect(Collectors.toList())

// Store relative path in filename; empty path component
UserFileInfo(rootType, "", relativePath, Files.getLastModifiedTime(it).toMillis(), sha)
}.collect(Collectors.toList())
Timber.i("Found ${files.size} file(s) in $basePath")

Timber.i("Found ${files.size} file(s) in $basePath for fallback recursive scan")
mapOf(Paths.get("%${rootType.name}%").pathString to files)

mapOf(Paths.get("%${rootType.name}%").pathString to files)
if (files.isNotEmpty()) {
val prefixKey = "%${rootType.name}%"
result.getOrPut(prefixKey) { mutableListOf() }.addAll(files)
}

result
}

val fileChangeListToUserFiles: (AppFileChangeList) -> List<UserFileInfo> = { appFileListChange ->
Expand Down Expand Up @@ -514,9 +540,19 @@ object SteamAutoCloud {
val uploadInfo = steamCloud.beginFileUpload(
appId = appInfo.id,
filename = if (appInfo.ufs.saveFilePatterns.isEmpty()) {
file.path + file.filename
// For SteamUserData files, use just the filename without folder prefix
if (file.root == PathType.SteamUserData) {
file.filename
} else {
file.path + file.filename
}
} else {
file.prefixPath
// For SteamUserData files, use just the filename to avoid folder prefix
if (file.root == PathType.SteamUserData) {
file.filename
} else {
file.prefixPath
}
},
fileSize = fileSize,
rawFileSize = fileSize,
Expand Down Expand Up @@ -633,9 +669,19 @@ object SteamAutoCloud {
appId = appInfo.id,
fileSha = file.sha,
filename = if (appInfo.ufs.saveFilePatterns.isEmpty()) {
file.path + file.filename
// For SteamUserData files, use just the filename without folder prefix
if (file.root == PathType.SteamUserData) {
file.filename
} else {
file.path + file.filename
}
} else {
file.prefixPath
// For SteamUserData files, use just the filename to avoid folder prefix
if (file.root == PathType.SteamUserData) {
file.filename
} else {
file.prefixPath
}
},
).await()

Expand Down
43 changes: 37 additions & 6 deletions app/src/main/java/app/gamenative/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -816,8 +816,33 @@ class SteamService : Service(), IChallengeUrlChanged {
val appInfo = getAppInfoOf(appId) ?: return emptyMap()
val ownedDlc = runBlocking { getOwnedAppDlc(appId) }
val hasSteamUnlockedBranch = runBlocking { getSteamUnlockedBranches(appId).isNotEmpty() }
val licensedDepots = getLicensedDepotIds(appId)
return resolveDownloadableDepots(appInfo.depots, containerLanguage, ownedDlc, licensedDepots, hasSteamUnlockedBranch)
val licensedDepots = getLicensedDepotIds(appId).orEmpty().toMutableSet()

// Use the dlcAppID of the ownedDlc, to find the licensed depotIds from steam_license
val mapDlcDepotIds = mutableMapOf<Int, List<Int>>()
ownedDlc.forEach { (dlcAppId, info) ->
val dlcDepotIds = getPkgInfoOf(dlcAppId)?.depotIds.orEmpty()
mapDlcDepotIds[dlcAppId] = dlcDepotIds

// Make sure licensedDepots contains the dlc depots
licensedDepots.addAll(dlcDepotIds)
}

val baseDepots = resolveDownloadableDepots(appInfo.depots, containerLanguage, ownedDlc, licensedDepots, hasSteamUnlockedBranch)

// Find in the depots of mainApp, that if any of the depotID is actually belongs to another steam_app entry
// override the dlcAppId to the corresponding app id
// It should fix Don't Starve DLC list, and keeping existing DLC logic correct
// For existing DLC logic, two games checked Halo MCC, Cyberpunk 2077 to have correct data
val map = mutableMapOf<Int, DepotInfo>()
baseDepots.forEach { (depotId, info) ->
val foundDlcAppId = mapDlcDepotIds
.filter { it.value.contains(info.depotId) }
.keys.firstOrNull()
map[depotId] = info.copy(dlcAppId = foundDlcAppId ?: info.dlcAppId)
}

return map
}

/**
Expand All @@ -837,13 +862,13 @@ class SteamService : Service(), IChallengeUrlChanged {
val appInfo = getAppInfoOf(appId) ?: return emptyMap()
val ownedDlc = runBlocking { getOwnedAppDlc(appId) }
val hasSteamUnlockedBranch = runBlocking { getSteamUnlockedBranches(appId).isNotEmpty() }
val licensedDepots = getLicensedDepotIds(appId)
val licensedDepots = getLicensedDepotIds(appId).orEmpty().toMutableSet()

val map = getMainAppDepots(appId, preferredLanguage).toMutableMap()

val baseDepots = resolveDownloadableDepots(appInfo.depots, preferredLanguage, ownedDlc, licensedDepots, hasSteamUnlockedBranch)
// parent app's arch applies to DLC arch selection
val has64Bit = eligibleDepots(appInfo.depots, preferredLanguage, ownedDlc, licensedDepots)
.any { it.osArch == OSArch.Arch64 }
val map = baseDepots.toMutableMap()

val indirectDlcApps = getDownloadableDlcAppsOf(appId).orEmpty()
indirectDlcApps.forEach { dlcApp ->
Expand Down Expand Up @@ -2062,7 +2087,7 @@ class SteamService : Service(), IChallengeUrlChanged {
return getAppInfoOf(appId)?.let { appInfo ->
appInfo.config.launch.filter { launchInfo ->
// since configOS was unreliable and configArch was even more unreliable
launchInfo.executable.endsWith(".exe")
launchInfo.executable.endsWith(".exe", ignoreCase = true)
}
}.orEmpty()
}
Expand Down Expand Up @@ -2143,6 +2168,9 @@ class SteamService : Service(), IChallengeUrlChanged {
return@async PostSyncInfo(SyncResult.InProgress)
}

// Migrate GSE Saves to Steam userdata
SteamUtils.migrateGSESavesToSteamUserdata(instance?.applicationContext!!, appId)

try {
var syncResult = PostSyncInfo(SyncResult.UnknownFail)

Expand Down Expand Up @@ -2231,6 +2259,9 @@ class SteamService : Service(), IChallengeUrlChanged {
return@async PostSyncInfo(SyncResult.InProgress)
}

// Migrate GSE Saves to Steam userdata
SteamUtils.migrateGSESavesToSteamUserdata(instance?.applicationContext!!, appId)

try {
var syncResult = PostSyncInfo(SyncResult.UnknownFail)

Expand Down
Loading
Loading