Compare commits

..

41 Commits

Author SHA1 Message Date
Yaakov 54b0af377b
Merge pull request #645 from SteamRE/dependabot/github_actions/actions/checkout-5 2 weeks ago
dependabot[bot] 5164a9f963
Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2 weeks ago
dependabot[bot] 2d2113a8e9
Bump actions/setup-dotnet from 4 to 5 (#650)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 weeks ago
Pavel Djundik 5d6950afd9
Merge pull request #635 from SteamRE/dependabot/nuget/DepotDownloader/SteamKit2-3.3.0
Bump SteamKit2 from 3.2.0 to 3.3.0
3 months ago
Yaakov b042b055c4
Adapt to SteamRE/SteamKit#1544 so that we can build again 3 months ago
dependabot[bot] 02a525acdb
Bump SteamKit2 from 3.2.0 to 3.3.0
---
updated-dependencies:
- dependency-name: SteamKit2
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
3 months ago
Pavel Djundik c553ef4d60 Update steamkit, bump version 4 months ago
Pavel Djundik 9e203e0d7c Bump version to 3.3.0 5 months ago
Pavel Djundik 76038d83e8 Add a message about missing `-remember-password` 5 months ago
Pavel Djundik c21489d304 Remove asking for beta password interactively because it messes with beta branches
For example steamvr has a public beta branch, and it also includes shared redist depots which obviously have no beta branch.
5 months ago
Pavel Djundik 0d37091adf Do not attempt to get free license from anonymous account
Fixes #624
5 months ago
Pavel Djundik 0d66cf09ac Add -no-mobile 5 months ago
Pavel Djundik 8d875579c5 Print that -qr can be replaced with -username when password is remembered 5 months ago
Pavel Djundik ff9c709787 -qr should not be used with -account 5 months ago
Pavel Djundik bce88e4d32 Fix printing qr code 5 months ago
Pavel Djundik 08542bd09f Suggest logging in if no manifest code is returned 5 months ago
Pavel Djundik 665f83983b Implement private branches
Fixes #514
Fixes #620
5 months ago
Pavel Djundik 272f5b646a Ignore launch settings from git 5 months ago
Pavel Djundik f078581947 Print arguments that were not consumed
Fixes #88
5 months ago
Pavel Djundik 4896ac0788
Add token in winget action 6 months ago
Pavel Djundik 401d086191 Make stored credentials dictionaries case insensitive
Fixes #539
6 months ago
Pavel Djundik 8d8c6fc59a
Merge pull request #610 from SteamRE/609-freetodownload-depotfromapp
Fix getting manifest code for freetodownload apps that use depotfromapp
6 months ago
Nicholas Hastings 19df5910c3 Fix getting manifest code for freetodownload apps that use depotfromapp 6 months ago
Pavel Djundik 1e72a47846 Add -debug to help 6 months ago
Pavel Djundik 2c07abb015 Exactly 64 is fine 6 months ago
Pavel Djundik 2682d44684 Add password length and non-ascii character warning
Fixes #601
6 months ago
Pavel Djundik be3682cd4b Remove -max-servers from help 6 months ago
Pavel Djundik 644e3f1ebc Add faq about -max-downloads 6 months ago
Pavel Djundik 56822a831f Return server when timing out as broken connection 6 months ago
Pavel Djundik 001f5303a7 Replace InvokeAsync with Parallel.ForEachAsync 6 months ago
Pavel Djundik 0150b7eff4 Vastly simplify CDNClientPool 6 months ago
Pavel Djundik 14c6a6dafa Remove -max-servers since it is unused 6 months ago
Yaakov 0617974ac0
Merge pull request #600 from SteamRE/dependabot/nuget/Microsoft.Windows.CsWin32-0.3.183
Bump Microsoft.Windows.CsWin32 from 0.3.162 to 0.3.183
7 months ago
dependabot[bot] 3249d284ba
Bump Microsoft.Windows.CsWin32 from 0.3.162 to 0.3.183
Bumps [Microsoft.Windows.CsWin32](https://github.com/microsoft/CsWin32) from 0.3.162 to 0.3.183.
- [Release notes](https://github.com/microsoft/CsWin32/releases)
- [Commits](https://github.com/microsoft/CsWin32/compare/v0.3.162...v0.3.183)

---
updated-dependencies:
- dependency-name: Microsoft.Windows.CsWin32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
7 months ago
Pavel Djundik 7b3cf4c6c7
Merge pull request #595 from SteamRE/dependabot/nuget/multi-acf9728d79
Bump protobuf-net and SteamKit2
8 months ago
dependabot[bot] b6fc6d5c6f
Bump protobuf-net and SteamKit2
Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) and [SteamKit2](https://github.com/SteamRE/SteamKit). These dependencies needed to be updated together.

Updates `protobuf-net` from 3.2.46 to 3.2.46
- [Release notes](https://github.com/protobuf-net/protobuf-net/releases)
- [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/protobuf-net/protobuf-net/commits)

Updates `SteamKit2` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/SteamRE/SteamKit/releases)
- [Commits](https://github.com/SteamRE/SteamKit/compare/3.0.1...3.0.2)

---
updated-dependencies:
- dependency-name: protobuf-net
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: SteamKit2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Pavel Djundik 4dbe7ede14
Merge pull request #592 from SteamRE/dependabot/nuget/protobuf-net-3.2.46
Bump protobuf-net from 3.2.45 to 3.2.46
8 months ago
dependabot[bot] 24d7f0b02a
Bump protobuf-net from 3.2.45 to 3.2.46
Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) from 3.2.45 to 3.2.46.
- [Release notes](https://github.com/protobuf-net/protobuf-net/releases)
- [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/protobuf-net/protobuf-net/commits)

---
updated-dependencies:
- dependency-name: protobuf-net
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Yaakov 3bd2cf52ba
Merge pull request #584 from SteamRE/dependabot/nuget/Microsoft.Windows.CsWin32-0.3.162
Bump Microsoft.Windows.CsWin32 from 0.3.106 to 0.3.162
8 months ago
dependabot[bot] f660b95d3f
Bump Microsoft.Windows.CsWin32 from 0.3.106 to 0.3.162
Bumps [Microsoft.Windows.CsWin32](https://github.com/microsoft/CsWin32) from 0.3.106 to 0.3.162.
- [Release notes](https://github.com/microsoft/CsWin32/releases)
- [Commits](https://github.com/microsoft/CsWin32/compare/v0.3.106...v0.3.162)

---
updated-dependencies:
- dependency-name: Microsoft.Windows.CsWin32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Pavel Djundik 17d3996588 Add `-branchpassword`, change `-beta` to `-branch` in help text 9 months ago

@ -25,10 +25,10 @@ jobs:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
- name: Build
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true

@ -17,10 +17,10 @@ jobs:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
- name: Configure NuGet
run: |

@ -14,7 +14,11 @@ jobs:
run: |
$wingetPackage = "SteamRE.DepotDownloader"
$github = Invoke-RestMethod -uri "https://api.github.com/repos/SteamRE/DepotDownloader/releases"
$headers = @{
Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}"
}
$github = Invoke-RestMethod -uri "https://api.github.com/repos/SteamRE/DepotDownloader/releases" -Headers $headers
$targetRelease = $github | Where-Object -Property name -match '^DepotDownloader' | Select -First 1
$assets = $targetRelease | Select -ExpandProperty assets -First 1

5
.gitignore vendored

@ -60,7 +60,7 @@ _ReSharper*
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
@ -117,4 +117,5 @@ protobuf/
cryptopp/
# misc
Thumbs.db
Thumbs.db
launchSettings.json

@ -32,8 +32,8 @@ namespace DepotDownloader
AccountSettingsStore()
{
ContentServerPenalty = new ConcurrentDictionary<string, int>();
LoginTokens = [];
GuardData = [];
LoginTokens = new(StringComparer.OrdinalIgnoreCase);
GuardData = new(StringComparer.OrdinalIgnoreCase);
}
static bool Loaded

@ -2,10 +2,8 @@
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SteamKit2.CDN;
@ -16,139 +14,80 @@ namespace DepotDownloader
/// </summary>
class CDNClientPool
{
private const int ServerEndpointMinimumSize = 8;
private readonly Steam3Session steamSession;
private readonly uint appId;
public Client CDNClient { get; }
public Server ProxyServer { get; private set; }
private readonly ConcurrentStack<Server> activeConnectionPool = [];
private readonly BlockingCollection<Server> availableServerEndpoints = [];
private readonly AutoResetEvent populatePoolEvent = new(true);
private readonly Task monitorTask;
private readonly CancellationTokenSource shutdownToken = new();
public CancellationTokenSource ExhaustedToken { get; set; }
private readonly List<Server> servers = [];
private int nextServer;
public CDNClientPool(Steam3Session steamSession, uint appId)
{
this.steamSession = steamSession;
this.appId = appId;
CDNClient = new Client(steamSession.steamClient);
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync).Unwrap();
}
public void Shutdown()
public async Task UpdateServerList()
{
shutdownToken.Cancel();
monitorTask.Wait();
}
var servers = await this.steamSession.steamContent.GetServersForSteamPipe();
private async Task<IReadOnlyCollection<Server>> FetchBootstrapServerListAsync()
{
try
{
var cdnServers = await this.steamSession.steamContent.GetServersForSteamPipe();
if (cdnServers != null)
{
return cdnServers;
}
}
catch (Exception ex)
{
Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
}
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
return null;
}
var weightedCdnServers = servers
.Where(server =>
{
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
})
.Select(server =>
{
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
private async Task ConnectionPoolMonitorAsync()
{
var didPopulate = false;
return (server, penalty);
})
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
while (!shutdownToken.IsCancellationRequested)
foreach (var (server, weight) in weightedCdnServers)
{
populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1));
// We want the Steam session so we can take the CellID from the session and pass it through to the ContentServer Directory Service
if (availableServerEndpoints.Count < ServerEndpointMinimumSize && steamSession.steamClient.IsConnected)
{
var servers = await FetchBootstrapServerListAsync().ConfigureAwait(false);
if (servers == null || servers.Count == 0)
{
ExhaustedToken?.Cancel();
return;
}
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
var weightedCdnServers = servers
.Where(server =>
{
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
})
.Select(server =>
{
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
return (server, penalty);
})
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
foreach (var (server, weight) in weightedCdnServers)
{
for (var i = 0; i < server.NumEntries; i++)
{
availableServerEndpoints.Add(server);
}
}
didPopulate = true;
}
else if (availableServerEndpoints.Count == 0 && !steamSession.steamClient.IsConnected && didPopulate)
for (var i = 0; i < server.NumEntries; i++)
{
ExhaustedToken?.Cancel();
return;
this.servers.Add(server);
}
}
}
private Server BuildConnection(CancellationToken token)
{
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
if (this.servers.Count == 0)
{
populatePoolEvent.Set();
throw new Exception("Failed to retrieve any download servers.");
}
return availableServerEndpoints.Take(token);
}
public Server GetConnection(CancellationToken token)
public Server GetConnection()
{
if (!activeConnectionPool.TryPop(out var connection))
{
connection = BuildConnection(token);
}
return connection;
return servers[nextServer % servers.Count];
}
public void ReturnConnection(Server server)
{
if (server == null) return;
activeConnectionPool.Push(server);
// nothing to do, maybe remove from ContentServerPenalty?
}
public void ReturnBrokenConnection(Server server)
{
if (server == null) return;
// Broken connections are not returned to the pool
lock (servers)
{
if (servers[nextServer % servers.Count] == server)
{
nextServer++;
// TODO: Add server to ContentServerPenalty
}
}
}
}
}

@ -0,0 +1,76 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Threading.Tasks;
using SteamKit2.Authentication;
namespace DepotDownloader
{
// This is practically copied from https://github.com/SteamRE/SteamKit/blob/master/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs
internal class ConsoleAuthenticator : IAuthenticator
{
/// <inheritdoc />
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect)
{
if (previousCodeWasIncorrect)
{
Console.Error.WriteLine("The previous 2-factor auth code you have provided is incorrect.");
}
string code;
do
{
Console.Error.Write("STEAM GUARD! Please enter your 2-factor auth code from your authenticator app: ");
code = Console.ReadLine()?.Trim();
if (code == null)
{
break;
}
}
while (string.IsNullOrEmpty(code));
return Task.FromResult(code!);
}
/// <inheritdoc />
public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect)
{
if (previousCodeWasIncorrect)
{
Console.Error.WriteLine("The previous 2-factor auth code you have provided is incorrect.");
}
string code;
do
{
Console.Error.Write($"STEAM GUARD! Please enter the auth code sent to the email at {email}: ");
code = Console.ReadLine()?.Trim();
if (code == null)
{
break;
}
}
while (string.IsNullOrEmpty(code));
return Task.FromResult(code!);
}
/// <inheritdoc />
public Task<bool> AcceptDeviceConfirmationAsync()
{
if (ContentDownloader.Config.SkipAppConfirmation)
{
return Task.FromResult(false);
}
Console.Error.WriteLine("STEAM GUARD! Use the Steam Mobile App to confirm your sign in...");
return Task.FromResult(true);
}
}
}

@ -231,59 +231,55 @@ namespace DepotDownloader
}
var manifests = depotChild["manifests"];
var manifests_encrypted = depotChild["encryptedmanifests"];
if (manifests.Children.Count == 0 && manifests_encrypted.Children.Count == 0)
if (manifests.Children.Count == 0)
return INVALID_MANIFEST_ID;
var node = manifests[branch]["gid"];
if (node == KeyValue.Invalid && !string.Equals(branch, DEFAULT_BRANCH, StringComparison.OrdinalIgnoreCase))
// Non passworded branch, found the manifest
if (node.Value != null)
return ulong.Parse(node.Value);
// If we requested public branch and it had no manifest, nothing to do
if (string.Equals(branch, DEFAULT_BRANCH, StringComparison.OrdinalIgnoreCase))
return INVALID_MANIFEST_ID;
// Either the branch just doesn't exist, or it has a password
if (string.IsNullOrEmpty(Config.BetaPassword))
{
var node_encrypted = manifests_encrypted[branch];
if (node_encrypted != KeyValue.Invalid)
{
var password = Config.BetaPassword;
while (string.IsNullOrEmpty(password))
{
Console.Write("Please enter the password for branch {0}: ", branch);
Config.BetaPassword = password = Console.ReadLine();
}
Console.WriteLine($"Branch {branch} for depot {depotId} was not found, either it does not exist or it has a password.");
return INVALID_MANIFEST_ID;
}
var encrypted_gid = node_encrypted["gid"];
if (!steam3.AppBetaPasswords.ContainsKey(branch))
{
// Submit the password to Steam now to get encryption keys
await steam3.CheckAppBetaPassword(appId, Config.BetaPassword);
if (encrypted_gid != KeyValue.Invalid)
{
// Submit the password to Steam now to get encryption keys
await steam3.CheckAppBetaPassword(appId, Config.BetaPassword);
if (!steam3.AppBetaPasswords.ContainsKey(branch))
{
Console.WriteLine($"Error: Password was invalid for branch {branch} (or the branch does not exist)");
return INVALID_MANIFEST_ID;
}
}
if (!steam3.AppBetaPasswords.TryGetValue(branch, out var appBetaPassword))
{
Console.WriteLine("Password was invalid for branch {0}", branch);
return INVALID_MANIFEST_ID;
}
// Got the password, request private depot section
// TODO: We're probably repeating this request for every depot?
var privateDepotSection = await steam3.GetPrivateBetaDepotSection(appId, branch);
var input = Util.DecodeHexString(encrypted_gid.Value);
byte[] manifest_bytes;
try
{
manifest_bytes = Util.SymmetricDecryptECB(input, appBetaPassword);
}
catch (Exception e)
{
Console.WriteLine("Failed to decrypt branch {0}: {1}", branch, e.Message);
return INVALID_MANIFEST_ID;
}
// Now repeat the same code to get the manifest gid from depot section
depotChild = privateDepotSection[depotId.ToString()];
return BitConverter.ToUInt64(manifest_bytes, 0);
}
if (depotChild == KeyValue.Invalid)
return INVALID_MANIFEST_ID;
Console.WriteLine("Unhandled depot encryption for depotId {0}", depotId);
return INVALID_MANIFEST_ID;
}
manifests = depotChild["manifests"];
if (manifests.Children.Count == 0)
return INVALID_MANIFEST_ID;
}
node = manifests[branch]["gid"];
if (node.Value == null)
return INVALID_MANIFEST_ID;
@ -333,12 +329,6 @@ namespace DepotDownloader
public static void ShutdownSteam3()
{
if (cdnPool != null)
{
cdnPool.Shutdown();
cdnPool = null;
}
if (steam3 == null)
return;
@ -435,7 +425,7 @@ namespace DepotDownloader
if (!await AccountHasAccess(appId, appId))
{
if (await steam3.RequestFreeAppLicense(appId))
if (steam3.steamUser.SteamID.AccountType != EAccountType.AnonUser && await steam3.RequestFreeAppLicense(appId))
{
Console.WriteLine("Obtained FreeOnDemand license for app {0}", appId);
@ -552,6 +542,8 @@ namespace DepotDownloader
}
}
Console.WriteLine();
try
{
await DownloadSteam3Async(infos).ConfigureAwait(false);
@ -609,10 +601,17 @@ namespace DepotDownloader
return null;
}
// For depots that are proxied through depotfromapp, we still need to resolve the proxy app id
// For depots that are proxied through depotfromapp, we still need to resolve the proxy app id, unless the app is freetodownload
var containingAppId = appId;
var proxyAppId = GetSteam3DepotProxyAppId(depotId, appId);
if (proxyAppId != INVALID_APP_ID) containingAppId = proxyAppId;
if (proxyAppId != INVALID_APP_ID)
{
var common = GetSteam3AppSection(appId, EAppInfoSection.Common);
if (common == null || !common["FreeToDownload"].AsBoolean())
{
containingAppId = proxyAppId;
}
}
return new DepotDownloadInfo(depotId, containingAppId, manifestId, branch, installDir, depotKey);
}
@ -660,9 +659,9 @@ namespace DepotDownloader
{
Ansi.Progress(Ansi.ProgressState.Indeterminate);
var cts = new CancellationTokenSource();
cdnPool.ExhaustedToken = cts;
await cdnPool.UpdateServerList();
var cts = new CancellationTokenSource();
var downloadCounter = new GlobalDownloadCounter();
var depotsToDownload = new List<DepotFilesData>(depots.Count);
var allFileNamesAllDepots = new HashSet<string>();
@ -759,7 +758,7 @@ namespace DepotDownloader
try
{
connection = cdnPool.GetConnection(cts.Token);
connection = cdnPool.GetConnection();
string cdnToken = null;
if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise))
@ -921,18 +920,25 @@ namespace DepotDownloader
var files = depotFilesData.filteredFiles.Where(f => !f.Flags.HasFlag(EDepotFileFlag.Directory)).ToArray();
var networkChunkQueue = new ConcurrentQueue<(FileStreamData fileStreamData, DepotManifest.FileData fileData, DepotManifest.ChunkData chunk)>();
await Util.InvokeAsync(
files.Select(file => new Func<Task>(async () =>
await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue)))),
maxDegreeOfParallelism: Config.MaxDownloads
);
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Config.MaxDownloads,
CancellationToken = cts.Token
};
await Util.InvokeAsync(
networkChunkQueue.Select(q => new Func<Task>(async () =>
await Task.Run(() => DownloadSteam3AsyncDepotFileChunk(cts, downloadCounter, depotFilesData,
q.fileData, q.fileStreamData, q.chunk)))),
maxDegreeOfParallelism: Config.MaxDownloads
);
await Parallel.ForEachAsync(files, parallelOptions, async (file, cancellationToken) =>
{
await Task.Yield();
DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue);
});
await Parallel.ForEachAsync(networkChunkQueue, parallelOptions, async (q, cancellationToken) =>
{
await DownloadSteam3AsyncDepotFileChunk(
cts, downloadCounter, depotFilesData,
q.fileData, q.fileStreamData, q.chunk
);
});
// Check for deleted files if updating the depot.
if (depotFilesData.previousManifest != null)
@ -1202,7 +1208,7 @@ namespace DepotDownloader
try
{
connection = cdnPool.GetConnection(cts.Token);
connection = cdnPool.GetConnection();
string cdnToken = null;
if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise))
@ -1228,6 +1234,7 @@ namespace DepotDownloader
catch (TaskCanceledException)
{
Console.WriteLine("Connection timeout downloading chunk {0}", chunkID);
cdnPool.ReturnBrokenConnection(connection);
}
catch (SteamKitWebRequestException e)
{

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RollForward>LatestMajor</RollForward>
<Version>3.0.0</Version>
<Version>3.4.0</Version>
<Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2025</Copyright>
@ -21,12 +21,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="protobuf-net" Version="3.2.45" />
<PackageReference Include="protobuf-net" Version="3.2.52" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="SteamKit2" Version="3.0.1" />
<PackageReference Include="SteamKit2" Version="3.3.0" />
</ItemGroup>
</Project>

@ -23,7 +23,6 @@ namespace DepotDownloader
public bool VerifyAll { get; set; }
public int MaxServers { get; set; }
public int MaxDownloads { get; set; }
public bool RememberPassword { get; set; }
@ -32,5 +31,6 @@ namespace DepotDownloader
public uint? LoginID { get; set; }
public bool UseQrCode { get; set; }
public bool SkipAppConfirmation { get; set; }
}
}

