Compare commits

...

49 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 4 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>
4 months ago
Pavel Djundik c553ef4d60 Update steamkit, bump version 5 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 7 months ago
Pavel Djundik 644e3f1ebc Add faq about -max-downloads 7 months ago
Pavel Djundik 56822a831f Return server when timing out as broken connection 7 months ago
Pavel Djundik 001f5303a7 Replace InvokeAsync with Parallel.ForEachAsync 7 months ago
Pavel Djundik 0150b7eff4 Vastly simplify CDNClientPool 7 months ago
Pavel Djundik 14c6a6dafa Remove -max-servers since it is unused 7 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
Pavel Djundik 5d0c4fb050 Bump version to 3.0.0 9 months ago
Pavel Djundik d087678628 Disallow using -remember-password and -qr without -username 9 months ago
Pavel Djundik deac83600c
Merge pull request #565 from tpill90/AddLancacheSupport
Adding support for Lancache
9 months ago
Tim Pilius f567a63d84 Adding log message for when the lancache has been detected. 9 months ago
Tim Pilius e9a5abecb7 Adding support for Lancache with a new optional flag. 9 months ago
Pavel Djundik 5fe8a827d8 Use depotfromapp for getting manifest codes
Partially reverts 3143362d83

Fixes #569
Fixes #570
10 months ago
Pavel Djundik 682bfbac27 Do not abort downloading if one depot fails to get depot key 10 months ago
NicknineTheEagle 1c97b42124 Use Steam manifest format 10 months ago

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

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

@ -14,7 +14,11 @@ jobs:
run: | run: |
$wingetPackage = "SteamRE.DepotDownloader" $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 $targetRelease = $github | Where-Object -Property name -match '^DepotDownloader' | Select -First 1
$assets = $targetRelease | Select -ExpandProperty assets -First 1 $assets = $targetRelease | Select -ExpandProperty assets -First 1

5
.gitignore vendored

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

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

@ -2,10 +2,8 @@
// in file 'LICENSE', which is part of this source code package. // in file 'LICENSE', which is part of this source code package.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SteamKit2.CDN; using SteamKit2.CDN;
@ -16,139 +14,80 @@ namespace DepotDownloader
/// </summary> /// </summary>
class CDNClientPool class CDNClientPool
{ {
private const int ServerEndpointMinimumSize = 8;
private readonly Steam3Session steamSession; private readonly Steam3Session steamSession;
private readonly uint appId; private readonly uint appId;
public Client CDNClient { get; } public Client CDNClient { get; }
public Server ProxyServer { get; private set; } public Server ProxyServer { get; private set; }
private readonly ConcurrentStack<Server> activeConnectionPool = []; private readonly List<Server> servers = [];
private readonly BlockingCollection<Server> availableServerEndpoints = []; private int nextServer;
private readonly AutoResetEvent populatePoolEvent = new(true);
private readonly Task monitorTask;
private readonly CancellationTokenSource shutdownToken = new();
public CancellationTokenSource ExhaustedToken { get; set; }
public CDNClientPool(Steam3Session steamSession, uint appId) public CDNClientPool(Steam3Session steamSession, uint appId)
{ {
this.steamSession = steamSession; this.steamSession = steamSession;
this.appId = appId; this.appId = appId;
CDNClient = new Client(steamSession.steamClient); CDNClient = new Client(steamSession.steamClient);
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync).Unwrap();
} }
public void Shutdown() public async Task UpdateServerList()
{ {
shutdownToken.Cancel(); var servers = await this.steamSession.steamContent.GetServersForSteamPipe();
monitorTask.Wait();
}
private async Task<IReadOnlyCollection<Server>> FetchBootstrapServerListAsync() ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
{
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);
}
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() return (server, penalty);
{ })
var didPopulate = false; .OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
while (!shutdownToken.IsCancellationRequested) foreach (var (server, weight) in weightedCdnServers)
{ {
populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1)); for (var i = 0; i < server.NumEntries; i++)
// 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)
{ {
ExhaustedToken?.Cancel(); this.servers.Add(server);
return;
} }
} }
}
private Server BuildConnection(CancellationToken token) if (this.servers.Count == 0)
{
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
{ {
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)) return servers[nextServer % servers.Count];
{
connection = BuildConnection(token);
}
return connection;
} }
public void ReturnConnection(Server server) public void ReturnConnection(Server server)
{ {
if (server == null) return; if (server == null) return;
activeConnectionPool.Push(server); // nothing to do, maybe remove from ContentServerPenalty?
} }
public void ReturnBrokenConnection(Server server) public void ReturnBrokenConnection(Server server)
{ {
if (server == null) return; 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);
}
}
}

