Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class CDNClientPool(
private val steamSession: Steam3Session,
private val appId: Int,
private val scope: CoroutineScope,
debug: Boolean = false,
private val debug: Boolean = false,
) : AutoCloseable {

private var logger: Logger? = null
Expand All @@ -49,13 +49,11 @@ class CDNClientPool(
init {
cdnClient = Client(steamSession.steamClient)

if (debug) {
logger = LogManager.getLogger(CDNClientPool::class.java)
}
logger = LogManager.getLogger(CDNClientPool::class.java)
}

override fun close() {
logger?.debug("Closing...")
if (debug) logger?.debug("Closing...")

servers.set(emptyList())

Expand Down Expand Up @@ -89,7 +87,7 @@ class CDNClientPool(
nextServer.set(0)

// servers.joinToString(separator = "\n", prefix = "Servers:\n") { "- $it" }
logger?.debug("Found ${weightedCdnServers.size} Servers")
if (debug) logger?.debug("Found ${weightedCdnServers.size} Servers")

if (weightedCdnServers.isEmpty()) {
throw Exception("Failed to retrieve any download servers.")
Expand All @@ -102,7 +100,7 @@ class CDNClientPool(
val index = nextServer.getAndIncrement()
val server = servers[index % servers.size]

logger?.debug("Getting connection $server")
if (debug) logger?.debug("Getting connection $server")

return server
}
Expand All @@ -113,7 +111,7 @@ class CDNClientPool(
return
}

logger?.debug("Returning connection: $server")
if (debug) logger?.debug("Returning connection: $server")

// (SK) nothing to do, maybe remove from ContentServerPenalty?
}
Expand All @@ -124,7 +122,7 @@ class CDNClientPool(
return
}

logger?.debug("Returning broken connection: $server")
if (debug) logger?.debug("Returning broken connection: $server")

val servers = servers.get()
val currentIndex = nextServer.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
Expand Down Expand Up @@ -105,6 +106,7 @@ import kotlin.text.toLongOrNull
* @param maxFileWrites Number of concurrent files being written. Default: 1
* @param androidEmulation Forces "Windows" as the default OS filter. Used when running Android games in PC emulators that expect Windows builds.
* @param parentJob Parent job for the downloader. If provided, the downloader will be cancelled when the parent job is cancelled.
* @param autoStartDownload Whether to start downloading automatically. If false, you must call [startDownloading] manually.
*
* @author Oxters
* @author Lossy
Expand All @@ -122,6 +124,7 @@ class DepotDownloader @JvmOverloads constructor(
private var maxFileWrites: Int = 1,
private val androidEmulation: Boolean = false,
private val parentJob: Job? = null,
private val autoStartDownload: Boolean = true,
) : Closeable {

companion object {
Expand Down Expand Up @@ -232,19 +235,24 @@ class DepotDownloader @JvmOverloads constructor(
// endregion

init {
if (debug) {
logger = LogManager.getLogger(DepotDownloader::class.java)
}
logger = LogManager.getLogger(DepotDownloader::class.java)

steam3 = Steam3Session(steamClient, debug)

logger?.debug("DepotDownloader launched with ${licenses.size} for account")
if (debug) logger?.debug("DepotDownloader launched with ${licenses.size} for account")

licenses.forEach { license ->
if (license.accessToken.toULong() > 0UL) {
steam3!!.packageTokens[license.packageID] = license.accessToken
}
}

if (autoStartDownload) {
startDownloading()
}
}

fun startDownloading() {
// Launch the processing loop
scope.launch {
processItems()
Expand Down Expand Up @@ -398,14 +406,14 @@ class DepotDownloader @JvmOverloads constructor(
filesystem.createDirectories(fileStagingPath.parent!!)

httpClient.getClient().use { client ->
logger?.debug("Starting download of $fileName...")
if (debug) logger?.debug("Starting download of $fileName...")

val response = client.get(url)
val channel = response.bodyAsChannel()

val totalBytes = response.headers[HttpHeaders.ContentLength]?.toLongOrNull()

logger?.debug("File size: ${totalBytes?.let { Util.formatBytes(it) } ?: "Unknown"}")
if (debug) logger?.debug("File size: ${totalBytes?.let { Util.formatBytes(it) } ?: "Unknown"}")

filesystem.sink(fileStagingPath).buffer().use { sink ->
val buffer = Buffer()
Expand All @@ -423,17 +431,17 @@ class DepotDownloader @JvmOverloads constructor(
}
}

logger?.debug("Download completed.")
if (debug) logger?.debug("Download completed.")
}

if (filesystem.exists(fileFinalPath)) {
logger?.debug("Deleting $fileFinalPath")
if (debug) logger?.debug("Deleting $fileFinalPath")
filesystem.delete(fileFinalPath)
}

try {
filesystem.atomicMove(fileStagingPath, fileFinalPath)
logger?.debug("File '$fileStagingPath' moved to final location: $fileFinalPath")
if (debug) logger?.debug("File '$fileStagingPath' moved to final location: $fileFinalPath")
} catch (e: IOException) {
logger?.error("Failed to move files", e)
throw e
Expand Down Expand Up @@ -470,7 +478,7 @@ class DepotDownloader @JvmOverloads constructor(

if (!accountHasAccess(appId, appId)) {
if (steamUser.steamID!!.accountType != EAccountType.AnonUser && steam3!!.requestFreeAppLicense(appId)) {
logger?.debug("Obtained FreeOnDemand license for app $appId")
if (debug) logger?.debug("Obtained FreeOnDemand license for app $appId")

// Fetch app info again in case we didn't get it fully without a license.
steam3!!.requestAppInfo(appId, true)
Expand All @@ -494,7 +502,7 @@ class DepotDownloader @JvmOverloads constructor(

depotIdsFound.addAll(depotIdsExpected)
} else {
logger?.debug("Using app branch: $branch")
if (debug) logger?.debug("Using app branch: $branch")

depots?.children?.forEach { depotSection ->
if (depotSection.children.isEmpty()) {
Expand Down Expand Up @@ -946,10 +954,14 @@ class DepotDownloader @JvmOverloads constructor(
}
}

logger?.debug(
"Total downloaded: ${downloadCounter.totalBytesCompressed} bytes " +
"(${downloadCounter.totalBytesUncompressed} bytes uncompressed) from ${depots.size} depots"
)
if (debug) {
logger?.debug(
"Total downloaded: ${downloadCounter.totalBytesCompressed} bytes " +
"(${downloadCounter.totalBytesUncompressed} bytes uncompressed) from ${depots.size} depots"
)
}

finishDepotDownload(mainAppId)
}

private suspend fun processDepotManifestAndFiles(
Expand All @@ -958,7 +970,7 @@ class DepotDownloader @JvmOverloads constructor(
): DepotFilesData? = withContext(Dispatchers.IO) {
val depotCounter = DepotDownloadCounter()

logger?.debug("Processing depot ${depot.depotId}")
if (debug) logger?.debug("Processing depot ${depot.depotId}")

var oldManifest: DepotManifest? = null

Expand All @@ -983,14 +995,14 @@ class DepotDownloader @JvmOverloads constructor(

if (lastManifestId == depot.manifestId && oldManifest != null) {
newManifest = oldManifest
logger?.debug("Already have manifest ${depot.manifestId} for depot ${depot.depotId}.")
if (debug) logger?.debug("Already have manifest ${depot.manifestId} for depot ${depot.depotId}.")
} else {
newManifest = Util.loadManifestFromFile(configDir, depot.depotId, depot.manifestId, true)

if (newManifest != null) {
logger?.debug("Already have manifest ${depot.manifestId} for depot ${depot.depotId}.")
if (debug) logger?.debug("Already have manifest ${depot.manifestId} for depot ${depot.depotId}.")
} else {
logger?.debug("Downloading depot ${depot.depotId} manifest")
if (debug) logger?.debug("Downloading depot ${depot.depotId} manifest")
notifyListeners { it.onStatusUpdate("Downloading manifest for depot ${depot.depotId}") }

var manifestRequestCode: ULong = 0U
Expand Down Expand Up @@ -1040,7 +1052,7 @@ class DepotDownloader @JvmOverloads constructor(
}
}

logger?.debug("Downloading manifest ${depot.manifestId} from $connection with ${cdnClientPool!!.proxyServer ?: "no proxy"}")
if (debug) logger?.debug("Downloading manifest ${depot.manifestId} from $connection with ${cdnClientPool!!.proxyServer ?: "no proxy"}")

newManifest = cdnClientPool!!.cdnClient!!.downloadManifest(
depotId = depot.depotId,
Expand Down Expand Up @@ -1100,7 +1112,7 @@ class DepotDownloader @JvmOverloads constructor(
}
}

logger?.debug("Manifest ${depot.manifestId} (${newManifest.creationTime})")
if (debug) logger?.debug("Manifest ${depot.manifestId} (${newManifest.creationTime})")

if (config.downloadManifestOnly) {
Util.dumpManifestToTextFile(depot, newManifest)
Expand Down Expand Up @@ -1162,7 +1174,7 @@ class DepotDownloader @JvmOverloads constructor(
val depot = depotFilesData.depotDownloadInfo
val depotCounter = depotFilesData.depotCounter

logger?.debug("Downloading depot ${depot.depotId}")
if (debug) logger?.debug("Downloading depot ${depot.depotId}")

val files = depotFilesData.filteredFiles.filter { !it.flags.contains(EDepotFileFlag.Directory) }

Expand All @@ -1185,19 +1197,19 @@ class DepotDownloader @JvmOverloads constructor(
}
} finally {
if (isLastDepot) {
logger?.debug("Waiting for ${pendingChunks.get()} pending chunks to complete for depot ${depot.depotId}")
if (debug) logger?.debug("Waiting for ${pendingChunks.get()} pending chunks to complete for depot ${depot.depotId}")

// Wait for all pending chunks to complete processing
while (pendingChunks.get() > 0) {
kotlinx.coroutines.delay(100)
delay(100)
}

logger?.debug("All chunks completed, canceling processing job for depot ${depot.depotId}")
if (debug) logger?.debug("All chunks completed, canceling processing job for depot ${depot.depotId}")

// Cancel the continuous flow job since no more chunks will be added
chunkProcessingJob?.cancel()

logger?.debug("Canceled chunk processing job for depot ${depot.depotId}")
if (debug) logger?.debug("Canceled chunk processing job for depot ${depot.depotId}")
}
}

Expand Down Expand Up @@ -1225,7 +1237,7 @@ class DepotDownloader @JvmOverloads constructor(
}

filesystem.delete(fileFinalPath)
logger?.debug("Deleted $fileFinalPath")
if (debug) logger?.debug("Deleted $fileFinalPath")
}
}

Expand All @@ -1241,11 +1253,7 @@ class DepotDownloader @JvmOverloads constructor(
)
}

logger?.debug("Depot ${depot.depotId} - Downloaded ${depotCounter.depotBytesCompressed} bytes (${depotCounter.depotBytesUncompressed} bytes uncompressed)")

if (isLastDepot) {
finishDepotDownload(mainAppId)
}
if (debug) logger?.debug("Depot ${depot.depotId} - Downloaded ${depotCounter.depotBytesCompressed} bytes (${depotCounter.depotBytesUncompressed} bytes uncompressed)")
}

private suspend fun downloadSteam3DepotFile(
Expand Down Expand Up @@ -1276,7 +1284,8 @@ class DepotDownloader @JvmOverloads constructor(
var neededChunks: MutableList<ChunkData>? = null
val fileDidExist = filesystem.exists(fileFinalPath)
if (!fileDidExist) {
logger?.debug("Pre-allocating: $fileFinalPath")
if (debug) logger?.debug("Pre-allocating: $fileFinalPath")

notifyListeners { it.onStatusUpdate("Allocating file: ${file.fileName}") }

// create new file. need all chunks
Expand All @@ -1299,7 +1308,7 @@ class DepotDownloader @JvmOverloads constructor(
if (config.verifyAll || !hashMatches) {
// we have a version of this file, but it doesn't fully match what we want
if (config.verifyAll) {
logger?.debug("Validating: $fileFinalPath")
if (debug) logger?.debug("Validating: $fileFinalPath")
}

val matchingChunks = arrayListOf<ChunkMatch>()
Expand Down Expand Up @@ -1396,7 +1405,8 @@ class DepotDownloader @JvmOverloads constructor(
}

filesystem.openReadWrite(fileFinalPath).use { handle ->
logger?.debug("Validating $fileFinalPath")
if (debug) logger?.debug("Validating $fileFinalPath")

notifyListeners { it.onStatusUpdate("Validating: ${file.fileName}") }

neededChunks = Util.validateSteam3FileChecksums(
Expand All @@ -1412,7 +1422,8 @@ class DepotDownloader @JvmOverloads constructor(

val percentage =
(depotDownloadCounter.sizeDownloaded / depotDownloadCounter.completeDownloadSize.toFloat()) * 100.0f
logger?.debug("%.2f%% %s".format(percentage, fileFinalPath))

if (debug) logger?.debug("%.2f%% %s".format(percentage, fileFinalPath))
}

synchronized(downloadCounter) {
Expand Down Expand Up @@ -1503,7 +1514,7 @@ class DepotDownloader @JvmOverloads constructor(
}
}

logger?.debug("Downloading chunk $chunkID from $connection with ${cdnClientPool!!.proxyServer ?: "no proxy"}")
if (debug) logger?.debug("Downloading chunk $chunkID from $connection with ${cdnClientPool!!.proxyServer ?: "no proxy"}")

downloaded = cdnClientPool!!.cdnClient!!.downloadDepotChunk(
depotId = depot.depotId,
Expand Down Expand Up @@ -1595,7 +1606,18 @@ class DepotDownloader @JvmOverloads constructor(
private suspend fun finishDepotDownload(mainAppId: Int) {
val appItem = processingItemsMap[mainAppId]
if (appItem != null) {
if (debug) {
logger?.debug("Notifying onDownloadCompleted")
}
notifyListeners { it.onDownloadCompleted(appItem) }
} else {
if (debug) {
logger?.error("AppItem not found, cannot notify onDownloadCompleted")
}
}

if (debug) {
logger?.debug("Complete the completionFuture.")
}

completionFuture.complete(null)
Expand Down Expand Up @@ -1700,13 +1722,13 @@ class DepotDownloader @JvmOverloads constructor(

when (item) {
is PubFileItem -> {
logger?.debug("Downloading PUB File for ${item.appId}")
if (debug) logger?.debug("Downloading PUB File for ${item.appId}")
notifyListeners { it.onDownloadStarted(item) }
downloadPubFile(item.appId, item.pubFile)
}

is UgcItem -> {
logger?.debug("Downloading UGC File for ${item.appId}")
if (debug) logger?.debug("Downloading UGC File for ${item.appId}")
notifyListeners { it.onDownloadStarted(item) }
downloadUGC(item.appId, item.ugcId)
}
Expand Down Expand Up @@ -1758,8 +1780,10 @@ class DepotDownloader @JvmOverloads constructor(
depotManifestIds.addAll(depotIdList.map { it to INVALID_MANIFEST_ID })
}

logger?.debug("Downloading App for ${item.appId}")
if (debug) logger?.debug("Downloading App for ${item.appId}")

notifyListeners { it.onDownloadStarted(item) }

downloadApp(
appId = item.appId,
depotManifestIds = depotManifestIds,
Expand Down Expand Up @@ -1863,7 +1887,7 @@ class DepotDownloader @JvmOverloads constructor(
)
}

logger?.debug("%.2f%% %s".format(depotPercentage, fileFinalPath))
if (debug) logger?.debug("%.2f%% %s".format(depotPercentage, fileFinalPath))
} else {
// Update counters and notify on chunk completion
val sizeDownloaded: Long
Expand Down
Loading