@ -17,6 +17,8 @@ namespace DepotDownloader
{
class Program
{
private static bool[] consumedArgs;
static async Task<int> Main(string[] args)
{
if (args.Length == 0)
@ -47,6 +49,8 @@ namespace DepotDownloader
return 0;
}
consumedArgs = new bool[args.Length];
if (HasParameter(args, "-debug"))
{
PrintVersion(true);
@ -64,21 +68,21 @@ namespace DepotDownloader
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass");
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password");
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr");
ContentDownloader.Config.SkipAppConfirmation = HasParameter(args, "-no-mobile");
if (username == null)
{
if (ContentDownloader.Config.RememberPassword)
{
Console.WriteLine("Error: -remember-password can not be used without -username.");
return 1;
}
if (ContentDownloader.Config.UseQrCode)
if (ContentDownloader.Config.RememberPassword && !ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -qr can not be used without -username.");
Console.WriteLine("Error: -remember-password can not be used without -username or -qr.");
return 1;
}
}
else if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -qr can not be used with -username.");
return 1;
}
ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only");
@ -133,7 +137,6 @@ namespace DepotDownloader
ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir");
ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate");
ContentDownloader.Config.MaxServers = GetParameter(args, "-max-servers", 20);
if (HasParameter(args, "-use-lancache"))
{
@ -152,7 +155,6 @@ namespace DepotDownloader
}
ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8);
ContentDownloader.Config.MaxServers = Math.Max(ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads);
ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null;
#endregion
@ -170,6 +172,8 @@ namespace DepotDownloader
{
#region Pubfile Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -205,6 +209,8 @@ namespace DepotDownloader
{
#region UGC Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -241,7 +247,13 @@ namespace DepotDownloader
#region App downloading
var branch = GetParameter<string>(args, "-branch") ?? GetParameter<string>(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH;
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-betapassword");
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-branchpassword") ?? GetParameter<string>(args, "-betapassword");
if (!string.IsNullOrEmpty(ContentDownloader.Config.BetaPassword) && string.IsNullOrEmpty(branch))
{
Console.WriteLine("Error: Cannot specify -branchpassword when -branch is not specified.");
return 1;
}
ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms");
@ -295,6 +307,8 @@ namespace DepotDownloader
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID)));
}
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -336,6 +350,11 @@ namespace DepotDownloader
{
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username)))
{
if (AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))
{
Console.WriteLine($"Account \"{username}\" has stored credentials. Did you forget to specify -remember-password?");
}
do
{
Console.Write("Enter account password for \"{0}\": ", username);
@ -358,6 +377,21 @@ namespace DepotDownloader
}
}
if (!string.IsNullOrEmpty(password))
{
const int MAX_PASSWORD_SIZE = 64;
if (password.Length > MAX_PASSWORD_SIZE)
{
Console.Error.WriteLine($"Warning: Password is longer than {MAX_PASSWORD_SIZE} characters, which is not supported by Steam.");
}
if (!password.All(char.IsAscii))
{
Console.Error.WriteLine("Warning: Password contains non-ASCII characters, which is not supported by Steam.");
}
}
return ContentDownloader.InitializeSteam3(username, password);
}
@ -366,7 +400,10 @@ namespace DepotDownloader
for (var x = 0; x < args.Length; ++x)
{
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
{
consumedArgs[x] = true;
return x;
}
}
return -1;
@ -389,6 +426,7 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index + 1] = true;
return (T)converter.ConvertFromString(strParam);
}
@ -414,6 +452,7 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index] = true;
list.Add((T)converter.ConvertFromString(strParam));
}
@ -423,6 +462,26 @@ namespace DepotDownloader
return list;
}
static void PrintUnconsumedArgs(string[] args)
{
var printError = false;
for (var index = 0; index < consumedArgs.Length; index++)
{
if (!consumedArgs[index])
{
printError = true;
Console.Error.WriteLine($"Argument #{index + 1} {args[index]} was not used.");
}
}
if (printError)
{
Console.Error.WriteLine("Make sure you specified the arguments correctly. Check --help for correct arguments.");
Console.Error.WriteLine();
}
}
static void PrintUsage()
{
// Do not use tabs to align parameters here because tab size may differ
@ -440,8 +499,8 @@ namespace DepotDownloader
Console.WriteLine(" -app <#> - the AppID to download.");
Console.WriteLine(" -depot <#> - the DepotID to download.");
Console.WriteLine(" -manifest <id> - manifest id of content to download (requires -depot, default: current for branch).");
Console.WriteLine($" -beta <branchname> - download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH}).");
Console.WriteLine(" -betapassword <pass> - branch password if applicable.");
Console.WriteLine($" -branch <branchname> - download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH}).");
Console.WriteLine(" -branchpassword <pass> - branch password if applicable.");
Console.WriteLine(" -all-platforms - downloads all platform-specific depots when -app is used.");
Console.WriteLine(" -all-archs - download all architecture-specific depots when -app is used.");
Console.WriteLine(" -os <os> - the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)");
@ -457,6 +516,8 @@ namespace DepotDownloader
Console.WriteLine(" -password <pass> - the password of the account to login to for restricted content.");
Console.WriteLine(" -remember-password - if set, remember the password for subsequent logins of this user.");
Console.WriteLine(" use -username <username> -remember-password as login credentials.");
Console.WriteLine(" -qr - display a login QR code to be scanned with the Steam mobile app");
Console.WriteLine(" -no-mobile - prefer entering a 2FA code instead of prompting to accept in the Steam mobile app");
Console.WriteLine();
Console.WriteLine(" -dir <installdir> - the directory in which to place downloaded files.");
Console.WriteLine(" -filelist <file.txt> - the name of a local file that contains a list of files to download (from the manifest).");
@ -465,10 +526,12 @@ namespace DepotDownloader
Console.WriteLine(" -validate - include checksum verification of files already downloaded");
Console.WriteLine(" -manifest-only - downloads a human readable manifest for any depots that would be downloaded.");
Console.WriteLine(" -cellid <#> - the overridden CellID of the content server to download from.");
Console.WriteLine(" -max-servers <#> - maximum number of content servers to use. (default: 20).");
Console.WriteLine(" -max-downloads <#> - maximum number of chunks to download concurrently. (default: 8).");
Console.WriteLine(" -loginid <#> - a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.");
Console.WriteLine(" -use-lancache - forces downloads over the local network via a Lancache instance.");
Console.WriteLine();
Console.WriteLine(" -debug - enable verbose debug logging.");
Console.WriteLine(" -V or --version - print version and runtime.");
}
static void PrintVersion(bool printExtra = false)