@ -189,6 +189,20 @@ namespace DepotDownloader
return uint.Parse(buildid.Value); return uint.Parse(buildid.Value);
} }
static uint GetSteam3DepotProxyAppId(uint depotId, uint appId)
{
var depots = GetSteam3AppSection(appId, EAppInfoSection.Depots);
var depotChild = depots[depotId.ToString()];
if (depotChild == KeyValue.Invalid)
return INVALID_APP_ID;
if (depotChild["depotfromapp"] == KeyValue.Invalid)
return INVALID_APP_ID;
return depotChild["depotfromapp"].AsUnsignedInteger();
}
static async Task<ulong> GetSteam3DepotManifest(uint depotId, uint appId, string branch) static async Task<ulong> GetSteam3DepotManifest(uint depotId, uint appId, string branch)
{ {
var depots = GetSteam3AppSection(appId, EAppInfoSection.Depots); var depots = GetSteam3AppSection(appId, EAppInfoSection.Depots);
@ -217,59 +231,55 @@ namespace DepotDownloader
} }
var manifests = depotChild["manifests"]; 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; return INVALID_MANIFEST_ID;
var node = manifests[branch]["gid"]; 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]; Console.WriteLine($"Branch {branch} for depot {depotId} was not found, either it does not exist or it has a password.");
if (node_encrypted != KeyValue.Invalid) return INVALID_MANIFEST_ID;
{ }
var password = Config.BetaPassword;
while (string.IsNullOrEmpty(password))
{
Console.Write("Please enter the password for branch {0}: ", branch);
Config.BetaPassword = password = Console.ReadLine();
}
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) if (!steam3.AppBetaPasswords.ContainsKey(branch))
{ {
// Submit the password to Steam now to get encryption keys Console.WriteLine($"Error: Password was invalid for branch {branch} (or the branch does not exist)");
await steam3.CheckAppBetaPassword(appId, Config.BetaPassword); return INVALID_MANIFEST_ID;
}
}
if (!steam3.AppBetaPasswords.TryGetValue(branch, out var appBetaPassword)) // Got the password, request private depot section
{ // TODO: We're probably repeating this request for every depot?
Console.WriteLine("Password was invalid for branch {0}", branch); var privateDepotSection = await steam3.GetPrivateBetaDepotSection(appId, branch);
return INVALID_MANIFEST_ID;
}
var input = Util.DecodeHexString(encrypted_gid.Value); // Now repeat the same code to get the manifest gid from depot section
byte[] manifest_bytes; depotChild = privateDepotSection[depotId.ToString()];
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;
}
return BitConverter.ToUInt64(manifest_bytes, 0); if (depotChild == KeyValue.Invalid)
} return INVALID_MANIFEST_ID;
Console.WriteLine("Unhandled depot encryption for depotId {0}", depotId); manifests = depotChild["manifests"];
return INVALID_MANIFEST_ID;
}
if (manifests.Children.Count == 0)
return INVALID_MANIFEST_ID; return INVALID_MANIFEST_ID;
}
node = manifests[branch]["gid"];
if (node.Value == null) if (node.Value == null)
return INVALID_MANIFEST_ID; return INVALID_MANIFEST_ID;
@ -319,12 +329,6 @@ namespace DepotDownloader
public static void ShutdownSteam3() public static void ShutdownSteam3()
{ {
if (cdnPool != null)
{
cdnPool.Shutdown();
cdnPool = null;
}
if (steam3 == null) if (steam3 == null)
return; return;
@ -421,7 +425,7 @@ namespace DepotDownloader
if (!await AccountHasAccess(appId, appId)) 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); Console.WriteLine("Obtained FreeOnDemand license for app {0}", appId);
@ -538,6 +542,8 @@ namespace DepotDownloader
} }
} }
Console.WriteLine();
try try
{ {
await DownloadSteam3Async(infos).ConfigureAwait(false); await DownloadSteam3Async(infos).ConfigureAwait(false);
@ -595,13 +601,25 @@ namespace DepotDownloader
return null; return null;
} }
return new DepotDownloadInfo(depotId, appId, manifestId, branch, installDir, depotKey); // 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)
{
var common = GetSteam3AppSection(appId, EAppInfoSection.Common);
if (common == null || !common["FreeToDownload"].AsBoolean())
{
containingAppId = proxyAppId;
}
}
return new DepotDownloadInfo(depotId, containingAppId, manifestId, branch, installDir, depotKey);
} }
private class ChunkMatch(ProtoManifest.ChunkData oldChunk, ProtoManifest.ChunkData newChunk) private class ChunkMatch(DepotManifest.ChunkData oldChunk, DepotManifest.ChunkData newChunk)
{ {
public ProtoManifest.ChunkData OldChunk { get; } = oldChunk; public DepotManifest.ChunkData OldChunk { get; } = oldChunk;
public ProtoManifest.ChunkData NewChunk { get; } = newChunk; public DepotManifest.ChunkData NewChunk { get; } = newChunk;
} }
private class DepotFilesData private class DepotFilesData
@ -609,9 +627,9 @@ namespace DepotDownloader
public DepotDownloadInfo depotDownloadInfo; public DepotDownloadInfo depotDownloadInfo;
public DepotDownloadCounter depotCounter; public DepotDownloadCounter depotCounter;
public string stagingDir; public string stagingDir;
public ProtoManifest manifest; public DepotManifest manifest;
public ProtoManifest previousManifest; public DepotManifest previousManifest;
public List<ProtoManifest.FileData> filteredFiles; public List<DepotManifest.FileData> filteredFiles;
public HashSet<string> allFileNames; public HashSet<string> allFileNames;
} }
@ -641,9 +659,9 @@ namespace DepotDownloader
{ {
Ansi.Progress(Ansi.ProgressState.Indeterminate); Ansi.Progress(Ansi.ProgressState.Indeterminate);
var cts = new CancellationTokenSource(); await cdnPool.UpdateServerList();
cdnPool.ExhaustedToken = cts;
var cts = new CancellationTokenSource();
var downloadCounter = new GlobalDownloadCounter(); var downloadCounter = new GlobalDownloadCounter();
var depotsToDownload = new List<DepotFilesData>(depots.Count); var depotsToDownload = new List<DepotFilesData>(depots.Count);
var allFileNamesAllDepots = new HashSet<string>(); var allFileNamesAllDepots = new HashSet<string>();
@ -694,8 +712,8 @@ namespace DepotDownloader
Console.WriteLine("Processing depot {0}", depot.DepotId); Console.WriteLine("Processing depot {0}", depot.DepotId);
ProtoManifest oldProtoManifest = null; DepotManifest oldManifest = null;
ProtoManifest newProtoManifest = null; DepotManifest newManifest = null;
var configDir = Path.Combine(depot.InstallDir, CONFIG_DIR); var configDir = Path.Combine(depot.InstallDir, CONFIG_DIR);
var lastManifestId = INVALID_MANIFEST_ID; var lastManifestId = INVALID_MANIFEST_ID;
@ -707,72 +725,28 @@ namespace DepotDownloader
if (lastManifestId != INVALID_MANIFEST_ID) if (lastManifestId != INVALID_MANIFEST_ID)
{ {
var oldManifestFileName = Path.Combine(configDir, string.Format("{0}_{1}.bin", depot.DepotId, lastManifestId)); // We only have to show this warning if the old manifest ID was different
var badHashWarning = (lastManifestId != depot.ManifestId);
if (File.Exists(oldManifestFileName)) oldManifest = Util.LoadManifestFromFile(configDir, depot.DepotId, lastManifestId, badHashWarning);
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(oldManifestFileName + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
oldProtoManifest = ProtoManifest.LoadFromFile(oldManifestFileName, out var currentChecksum);
if (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum))
{
// We only have to show this warning if the old manifest ID was different
if (lastManifestId != depot.ManifestId)
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", lastManifestId);
oldProtoManifest = null;
}
}
} }
if (lastManifestId == depot.ManifestId && oldProtoManifest != null) if (lastManifestId == depot.ManifestId && oldManifest != null)
{ {
newProtoManifest = oldProtoManifest; newManifest = oldManifest;
Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.DepotId); Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.DepotId);
} }
else else
{ {
var newManifestFileName = Path.Combine(configDir, string.Format("{0}_{1}.bin", depot.DepotId, depot.ManifestId)); newManifest = Util.LoadManifestFromFile(configDir, depot.DepotId, depot.ManifestId, true);
if (newManifestFileName != null)
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(newManifestFileName + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
newProtoManifest = ProtoManifest.LoadFromFile(newManifestFileName, out var currentChecksum); if (newManifest != null)
if (newProtoManifest != null && (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum)))
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", depot.ManifestId);
newProtoManifest = null;
}
}
if (newProtoManifest != null)
{ {
Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.DepotId); Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.DepotId);
} }
else else
{ {
Console.Write("Downloading depot manifest... "); Console.WriteLine($"Downloading depot {depot.DepotId} manifest");
DepotManifest depotManifest = null;
ulong manifestRequestCode = 0; ulong manifestRequestCode = 0;
var manifestRequestCodeExpiration = DateTime.MinValue; var manifestRequestCodeExpiration = DateTime.MinValue;
@ -784,7 +758,7 @@ namespace DepotDownloader
try try
{ {
connection = cdnPool.GetConnection(cts.Token); connection = cdnPool.GetConnection();
string cdnToken = null; string cdnToken = null;
if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise)) if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise))
@ -810,7 +784,6 @@ namespace DepotDownloader
// If we could not get the manifest code, this is a fatal error // If we could not get the manifest code, this is a fatal error
if (manifestRequestCode == 0) if (manifestRequestCode == 0)
{ {
Console.WriteLine("No manifest request code was returned for {0} {1}", depot.DepotId, depot.ManifestId);
cts.Cancel(); cts.Cancel();
} }
} }
@ -820,7 +793,7 @@ namespace DepotDownloader
depot.ManifestId, depot.ManifestId,
connection, connection,
cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy"); cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy");
depotManifest = await cdnPool.CDNClient.DownloadManifestAsync( newManifest = await cdnPool.CDNClient.DownloadManifestAsync(
depot.DepotId, depot.DepotId,
depot.ManifestId, depot.ManifestId,
manifestRequestCode, manifestRequestCode,
@ -872,9 +845,9 @@ namespace DepotDownloader
cdnPool.ReturnBrokenConnection(connection); cdnPool.ReturnBrokenConnection(connection);
Console.WriteLine("Encountered error downloading manifest for depot {0} {1}: {2}", depot.DepotId, depot.ManifestId, e.Message); Console.WriteLine("Encountered error downloading manifest for depot {0} {1}: {2}", depot.DepotId, depot.ManifestId, e.Message);
} }
} while (depotManifest == null); } while (newManifest == null);
if (depotManifest == null) if (newManifest == null)
{ {
Console.WriteLine("\nUnable to download manifest {0} for depot {1}", depot.ManifestId, depot.DepotId); Console.WriteLine("\nUnable to download manifest {0} for depot {1}", depot.ManifestId, depot.DepotId);
cts.Cancel(); cts.Cancel();
@ -883,28 +856,21 @@ namespace DepotDownloader
// Throw the cancellation exception if requested so that this task is marked failed // Throw the cancellation exception if requested so that this task is marked failed
cts.Token.ThrowIfCancellationRequested(); cts.Token.ThrowIfCancellationRequested();
Util.SaveManifestToFile(configDir, newManifest);
newProtoManifest = new ProtoManifest(depotManifest, depot.ManifestId);
newProtoManifest.SaveToFile(newManifestFileName, out var checksum);
File.WriteAllBytes(newManifestFileName + ".sha", checksum);
Console.WriteLine(" Done!");
} }
} }
newProtoManifest.Files.Sort((x, y) => string.Compare(x.FileName, y.FileName, StringComparison.Ordinal)); Console.WriteLine("Manifest {0} ({1})", depot.ManifestId, newManifest.CreationTime);
Console.WriteLine("Manifest {0} ({1})", depot.ManifestId, newProtoManifest.CreationTime);
if (Config.DownloadManifestOnly) if (Config.DownloadManifestOnly)
{ {
DumpManifestToTextFile(depot, newProtoManifest); DumpManifestToTextFile(depot, newManifest);
return null; return null;
} }
var stagingDir = Path.Combine(depot.InstallDir, STAGING_DIR); var stagingDir = Path.Combine(depot.InstallDir, STAGING_DIR);
var filesAfterExclusions = newProtoManifest.Files.AsParallel().Where(f => TestIsFileIncluded(f.FileName)).ToList(); var filesAfterExclusions = newManifest.Files.AsParallel().Where(f => TestIsFileIncluded(f.FileName)).ToList();
var allFileNames = new HashSet<string>(filesAfterExclusions.Count); var allFileNames = new HashSet<string>(filesAfterExclusions.Count);
// Pre-process // Pre-process
@ -936,8 +902,8 @@ namespace DepotDownloader
depotDownloadInfo = depot, depotDownloadInfo = depot,
depotCounter = depotCounter, depotCounter = depotCounter,
stagingDir = stagingDir, stagingDir = stagingDir,
manifest = newProtoManifest, manifest = newManifest,
previousManifest = oldProtoManifest, previousManifest = oldManifest,
filteredFiles = filesAfterExclusions, filteredFiles = filesAfterExclusions,
allFileNames = allFileNames allFileNames = allFileNames
}; };
@ -952,20 +918,27 @@ namespace DepotDownloader
Console.WriteLine("Downloading depot {0}", depot.DepotId); Console.WriteLine("Downloading depot {0}", depot.DepotId);
var files = depotFilesData.filteredFiles.Where(f => !f.Flags.HasFlag(EDepotFileFlag.Directory)).ToArray(); var files = depotFilesData.filteredFiles.Where(f => !f.Flags.HasFlag(EDepotFileFlag.Directory)).ToArray();
var networkChunkQueue = new ConcurrentQueue<(FileStreamData fileStreamData, ProtoManifest.FileData fileData, ProtoManifest.ChunkData chunk)>(); var networkChunkQueue = new ConcurrentQueue<(FileStreamData fileStreamData, DepotManifest.FileData fileData, DepotManifest.ChunkData chunk)>();
await Util.InvokeAsync( var parallelOptions = new ParallelOptions
files.Select(file => new Func<Task>(async () => {
await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue)))), MaxDegreeOfParallelism = Config.MaxDownloads,
maxDegreeOfParallelism: Config.MaxDownloads CancellationToken = cts.Token
); };
await Util.InvokeAsync( await Parallel.ForEachAsync(files, parallelOptions, async (file, cancellationToken) =>
networkChunkQueue.Select(q => new Func<Task>(async () => {
await Task.Run(() => DownloadSteam3AsyncDepotFileChunk(cts, downloadCounter, depotFilesData, await Task.Yield();
q.fileData, q.fileStreamData, q.chunk)))), DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue);
maxDegreeOfParallelism: Config.MaxDownloads });
);
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. // Check for deleted files if updating the depot.
if (depotFilesData.previousManifest != null) if (depotFilesData.previousManifest != null)
@ -1006,8 +979,8 @@ namespace DepotDownloader
CancellationTokenSource cts, CancellationTokenSource cts,
GlobalDownloadCounter downloadCounter, GlobalDownloadCounter downloadCounter,
DepotFilesData depotFilesData, DepotFilesData depotFilesData,
ProtoManifest.FileData file, DepotManifest.FileData file,
ConcurrentQueue<(FileStreamData, ProtoManifest.FileData, ProtoManifest.ChunkData)> networkChunkQueue) ConcurrentQueue<(FileStreamData, DepotManifest.FileData, DepotManifest.ChunkData)> networkChunkQueue)
{ {
cts.Token.ThrowIfCancellationRequested(); cts.Token.ThrowIfCancellationRequested();
@ -1015,7 +988,7 @@ namespace DepotDownloader
var stagingDir = depotFilesData.stagingDir; var stagingDir = depotFilesData.stagingDir;
var depotDownloadCounter = depotFilesData.depotCounter; var depotDownloadCounter = depotFilesData.depotCounter;
var oldProtoManifest = depotFilesData.previousManifest; var oldProtoManifest = depotFilesData.previousManifest;
ProtoManifest.FileData oldManifestFile = null; DepotManifest.FileData oldManifestFile = null;
if (oldProtoManifest != null) if (oldProtoManifest != null)
{ {
oldManifestFile = oldProtoManifest.Files.SingleOrDefault(f => f.FileName == file.FileName); oldManifestFile = oldProtoManifest.Files.SingleOrDefault(f => f.FileName == file.FileName);
@ -1030,7 +1003,7 @@ namespace DepotDownloader
File.Delete(fileStagingPath); File.Delete(fileStagingPath);
} }
List<ProtoManifest.ChunkData> neededChunks; List<DepotManifest.ChunkData> neededChunks;
var fi = new FileInfo(fileFinalPath); var fi = new FileInfo(fileFinalPath);
var fileDidExist = fi.Exists; var fileDidExist = fi.Exists;
if (!fileDidExist) if (!fileDidExist)
@ -1048,7 +1021,7 @@ namespace DepotDownloader
throw new ContentDownloaderException(string.Format("Failed to allocate file {0}: {1}", fileFinalPath, ex.Message)); throw new ContentDownloaderException(string.Format("Failed to allocate file {0}: {1}", fileFinalPath, ex.Message));
} }
neededChunks = new List<ProtoManifest.ChunkData>(file.Chunks); neededChunks = new List<DepotManifest.ChunkData>(file.Chunks);
} }
else else
{ {
@ -1092,7 +1065,7 @@ namespace DepotDownloader
fsOld.Seek((long)match.OldChunk.Offset, SeekOrigin.Begin); fsOld.Seek((long)match.OldChunk.Offset, SeekOrigin.Begin);
var adler = Util.AdlerHash(fsOld, (int)match.OldChunk.UncompressedLength); var adler = Util.AdlerHash(fsOld, (int)match.OldChunk.UncompressedLength);
if (!adler.SequenceEqual(match.OldChunk.Checksum)) if (!adler.SequenceEqual(BitConverter.GetBytes(match.OldChunk.Checksum)))
{ {
neededChunks.Add(match.NewChunk); neededChunks.Add(match.NewChunk);
} }
@ -1211,9 +1184,9 @@ namespace DepotDownloader
CancellationTokenSource cts, CancellationTokenSource cts,
GlobalDownloadCounter downloadCounter, GlobalDownloadCounter downloadCounter,
DepotFilesData depotFilesData, DepotFilesData depotFilesData,
ProtoManifest.FileData file, DepotManifest.FileData file,
FileStreamData fileStreamData, FileStreamData fileStreamData,
ProtoManifest.ChunkData chunk) DepotManifest.ChunkData chunk)
{ {
cts.Token.ThrowIfCancellationRequested(); cts.Token.ThrowIfCancellationRequested();
@ -1222,17 +1195,8 @@ namespace DepotDownloader
var chunkID = Convert.ToHexString(chunk.ChunkID).ToLowerInvariant(); var chunkID = Convert.ToHexString(chunk.ChunkID).ToLowerInvariant();
var data = new DepotManifest.ChunkData
{
ChunkID = chunk.ChunkID,
Checksum = BitConverter.ToUInt32(chunk.Checksum),
Offset = chunk.Offset,
CompressedLength = chunk.CompressedLength,
UncompressedLength = chunk.UncompressedLength
};
var written = 0; var written = 0;
var chunkBuffer = ArrayPool<byte>.Shared.Rent((int)data.UncompressedLength); var chunkBuffer = ArrayPool<byte>.Shared.Rent((int)chunk.UncompressedLength);
try try
{ {
@ -1244,7 +1208,7 @@ namespace DepotDownloader
try try
{ {
connection = cdnPool.GetConnection(cts.Token); connection = cdnPool.GetConnection();
string cdnToken = null; string cdnToken = null;
if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise)) if (steam3.CDNAuthTokens.TryGetValue((depot.DepotId, connection.Host), out var authTokenCallbackPromise))
@ -1256,7 +1220,7 @@ namespace DepotDownloader
DebugLog.WriteLine("ContentDownloader", "Downloading chunk {0} from {1} with {2}", chunkID, connection, cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy"); DebugLog.WriteLine("ContentDownloader", "Downloading chunk {0} from {1} with {2}", chunkID, connection, cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy");
written = await cdnPool.CDNClient.DownloadDepotChunkAsync( written = await cdnPool.CDNClient.DownloadDepotChunkAsync(
depot.DepotId, depot.DepotId,
data, chunk,
connection, connection,
chunkBuffer, chunkBuffer,
depot.DepotKey, depot.DepotKey,
@ -1270,6 +1234,7 @@ namespace DepotDownloader
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
Console.WriteLine("Connection timeout downloading chunk {0}", chunkID); Console.WriteLine("Connection timeout downloading chunk {0}", chunkID);
cdnPool.ReturnBrokenConnection(connection);
} }
catch (SteamKitWebRequestException e) catch (SteamKitWebRequestException e)
{ {
@ -1325,7 +1290,7 @@ namespace DepotDownloader
fileStreamData.fileStream = File.Open(fileFinalPath, FileMode.Open); fileStreamData.fileStream = File.Open(fileFinalPath, FileMode.Open);
} }
fileStreamData.fileStream.Seek((long)data.Offset, SeekOrigin.Begin); fileStreamData.fileStream.Seek((long)chunk.Offset, SeekOrigin.Begin);
await fileStreamData.fileStream.WriteAsync(chunkBuffer.AsMemory(0, written), cts.Token); await fileStreamData.fileStream.WriteAsync(chunkBuffer.AsMemory(0, written), cts.Token);
} }
finally finally
@ -1369,44 +1334,55 @@ namespace DepotDownloader
} }
} }
static void DumpManifestToTextFile(DepotDownloadInfo depot, ProtoManifest manifest) class ChunkIdComparer : IEqualityComparer<byte[]>
{
public bool Equals(byte[] x, byte[] y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.SequenceEqual(y);
}
public int GetHashCode(byte[] obj)
{
ArgumentNullException.ThrowIfNull(obj);
// ChunkID is SHA-1, so we can just use the first 4 bytes
return BitConverter.ToInt32(obj, 0);
}
}
static void DumpManifestToTextFile(DepotDownloadInfo depot, DepotManifest manifest)
{ {
var txtManifest = Path.Combine(depot.InstallDir, $"manifest_{depot.DepotId}_{depot.ManifestId}.txt"); var txtManifest = Path.Combine(depot.InstallDir, $"manifest_{depot.DepotId}_{depot.ManifestId}.txt");
using var sw = new StreamWriter(txtManifest); using var sw = new StreamWriter(txtManifest);
sw.WriteLine($"Content Manifest for Depot {depot.DepotId}"); sw.WriteLine($"Content Manifest for Depot {depot.DepotId} ");
sw.WriteLine(); sw.WriteLine();
sw.WriteLine($"Manifest ID / date : {depot.ManifestId} / {manifest.CreationTime}"); sw.WriteLine($"Manifest ID / date : {depot.ManifestId} / {manifest.CreationTime} ");
int numFiles = 0, numChunks = 0; var uniqueChunks = new HashSet<byte[]>(new ChunkIdComparer());
ulong uncompressedSize = 0, compressedSize = 0;
foreach (var file in manifest.Files) foreach (var file in manifest.Files)
{ {
if (file.Flags.HasFlag(EDepotFileFlag.Directory))
continue;
numFiles++;
numChunks += file.Chunks.Count;
foreach (var chunk in file.Chunks) foreach (var chunk in file.Chunks)
{ {
uncompressedSize += chunk.UncompressedLength; uniqueChunks.Add(chunk.ChunkID);
compressedSize += chunk.CompressedLength;
} }
} }
sw.WriteLine($"Total number of files : {numFiles}"); sw.WriteLine($"Total number of files : {manifest.Files.Count} ");
sw.WriteLine($"Total number of chunks : {numChunks}"); sw.WriteLine($"Total number of chunks : {uniqueChunks.Count} ");
sw.WriteLine($"Total bytes on disk : {uncompressedSize}"); sw.WriteLine($"Total bytes on disk : {manifest.TotalUncompressedSize} ");
sw.WriteLine($"Total bytes compressed : {compressedSize}"); sw.WriteLine($"Total bytes compressed : {manifest.TotalCompressedSize} ");
sw.WriteLine();
sw.WriteLine(); sw.WriteLine();
sw.WriteLine(" Size Chunks File SHA Flags Name"); sw.WriteLine(" Size Chunks File SHA Flags Name");
foreach (var file in manifest.Files) foreach (var file in manifest.Files)
{ {
var sha1Hash = Convert.ToHexString(file.FileHash); var sha1Hash = Convert.ToHexString(file.FileHash).ToLower();
sw.WriteLine($"{file.TotalSize,14} {file.Chunks.Count,6} {sha1Hash} {file.Flags,5:D} {file.FileName}"); sw.WriteLine($"{file.TotalSize,14:d} {file.Chunks.Count,6:d} {sha1Hash} {(int)file.Flags,5:x} {file.FileName}");
} }
} }
} }

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RollForward>LatestMajor</RollForward> <RollForward>LatestMajor</RollForward>
<Version>2.7.3</Version> <Version>3.4.0</Version>
<Description>Steam Downloading Utility</Description> <Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors> <Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2024</Copyright> <Copyright>Copyright © SteamRE Team 2025</Copyright>
<ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon> <ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors> <TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
@ -21,12 +21,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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="QRCoder" Version="1.6.0" />
<PackageReference Include="SteamKit2" Version="3.0.0" /> <PackageReference Include="SteamKit2" Version="3.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

@ -11,11 +11,14 @@ using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using SteamKit2; using SteamKit2;
using SteamKit2.CDN;
namespace DepotDownloader namespace DepotDownloader
{ {
class Program class Program
{ {
private static bool[] consumedArgs;
static async Task<int> Main(string[] args) static async Task<int> Main(string[] args)
{ {
if (args.Length == 0) if (args.Length == 0)
@ -46,6 +49,8 @@ namespace DepotDownloader
return 0; return 0;
} }
consumedArgs = new bool[args.Length];
if (HasParameter(args, "-debug")) if (HasParameter(args, "-debug"))
{ {
PrintVersion(true); PrintVersion(true);
@ -63,6 +68,21 @@ namespace DepotDownloader
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass"); var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass");
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password"); ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password");
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr"); ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr");
ContentDownloader.Config.SkipAppConfirmation = HasParameter(args, "-no-mobile");
if (username == null)
{
if (ContentDownloader.Config.RememberPassword && !ContentDownloader.Config.UseQrCode)
{
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"); ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only");
@ -117,9 +137,24 @@ namespace DepotDownloader
ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir"); ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir");
ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate"); 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"))
{
await Client.DetectLancacheServerAsync();
if (Client.UseLancacheServer)
{
Console.WriteLine("Detected Lancache server! Downloads will be directed through the Lancache.");
// Increasing the number of concurrent downloads when the cache is detected since the downloads will likely
// be served much faster than over the internet. Steam internally has this behavior as well.
if (!HasParameter(args, "-max-downloads"))
{
ContentDownloader.Config.MaxDownloads = 25;
}
}
}
ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8); 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; ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null;
#endregion #endregion
@ -137,6 +172,8 @@ namespace DepotDownloader
{ {
#region Pubfile Downloading #region Pubfile Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password)) if (InitializeSteam(username, password))
{ {
try try
@ -172,6 +209,8 @@ namespace DepotDownloader
{ {
#region UGC Downloading #region UGC Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password)) if (InitializeSteam(username, password))
{ {
try try
@ -208,7 +247,13 @@ namespace DepotDownloader
#region App downloading #region App downloading
var branch = GetParameter<string>(args, "-branch") ?? GetParameter<string>(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH; 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"); ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms");
@ -262,6 +307,8 @@ namespace DepotDownloader
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID)));
} }
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password)) if (InitializeSteam(username, password))
{ {
try try
@ -303,6 +350,11 @@ namespace DepotDownloader
{ {
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))) 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 do
{ {
Console.Write("Enter account password for \"{0}\": ", username); Console.Write("Enter account password for \"{0}\": ", username);
@ -325,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); return ContentDownloader.InitializeSteam3(username, password);
} }
@ -333,7 +400,10 @@ namespace DepotDownloader
for (var x = 0; x < args.Length; ++x) for (var x = 0; x < args.Length; ++x)
{ {
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase)) if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
{
consumedArgs[x] = true;
return x; return x;
}
} }
return -1; return -1;
@ -356,6 +426,7 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T)); var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null) if (converter != null)
{ {
consumedArgs[index + 1] = true;
return (T)converter.ConvertFromString(strParam); return (T)converter.ConvertFromString(strParam);
} }
@ -381,6 +452,7 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T)); var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null) if (converter != null)
{ {
consumedArgs[index] = true;
list.Add((T)converter.ConvertFromString(strParam)); list.Add((T)converter.ConvertFromString(strParam));
} }
@ -390,6 +462,26 @@ namespace DepotDownloader
return list; 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() static void PrintUsage()
{ {
// Do not use tabs to align parameters here because tab size may differ // Do not use tabs to align parameters here because tab size may differ
@ -407,8 +499,8 @@ namespace DepotDownloader
Console.WriteLine(" -app <#> - the AppID to download."); Console.WriteLine(" -app <#> - the AppID to download.");
Console.WriteLine(" -depot <#> - the DepotID 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(" -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($" -branch <branchname> - download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH}).");
Console.WriteLine(" -betapassword <pass> - branch password if applicable."); Console.WriteLine(" -branchpassword <pass> - branch password if applicable.");
Console.WriteLine(" -all-platforms - downloads all platform-specific depots when -app is used."); 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(" -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)"); 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)");
@ -424,6 +516,8 @@ namespace DepotDownloader
Console.WriteLine(" -password <pass> - the password of the account to login to for restricted content."); 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(" -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(" 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();
Console.WriteLine(" -dir <installdir> - the directory in which to place downloaded files."); 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)."); Console.WriteLine(" -filelist <file.txt> - the name of a local file that contains a list of files to download (from the manifest).");
@ -432,9 +526,12 @@ namespace DepotDownloader
Console.WriteLine(" -validate - include checksum verification of files already downloaded"); 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(" -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(" -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(" -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(" -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) static void PrintVersion(bool printExtra = false)

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using ProtoBuf; using ProtoBuf;
using SteamKit2; using SteamKit2;
@ -157,5 +158,39 @@ namespace DepotDownloader
using var ds = new DeflateStream(fs, CompressionMode.Compress); using var ds = new DeflateStream(fs, CompressionMode.Compress);
ms.CopyTo(ds); ms.CopyTo(ds);
} }
public DepotManifest ConvertToSteamManifest(uint depotId)
{
ulong uncompressedSize = 0, compressedSize = 0;
var newManifest = new DepotManifest();
newManifest.Files = new List<DepotManifest.FileData>(Files.Count);
foreach (var file in Files)
{
var fileNameHash = SHA1.HashData(Encoding.UTF8.GetBytes(file.FileName.Replace('/', '\\').ToLowerInvariant()));
var newFile = new DepotManifest.FileData(file.FileName, fileNameHash, file.Flags, file.TotalSize, file.FileHash, null, false, file.Chunks.Count);
foreach (var chunk in file.Chunks)
{
var newChunk = new DepotManifest.ChunkData(chunk.ChunkID, BitConverter.ToUInt32(chunk.Checksum, 0), chunk.Offset, chunk.CompressedLength, chunk.UncompressedLength);
newFile.Chunks.Add(newChunk);
uncompressedSize += chunk.UncompressedLength;
compressedSize += chunk.CompressedLength;
}
newManifest.Files.Add(newFile);
}
newManifest.FilenamesEncrypted = false;
newManifest.DepotID = depotId;
newManifest.ManifestGID = ID;
newManifest.CreationTime = CreationTime;
newManifest.TotalUncompressedSize = uncompressedSize;
newManifest.TotalCompressedSize = compressedSize;
newManifest.EncryptedCRC = 0;
return newManifest;
}
} }
} }