@ -64,7 +64,7 @@ namespace DepotDownloader
var clientConfiguration = SteamConfiguration.Create(config =>
config
.WithHttpClientFactory(HttpClientFactory.CreateHttpClient)
.WithHttpClientFactory(static purpose => HttpClientFactory.CreateHttpClient())
);
this.steamClient = new SteamClient(clientConfiguration);
@ -265,6 +265,11 @@ namespace DepotDownloader
if (requestCode == 0)
{
Console.WriteLine($"No manifest request code was returned for depot {depotId} from app {appId}, manifest {manifestId}");
if (!authenticatedUser)
{
Console.WriteLine("Suggestion: Try logging in with -username as old manifests may not be available for anonymous accounts.");
}
}
else
{
@ -310,6 +315,22 @@ namespace DepotDownloader
}
}
public async Task<KeyValue> GetPrivateBetaDepotSection(uint appid, string branch)
{
if (!AppBetaPasswords.TryGetValue(branch, out var branchPassword)) // Should be filled by CheckAppBetaPassword
{
return new KeyValue();
}
AppTokens.TryGetValue(appid, out var accessToken); // Should be filled by RequestAppInfo
var privateBeta = await steamApps.PICSGetPrivateBeta(appid, accessToken, branch, branchPassword);
Console.WriteLine($"Retrieved private beta depot section for {appid} with result: {privateBeta.Result}");
return privateBeta.DepotSection;
}
public async Task<PublishedFileDetails> GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
{
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
@ -421,13 +442,14 @@ namespace DepotDownloader
try
{
_ = AccountSettingsStore.Instance.GuardData.TryGetValue(logonDetails.Username, out var guarddata);
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new SteamKit2.Authentication.AuthSessionDetails
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
Username = logonDetails.Username,
Password = logonDetails.Password,
IsPersistentSession = ContentDownloader.Config.RememberPassword,
GuardData = guarddata,
Authenticator = new UserConsoleAuthenticator(),
Authenticator = new ConsoleAuthenticator(),
});
}
catch (TaskCanceledException)
@ -449,8 +471,8 @@ namespace DepotDownloader
{
var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
IsPersistentSession = ContentDownloader.Config.RememberPassword,
Authenticator = new UserConsoleAuthenticator(),
});
authSession = session;
@ -493,11 +515,17 @@ namespace DepotDownloader
if (result.NewGuardData != null)
{
AccountSettingsStore.Instance.GuardData[result.AccountName] = result.NewGuardData;
if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine($"Success! Next time you can login with -username {result.AccountName} -remember-password instead of -qr.");
}
}
else
{
AccountSettingsStore.Instance.GuardData.Remove(result.AccountName);
}
AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken;
AccountSettingsStore.Save();
}
@ -678,10 +706,14 @@ namespace DepotDownloader
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L);
using var qrCode = new AsciiQRCode(qrCodeData);
var qrCodeAsAsciiArt = qrCode.GetGraphic(1, drawQuietZones: false);
var qrCodeAsAsciiArt = qrCode.GetLineByLineGraphic(1, drawQuietZones: true);
Console.WriteLine("Use the Steam Mobile App to sign in with this QR code:");
Console.WriteLine(qrCodeAsAsciiArt);
foreach (var line in qrCodeAsAsciiArt)
{
Console.WriteLine(line);
}
}
}
}