@ -64,7 +64,7 @@ namespace DepotDownloader
var clientConfiguration = SteamConfiguration.Create(config => var clientConfiguration = SteamConfiguration.Create(config =>
config config
.WithHttpClientFactory(HttpClientFactory.CreateHttpClient) .WithHttpClientFactory(static purpose => HttpClientFactory.CreateHttpClient())
); );
this.steamClient = new SteamClient(clientConfiguration); this.steamClient = new SteamClient(clientConfiguration);
@ -248,7 +248,6 @@ namespace DepotDownloader
if (depotKey.Result != EResult.OK) if (depotKey.Result != EResult.OK)
{ {
Abort();
return; return;
} }
@ -263,9 +262,19 @@ namespace DepotDownloader
var requestCode = await steamContent.GetManifestRequestCode(depotId, appId, manifestId, branch); var requestCode = await steamContent.GetManifestRequestCode(depotId, appId, manifestId, branch);
Console.WriteLine("Got manifest request code for {0} {1} result: {2}", if (requestCode == 0)
depotId, manifestId, {
requestCode); 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
{
Console.WriteLine($"Got manifest request code for depot {depotId} from app {appId}, manifest {manifestId}, result: {requestCode}");
}
return requestCode; return requestCode;
} }
@ -306,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) public async Task<PublishedFileDetails> GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
{ {
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId }; var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
@ -417,13 +442,14 @@ namespace DepotDownloader
try try
{ {
_ = AccountSettingsStore.Instance.GuardData.TryGetValue(logonDetails.Username, out var guarddata); _ = 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, Username = logonDetails.Username,
Password = logonDetails.Password, Password = logonDetails.Password,
IsPersistentSession = ContentDownloader.Config.RememberPassword, IsPersistentSession = ContentDownloader.Config.RememberPassword,
GuardData = guarddata, GuardData = guarddata,
Authenticator = new UserConsoleAuthenticator(), Authenticator = new ConsoleAuthenticator(),
}); });
} }
catch (TaskCanceledException) catch (TaskCanceledException)
@ -445,8 +471,8 @@ namespace DepotDownloader
{ {
var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails
{ {
DeviceFriendlyName = nameof(DepotDownloader),
IsPersistentSession = ContentDownloader.Config.RememberPassword, IsPersistentSession = ContentDownloader.Config.RememberPassword,
Authenticator = new UserConsoleAuthenticator(),
}); });
authSession = session; authSession = session;
@ -489,11 +515,17 @@ namespace DepotDownloader
if (result.NewGuardData != null) if (result.NewGuardData != null)
{ {
AccountSettingsStore.Instance.GuardData[result.AccountName] = result.NewGuardData; 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 else
{ {
AccountSettingsStore.Instance.GuardData.Remove(result.AccountName); AccountSettingsStore.Instance.GuardData.Remove(result.AccountName);
} }
AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken; AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken;
AccountSettingsStore.Save(); AccountSettingsStore.Save();
} }
@ -674,10 +706,14 @@ namespace DepotDownloader
using var qrGenerator = new QRCodeGenerator(); using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L); var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L);
using var qrCode = new AsciiQRCode(qrCodeData); 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("Use the Steam Mobile App to sign in with this QR code:");
Console.WriteLine(qrCodeAsAsciiArt);
foreach (var line in qrCodeAsAsciiArt)
{
Console.WriteLine(line);
}
} }
} }
} }

@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SteamKit2;
namespace DepotDownloader namespace DepotDownloader
{ {
@ -78,16 +79,16 @@ namespace DepotDownloader
} }
// Validate a file against Steam3 Chunk data // Validate a file against Steam3 Chunk data
public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata) public static List<DepotManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, DepotManifest.ChunkData[] chunkdata)
{ {
var neededChunks = new List<ProtoManifest.ChunkData>(); var neededChunks = new List<DepotManifest.ChunkData>();
foreach (var data in chunkdata) foreach (var data in chunkdata)
{ {
fs.Seek((long)data.Offset, SeekOrigin.Begin); fs.Seek((long)data.Offset, SeekOrigin.Begin);
var adler = AdlerHash(fs, (int)data.UncompressedLength); var adler = AdlerHash(fs, (int)data.UncompressedLength);
if (!adler.SequenceEqual(data.Checksum)) if (!adler.SequenceEqual(BitConverter.GetBytes(data.Checksum)))
{ {
neededChunks.Add(data); neededChunks.Add(data);
} }
@ -110,6 +111,100 @@ namespace DepotDownloader
return BitConverter.GetBytes(a | (b << 16)); return BitConverter.GetBytes(a | (b << 16));
} }
public static byte[] FileSHAHash(string filename)
{
using (var fs = File.Open(filename, FileMode.Open))
using (var sha = SHA1.Create())
{
var output = sha.ComputeHash(fs);
return output;
}
}
public static DepotManifest LoadManifestFromFile(string directory, uint depotId, ulong manifestId, bool badHashWarning)
{
// Try loading Steam format manifest first.
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
var currentChecksum = FileSHAHash(filename);
if (expectedChecksum != null && expectedChecksum.SequenceEqual(currentChecksum))
{
return DepotManifest.LoadFromFile(filename);
}
else if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
// Try converting legacy manifest format.
filename = Path.Combine(directory, string.Format("{0}_{1}.bin", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
byte[] currentChecksum;
var oldManifest = ProtoManifest.LoadFromFile(filename, out currentChecksum);
if (oldManifest != null && (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum)))
{
oldManifest = null;
if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
if (oldManifest != null)
{
return oldManifest.ConvertToSteamManifest(depotId);
}
}
return null;
}
public static bool SaveManifestToFile(string directory, DepotManifest manifest)
{
try
{
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", manifest.DepotID, manifest.ManifestGID));
manifest.SaveToFile(filename);
File.WriteAllBytes(filename + ".sha", FileSHAHash(filename));
return true; // If serialization completes without throwing an exception, return true
}
catch (Exception)
{
return false; // Return false if an error occurs
}
}
public static byte[] DecodeHexString(string hex) public static byte[] DecodeHexString(string hex)
{ {
if (hex == null) if (hex == null)
@ -140,37 +235,5 @@ namespace DepotDownloader
return output; 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,34 +61,54 @@ For example: `./DepotDownloader -app 730 -ugc 770604181014286929`
## Parameters ## Parameters
#### Authentication
Parameter | Description
----------------------- | -----------
`-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 Parameter | Description
----------------------- | ----------- ----------------------- | -----------
`-app <#>` | the AppID to download. `-all-platforms` | downloads all platform-specific depots when `-app` is used.
`-depot <#>` | the DepotID to download. `-os <os>` | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch). `-osarch <arch>` | the architecture for which to download the game (32 or 64, default: the host's architecture)
`-ugc <#>` | the UGC ID to download. `-all-archs` | download all architecture-specific depots when `-app` is used.
`-beta <branchname>` | download from specified branch if available (default: Public). `-all-languages` | download all language-specific depots when `-app` is used.
`-betapassword <pass>` | branch password if applicable. `-language <lang>` | the language for which to download the game (default: english)
`-all-platforms` | downloads all platform-specific depots when `-app` is used. `-lowviolence` | download low violence 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)
`-dir <installdir>` | the directory in which to place downloaded files. `-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. `-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 `-validate` | include checksum verification of files already downloaded.
`-manifest-only` | downloads a human readable manifest for any depots that would be 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. `-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).
`-max-downloads <#>` | maximum number of chunks to download concurrently. (default: 8). `-use-lancache` | forces downloads over the local network via a Lancache instance.
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
`-V` or `--version` | print version and runtime #### Other
Parameter | Description
----------------------- | -----------
`-debug` | enable verbose debug logging.
`-V` or `--version` | print version and runtime.
## Frequently Asked Questions ## Frequently Asked Questions
@ -100,3 +120,12 @@ Any connection to Steam will be closed if they share a LoginID. You can specify
### Why doesn't my password containing special characters work? Do I have to specify the password on the command line? ### Why doesn't my password containing special characters work? Do I have to specify the password on the command line?
If you pass the `-password` parameter with a password that contains special characters, you will need to escape the command appropriately for the shell you are using. You do not have to include the `-password` parameter on the command line as long as you include a `-username`. You will be prompted to enter your password interactively. If you pass the `-password` parameter with a password that contains special characters, you will need to escape the command appropriately for the shell you are using. You do not have to include the `-password` parameter on the command line as long as you include a `-username`. You will be prompted to enter your password interactively.
### I am getting error 401 or no manifest code returned for old manifest ids
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