@ -235,37 +235,5 @@ namespace DepotDownloader
return output;
}
public static async Task InvokeAsync(IEnumerable<Func<Task>> taskFactories, int maxDegreeOfParallelism)
{
ArgumentNullException.ThrowIfNull(taskFactories);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(maxDegreeOfParallelism, 0);
var queue = taskFactories.ToArray();
if (queue.Length == 0)
{
return;
}
var tasksInFlight = new List<Task>(maxDegreeOfParallelism);
var index = 0;
do
{
while (tasksInFlight.Count < maxDegreeOfParallelism && index < queue.Length)
{
var taskFactory = queue[index++];
tasksInFlight.Add(taskFactory());
}
var completedTask = await Task.WhenAny(tasksInFlight).ConfigureAwait(false);
await completedTask.ConfigureAwait(false);
tasksInFlight.Remove(completedTask);
} while (index < queue.Length || tasksInFlight.Count != 0);
}
}
}

@ -61,35 +61,54 @@ For example: `./DepotDownloader -app 730 -ugc 770604181014286929`
## Parameters
#### Authentication
Parameter | Description
----------------------- | -----------
`-app <#>` | the AppID to download.
`-depot <#>` | the DepotID to download.
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch).
`-ugc <#>` | the UGC ID to download.
`-beta <branchname>` | download from specified branch if available (default: Public).
`-betapassword <pass>` | branch password if applicable.
`-all-platforms` | downloads all platform-specific depots when `-app` is used.
`-os <os>` | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
`-osarch <arch>` | the architecture for which to download the game (32 or 64, default: the host's architecture)
`-all-archs` | download all architecture-specific depots when `-app` is used.
`-all-languages` | download all language-specific depots when `-app` is used.
`-language <lang>` | the language for which to download the game (default: english)
`-lowviolence` | download low violence depots when `-app` is used.
`-pubfile <#>` | the PublishedFileId to download. (Will automatically resolve to UGC id)
`-username <user>` | the username of the account to login to for restricted content.
`-password <pass>` | the password of the account to login to for restricted content.
`-remember-password` | if set, remember the password for subsequent logins of this user. (Use `-username <username> -remember-password` as login credentials)
`-username <user>` | the username of the account to login to for restricted content.
`-password <pass>` | the password of the account to login to for restricted content.
`-remember-password` | if set, remember the password for subsequent logins of this user. (Use `-username <username> -remember-password` as login credentials)
`-qr` | display a login QR code to be scanned with the Steam mobile app
`-no-mobile` | prefer entering a 2FA code instead of prompting to accept in the Steam mobile app.
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
#### Downloading
Parameter | Description
------------------------ | -----------
`-app <#>` | the AppID to download.
`-depot <#>` | the DepotID to download.
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch).
`-ugc <#>` | the UGC ID to download.
`-pubfile <#>` | the PublishedFileId to download. (Will automatically resolve to UGC id)
`-branch <branchname>` | download from specified branch if available (default: Public).
`-branchpassword <pass>` | branch password if applicable.
#### Download configuration
Parameter | Description
----------------------- | -----------
`-all-platforms` | downloads all platform-specific depots when `-app` is used.
`-os <os>` | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
`-osarch <arch>` | the architecture for which to download the game (32 or 64, default: the host's architecture)
`-all-archs` | download all architecture-specific depots when `-app` is used.
`-all-languages` | download all language-specific depots when `-app` is used.
`-language <lang>` | the language for which to download the game (default: english)
`-lowviolence` | download low violence depots when `-app` is used.
`-dir <installdir>` | the directory in which to place downloaded files.
`-filelist <file.txt>` | the name of a local file that contains a list of files to download (from the manifest). prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.
`-validate` | Include checksum verification of files already downloaded
`-manifest-only` | downloads a human readable manifest for any depots that would be downloaded.
`-cellid <#>` | the overridden CellID of the content server to download from.
`-max-servers <#>` | maximum number of content servers to use. (default: 20).
`-max-downloads <#>` | maximum number of chunks to download concurrently. (default: 8).
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
`-use-lancache` | forces downloads over the local network via a Lancache instance
`-V` or `--version` | print version and runtime
`-filelist <file.txt>` | the name of a local file that contains a list of files to download (from the manifest). prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.
`-validate` | include checksum verification of files already downloaded.
`-manifest-only` | downloads a human readable manifest for any depots that would be downloaded.
`-cellid <#>` | the overridden CellID of the content server to download from.
`-max-downloads <#>` | maximum number of chunks to download concurrently. (default: 8).
`-use-lancache` | forces downloads over the local network via a Lancache instance.
#### Other
Parameter | Description
----------------------- | -----------
`-debug` | enable verbose debug logging.
`-V` or `--version` | print version and runtime.
## Frequently Asked Questions
@ -106,3 +125,7 @@ If you pass the `-password` parameter with a password that contains special char
Try logging in with a Steam account, this may happen when using anonymous account.
Steam allows developers to block downloading old manifests, in which case no manifest code is returned even when parameters appear correct.
### Why am I getting slow download speeds and frequent connection timeouts?
When downloading old builds, cache server may not have the chunks readily available which makes downloading slower.
Try increasing `-max-downloads` to saturate the network more.

Loading…
Cancel
Save