mirror of
https://github.com/SteamRE/DepotDownloader.git
synced 2026-10-20 11:39:47 +00:00
Compare commits
110
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6396afe2e3 | ||
|
|
16a95c1bfd | ||
|
|
989f37b1fb | ||
|
|
8275f16d3e | ||
|
|
e7474cb9ee | ||
|
|
c124842e86 | ||
|
|
b1557786b2 | ||
|
|
ae192f6448 | ||
|
|
48b0f51c53 | ||
|
|
1e8e20c72c | ||
|
|
c5fc766986 | ||
|
|
d178832b38 | ||
|
|
b2b7e975ad | ||
|
|
91d623419a | ||
|
|
282b9eae81 | ||
|
|
66c63c6dad | ||
|
|
3dc34b50bd | ||
|
|
4aac37401c | ||
|
|
58794cbbb3 | ||
|
|
81cf3f6b3d | ||
|
|
4d56522e44 | ||
|
|
c220fc5b88 | ||
|
|
f5318b9ed6 | ||
|
|
370b800967 | ||
|
|
027b4367fc | ||
|
|
43a31b2713 | ||
|
|
68089f6e7c | ||
|
|
e8e3d1bbfc | ||
|
|
f5c7f2d428 | ||
|
|
5baa3f7625 | ||
|
|
8f8d0c9adf | ||
|
|
b8ca1d44f1 | ||
|
|
007857c13f | ||
|
|
77f9067eaa | ||
|
|
9a2bafeb05 | ||
|
|
748fe11132 | ||
|
|
97653ac481 | ||
|
|
bfeeeb5b40 | ||
|
|
d5afc41b97 | ||
|
|
3d8b7dd696 | ||
|
|
560e48a8b5 | ||
|
|
54b0af377b | ||
|
|
5164a9f963 | ||
|
|
2d2113a8e9 | ||
|
|
5d6950afd9 | ||
|
|
b042b055c4 | ||
|
|
02a525acdb | ||
|
|
c553ef4d60 | ||
|
|
9e203e0d7c | ||
|
|
76038d83e8 | ||
|
|
c21489d304 | ||
|
|
0d37091adf | ||
|
|
0d66cf09ac | ||
|
|
8d875579c5 | ||
|
|
ff9c709787 | ||
|
|
bce88e4d32 | ||
|
|
08542bd09f | ||
|
|
665f83983b | ||
|
|
272f5b646a | ||
|
|
f078581947 | ||
|
|
4896ac0788 | ||
|
|
401d086191 | ||
|
|
8d8c6fc59a | ||
|
|
19df5910c3 | ||
|
|
1e72a47846 | ||
|
|
2c07abb015 | ||
|
|
2682d44684 | ||
|
|
be3682cd4b | ||
|
|
644e3f1ebc | ||
|
|
56822a831f | ||
|
|
001f5303a7 | ||
|
|
0150b7eff4 | ||
|
|
14c6a6dafa | ||
|
|
0617974ac0 | ||
|
|
3249d284ba | ||
|
|
7b3cf4c6c7 | ||
|
|
b6fc6d5c6f | ||
|
|
4dbe7ede14 | ||
|
|
24d7f0b02a | ||
|
|
3bd2cf52ba | ||
|
|
f660b95d3f | ||
|
|
17d3996588 | ||
|
|
5d0c4fb050 | ||
|
|
d087678628 | ||
|
|
deac83600c | ||
|
|
f567a63d84 | ||
|
|
e9a5abecb7 | ||
|
|
5fe8a827d8 | ||
|
|
682bfbac27 | ||
|
|
1c97b42124 | ||
|
|
0fb803b5f6 | ||
|
|
2a939053d0 | ||
|
|
4539666214 | ||
|
|
fac3a22f4f | ||
|
|
6c09739c8c | ||
|
|
ff37997713 | ||
|
|
72ffd79da6 | ||
|
|
2d06bf1c63 | ||
|
|
6a9364029b | ||
|
|
9ed8a70508 | ||
|
|
247f8324ca | ||
|
|
a5bdf23880 | ||
|
|
926c41e7e0 | ||
|
|
33738aeb49 | ||
|
|
177c44ef4a | ||
|
|
9f64bd0439 | ||
|
|
e7d637ace6 | ||
|
|
39e43a1bf9 | ||
|
|
b310d804f2 | ||
|
|
ddda3924dc |
No files matched your search
@@ -3,7 +3,7 @@ updates:
|
||||
- package-ecosystem: nuget
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
|
||||
+12
-14
@@ -25,18 +25,16 @@ jobs:
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
uses: actions/setup-dotnet@v6
|
||||
|
||||
- name: Build
|
||||
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
|
||||
with:
|
||||
name: DepotDownloader-framework
|
||||
@@ -72,7 +70,7 @@ jobs:
|
||||
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime osx-arm64 --output selfcontained-osx-arm64
|
||||
|
||||
- name: Upload Windows-x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
|
||||
with:
|
||||
name: DepotDownloader-windows-x64
|
||||
@@ -80,7 +78,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows-arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
|
||||
with:
|
||||
name: DepotDownloader-windows-arm64
|
||||
@@ -88,7 +86,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux-x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
|
||||
with:
|
||||
name: DepotDownloader-linux-x64
|
||||
@@ -96,7 +94,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux-arm
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
|
||||
with:
|
||||
name: DepotDownloader-linux-arm
|
||||
@@ -104,7 +102,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux-arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
|
||||
with:
|
||||
name: DepotDownloader-linux-arm64
|
||||
@@ -112,7 +110,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload macOS-x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-latest'
|
||||
with:
|
||||
name: DepotDownloader-macos-x64
|
||||
@@ -120,7 +118,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload macOS-arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-14'
|
||||
with:
|
||||
name: DepotDownloader-macos-arm64
|
||||
@@ -133,7 +131,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
@@ -164,7 +162,7 @@ jobs:
|
||||
working-directory: release
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
files: release/*
|
||||
|
||||
@@ -17,12 +17,10 @@ jobs:
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
uses: actions/setup-dotnet@v6
|
||||
|
||||
- name: Configure NuGet
|
||||
run: |
|
||||
@@ -33,7 +31,7 @@ jobs:
|
||||
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.configuration == 'Release'
|
||||
with:
|
||||
name: DepotDownloader-${{ matrix.runs-on }}
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
@@ -31,6 +31,11 @@ static class Ansi
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (supportsAnsi, legacyConsole) = AnsiDetector.Detect(stdError: false, upgrade: true);
|
||||
|
||||
useProgress = supportsAnsi && !legacyConsole;
|
||||
|
||||
@@ -7,6 +7,8 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Console;
|
||||
|
||||
namespace Spectre.Console;
|
||||
|
||||
@@ -45,7 +47,7 @@ internal static class AnsiDetector
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var supportsAnsi = Windows.SupportsAnsi(upgrade, stdError, out var legacyConsole);
|
||||
var supportsAnsi = WindowsSupportsAnsi(upgrade, stdError, out var legacyConsole);
|
||||
return (supportsAnsi, legacyConsole);
|
||||
}
|
||||
|
||||
@@ -67,68 +69,49 @@ internal static class AnsiDetector
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
private static class Windows
|
||||
private static bool WindowsSupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
|
||||
{
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
|
||||
private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
|
||||
isLegacy = false;
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern uint GetLastError();
|
||||
|
||||
public static bool SupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
|
||||
try
|
||||
{
|
||||
isLegacy = false;
|
||||
var @out = PInvoke.GetStdHandle_SafeHandle(stdError ? STD_HANDLE.STD_ERROR_HANDLE :STD_HANDLE.STD_OUTPUT_HANDLE);
|
||||
|
||||
try
|
||||
if (!PInvoke.GetConsoleMode(@out, out var mode))
|
||||
{
|
||||
var @out = GetStdHandle(stdError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
|
||||
if (!GetConsoleMode(@out, out var mode))
|
||||
{
|
||||
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
|
||||
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
|
||||
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
|
||||
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
|
||||
|
||||
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
|
||||
return ansiFromTerm;
|
||||
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
|
||||
return ansiFromTerm;
|
||||
}
|
||||
|
||||
if ((mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0||true)
|
||||
{
|
||||
isLegacy = true;
|
||||
|
||||
if (!upgrade)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
|
||||
// Try enable ANSI support.
|
||||
mode |= CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING | CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN;
|
||||
if (!PInvoke.SetConsoleMode(@out, mode))
|
||||
{
|
||||
isLegacy = true;
|
||||
|
||||
if (!upgrade)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try enable ANSI support.
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
|
||||
if (!SetConsoleMode(@out, mode))
|
||||
{
|
||||
// Enabling failed.
|
||||
return false;
|
||||
}
|
||||
|
||||
isLegacy = false;
|
||||
// Enabling failed.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// All we know here is that we don't support ANSI.
|
||||
return false;
|
||||
isLegacy = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// All we know here is that we don't support ANSI.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
|
||||
|
||||
var weightedCdnServers = servers
|
||||
.Where(server =>
|
||||
{
|
||||
return cdnServers;
|
||||
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++)
|
||||
{
|
||||
this.servers.Add(server);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (this.servers.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ConnectionPoolMonitorAsync()
|
||||
{
|
||||
var didPopulate = false;
|
||||
|
||||
while (!shutdownToken.IsCancellationRequested)
|
||||
{
|
||||
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)
|
||||
{
|
||||
ExhaustedToken?.Cancel();
|
||||
return;
|
||||
}
|
||||
throw new Exception("Failed to retrieve any download servers.");
|
||||
}
|
||||
}
|
||||
|
||||
private Server BuildConnection(CancellationToken token)
|
||||
public Server GetConnection()
|
||||
{
|
||||
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
|
||||
{
|
||||
populatePoolEvent.Set();
|
||||
}
|
||||
|
||||
return availableServerEndpoints.Take(token);
|
||||
}
|
||||
|
||||
public Server GetConnection(CancellationToken token)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -35,6 +36,16 @@ namespace DepotDownloader
|
||||
private const string CONFIG_DIR = ".DepotDownloader";
|
||||
private static readonly string STAGING_DIR = Path.Combine(CONFIG_DIR, "staging");
|
||||
|
||||
private static readonly FrozenSet<EWorkshopFileType> SupportedWorkshopFileTypes = FrozenSet.ToFrozenSet(new[]
|
||||
{
|
||||
EWorkshopFileType.Community,
|
||||
EWorkshopFileType.Art,
|
||||
EWorkshopFileType.Screenshot,
|
||||
EWorkshopFileType.Merch,
|
||||
EWorkshopFileType.IntegratedGuide,
|
||||
EWorkshopFileType.ControllerBinding,
|
||||
});
|
||||
|
||||
private sealed class DepotDownloadInfo(
|
||||
uint depotid, uint appId, ulong manifestId, string branch,
|
||||
string installDir, byte[] depotKey)
|
||||
@@ -106,7 +117,7 @@ namespace DepotDownloader
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool AccountHasAccess(uint depotId)
|
||||
static async Task<bool> AccountHasAccess(uint appId, uint depotId)
|
||||
{
|
||||
if (steam3 == null || steam3.steamUser.SteamID == null || (steam3.Licenses == null && steam3.steamUser.SteamID.AccountType != EAccountType.AnonUser))
|
||||
return false;
|
||||
@@ -121,7 +132,7 @@ namespace DepotDownloader
|
||||
licenseQuery = steam3.Licenses.Select(x => x.PackageID).Distinct();
|
||||
}
|
||||
|
||||
steam3.RequestPackageInfo(licenseQuery);
|
||||
await steam3.RequestPackageInfo(licenseQuery);
|
||||
|
||||
foreach (var license in licenseQuery)
|
||||
{
|
||||
@@ -135,6 +146,11 @@ namespace DepotDownloader
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this app is free to download without a license
|
||||
var info = GetSteam3AppSection(appId, EAppInfoSection.Common);
|
||||
if (info != null && info["FreeToDownload"].AsBoolean())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -184,7 +200,21 @@ namespace DepotDownloader
|
||||
return uint.Parse(buildid.Value);
|
||||
}
|
||||
|
||||
static ulong GetSteam3DepotManifest(uint depotId, uint appId, string branch)
|
||||
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)
|
||||
{
|
||||
var depots = GetSteam3AppSection(appId, EAppInfoSection.Depots);
|
||||
var depotChild = depots[depotId.ToString()];
|
||||
@@ -206,66 +236,62 @@ namespace DepotDownloader
|
||||
return INVALID_MANIFEST_ID;
|
||||
}
|
||||
|
||||
steam3.RequestAppInfo(otherAppId);
|
||||
await steam3.RequestAppInfo(otherAppId);
|
||||
|
||||
return GetSteam3DepotManifest(depotId, otherAppId, branch);
|
||||
return await GetSteam3DepotManifest(depotId, otherAppId, branch);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
var encrypted_gid = node_encrypted["gid"];
|
||||
|
||||
if (encrypted_gid != KeyValue.Invalid)
|
||||
{
|
||||
// Submit the password to Steam now to get encryption keys
|
||||
steam3.CheckAppBetaPassword(appId, Config.BetaPassword);
|
||||
|
||||
if (!steam3.AppBetaPasswords.TryGetValue(branch, out var appBetaPassword))
|
||||
{
|
||||
Console.WriteLine("Password was invalid for branch {0}", branch);
|
||||
return INVALID_MANIFEST_ID;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return BitConverter.ToUInt64(manifest_bytes, 0);
|
||||
}
|
||||
|
||||
Console.WriteLine("Unhandled depot encryption for depotId {0}", depotId);
|
||||
return INVALID_MANIFEST_ID;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Branch {branch} for depot {depotId} was not found, either it does not exist or it has a password.");
|
||||
return INVALID_MANIFEST_ID;
|
||||
}
|
||||
|
||||
if (!steam3.AppBetaPasswords.ContainsKey(branch))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Got the password, request private depot section
|
||||
// TODO: We're probably repeating this request for every depot?
|
||||
var privateDepotSection = await steam3.GetPrivateBetaDepotSection(appId, branch);
|
||||
|
||||
// Now repeat the same code to get the manifest gid from depot section
|
||||
depotChild = privateDepotSection[depotId.ToString()];
|
||||
|
||||
if (depotChild == KeyValue.Invalid)
|
||||
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;
|
||||
|
||||
@@ -307,38 +333,67 @@ namespace DepotDownloader
|
||||
return false;
|
||||
}
|
||||
|
||||
Task.Run(steam3.TickCallbacks);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ShutdownSteam3()
|
||||
{
|
||||
if (cdnPool != null)
|
||||
{
|
||||
cdnPool.Shutdown();
|
||||
cdnPool = null;
|
||||
}
|
||||
|
||||
if (steam3 == null)
|
||||
return;
|
||||
|
||||
steam3.Disconnect();
|
||||
}
|
||||
|
||||
public static async Task DownloadPubfileAsync(uint appId, ulong publishedFileId)
|
||||
private static async Task ProcessPublishedFileAsync(uint appId, ulong publishedFileId, List<ValueTuple<string, string>> fileUrls, List<ulong> contentFileIds)
|
||||
{
|
||||
var details = steam3.GetPublishedFileDetails(appId, publishedFileId);
|
||||
var details = await steam3.GetPublishedFileDetails(appId, publishedFileId);
|
||||
var fileType = (EWorkshopFileType)details.file_type;
|
||||
|
||||
if (!string.IsNullOrEmpty(details?.file_url))
|
||||
if (fileType == EWorkshopFileType.Collection)
|
||||
{
|
||||
await DownloadWebFile(appId, details.filename, details.file_url);
|
||||
foreach (var child in details.children)
|
||||
{
|
||||
await ProcessPublishedFileAsync(appId, child.publishedfileid, fileUrls, contentFileIds);
|
||||
}
|
||||
}
|
||||
else if (details?.hcontent_file > 0)
|
||||
else if (SupportedWorkshopFileTypes.Contains(fileType))
|
||||
{
|
||||
await DownloadAppAsync(appId, new List<(uint, ulong)> { (appId, details.hcontent_file) }, DEFAULT_BRANCH, null, null, null, false, true);
|
||||
if (!string.IsNullOrEmpty(details?.file_url))
|
||||
{
|
||||
fileUrls.Add((details.filename, details.file_url));
|
||||
}
|
||||
else if (details?.hcontent_file > 0)
|
||||
{
|
||||
contentFileIds.Add(details.hcontent_file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Unable to locate manifest ID for published file {0}", publishedFileId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Unable to locate manifest ID for published file {0}", publishedFileId);
|
||||
Console.WriteLine("Published file {0} has unsupported file type {1}. Skipping file", publishedFileId, fileType);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task DownloadPubfileAsync(uint appId, ulong publishedFileId)
|
||||
{
|
||||
List<ValueTuple<string, string>> fileUrls = new();
|
||||
List<ulong> contentFileIds = new();
|
||||
|
||||
await ProcessPublishedFileAsync(appId, publishedFileId, fileUrls, contentFileIds);
|
||||
|
||||
foreach (var item in fileUrls)
|
||||
{
|
||||
await DownloadWebFile(appId, item.Item1, item.Item2);
|
||||
}
|
||||
|
||||
if (contentFileIds.Count > 0)
|
||||
{
|
||||
var depotManifestIds = contentFileIds.Select(id => (appId, id)).ToList();
|
||||
await DownloadAppAsync(appId, depotManifestIds, DEFAULT_BRANCH, null, null, null, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +403,7 @@ namespace DepotDownloader
|
||||
|
||||
if (steam3.steamUser.SteamID.AccountType != EAccountType.AnonUser)
|
||||
{
|
||||
details = steam3.GetUGCDetails(ugcId);
|
||||
details = await steam3.GetUGCDetails(ugcId);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -361,7 +416,7 @@ namespace DepotDownloader
|
||||
}
|
||||
else
|
||||
{
|
||||
await DownloadAppAsync(appId, new List<(uint, ulong)> { (appId, ugcId) }, DEFAULT_BRANCH, null, null, null, false, true);
|
||||
await DownloadAppAsync(appId, [(appId, ugcId)], DEFAULT_BRANCH, null, null, null, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,16 +465,16 @@ namespace DepotDownloader
|
||||
Directory.CreateDirectory(Path.Combine(configPath, CONFIG_DIR));
|
||||
DepotConfigStore.LoadFromFile(Path.Combine(configPath, CONFIG_DIR, "depot.config"));
|
||||
|
||||
steam3?.RequestAppInfo(appId);
|
||||
await steam3?.RequestAppInfo(appId);
|
||||
|
||||
if (!AccountHasAccess(appId))
|
||||
if (!await AccountHasAccess(appId, appId))
|
||||
{
|
||||
if (steam3.RequestFreeAppLicense(appId))
|
||||
if (steam3.steamUser.SteamID.AccountType != EAccountType.AnonUser && await steam3.RequestFreeAppLicense(appId))
|
||||
{
|
||||
Console.WriteLine("Obtained FreeOnDemand license for app {0}", appId);
|
||||
|
||||
// Fetch app info again in case we didn't get it fully without a license.
|
||||
steam3.RequestAppInfo(appId, true);
|
||||
await steam3.RequestAppInfo(appId, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -476,7 +531,8 @@ namespace DepotDownloader
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depotConfig["osarch"] != KeyValue.Invalid &&
|
||||
if (!Config.DownloadAllArchs &&
|
||||
depotConfig["osarch"] != KeyValue.Invalid &&
|
||||
!string.IsNullOrWhiteSpace(depotConfig["osarch"].Value))
|
||||
{
|
||||
var depotArch = depotConfig["osarch"].Value;
|
||||
@@ -523,13 +579,15 @@ namespace DepotDownloader
|
||||
|
||||
foreach (var (depotId, manifestId) in depotManifestIds)
|
||||
{
|
||||
var info = GetDepotInfo(depotId, appId, manifestId, branch);
|
||||
var info = await GetDepotInfo(depotId, appId, manifestId, branch);
|
||||
if (info != null)
|
||||
{
|
||||
infos.Add(info);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
await DownloadSteam3Async(infos).ConfigureAwait(false);
|
||||
@@ -541,12 +599,14 @@ namespace DepotDownloader
|
||||
}
|
||||
}
|
||||
|
||||
static DepotDownloadInfo GetDepotInfo(uint depotId, uint appId, ulong manifestId, string branch)
|
||||
static async Task<DepotDownloadInfo> GetDepotInfo(uint depotId, uint appId, ulong manifestId, string branch)
|
||||
{
|
||||
if (steam3 != null && appId != INVALID_APP_ID)
|
||||
steam3.RequestAppInfo(appId);
|
||||
{
|
||||
await steam3.RequestAppInfo(appId);
|
||||
}
|
||||
|
||||
if (!AccountHasAccess(depotId))
|
||||
if (!await AccountHasAccess(appId, depotId))
|
||||
{
|
||||
Console.WriteLine("Depot {0} is not available from this account.", depotId);
|
||||
|
||||
@@ -555,12 +615,12 @@ namespace DepotDownloader
|
||||
|
||||
if (manifestId == INVALID_MANIFEST_ID)
|
||||
{
|
||||
manifestId = GetSteam3DepotManifest(depotId, appId, branch);
|
||||
manifestId = await GetSteam3DepotManifest(depotId, appId, branch);
|
||||
if (manifestId == INVALID_MANIFEST_ID && !string.Equals(branch, DEFAULT_BRANCH, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine("Warning: Depot {0} does not have branch named \"{1}\". Trying {2} branch.", depotId, branch, DEFAULT_BRANCH);
|
||||
branch = DEFAULT_BRANCH;
|
||||
manifestId = GetSteam3DepotManifest(depotId, appId, branch);
|
||||
manifestId = await GetSteam3DepotManifest(depotId, appId, branch);
|
||||
}
|
||||
|
||||
if (manifestId == INVALID_MANIFEST_ID)
|
||||
@@ -570,7 +630,7 @@ namespace DepotDownloader
|
||||
}
|
||||
}
|
||||
|
||||
steam3.RequestDepotKey(depotId, appId);
|
||||
await steam3.RequestDepotKey(depotId, appId);
|
||||
if (!steam3.DepotKeys.TryGetValue(depotId, out var depotKey))
|
||||
{
|
||||
Console.WriteLine("No valid depot key for {0}, unable to download.", depotId);
|
||||
@@ -585,13 +645,25 @@ namespace DepotDownloader
|
||||
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 ProtoManifest.ChunkData NewChunk { get; } = newChunk;
|
||||
public DepotManifest.ChunkData OldChunk { get; } = oldChunk;
|
||||
public DepotManifest.ChunkData NewChunk { get; } = newChunk;
|
||||
}
|
||||
|
||||
private class DepotFilesData
|
||||
@@ -599,9 +671,9 @@ namespace DepotDownloader
|
||||
public DepotDownloadInfo depotDownloadInfo;
|
||||
public DepotDownloadCounter depotCounter;
|
||||
public string stagingDir;
|
||||
public ProtoManifest manifest;
|
||||
public ProtoManifest previousManifest;
|
||||
public List<ProtoManifest.FileData> filteredFiles;
|
||||
public DepotManifest manifest;
|
||||
public DepotManifest previousManifest;
|
||||
public List<DepotManifest.FileData> filteredFiles;
|
||||
public HashSet<string> allFileNames;
|
||||
}
|
||||
|
||||
@@ -631,9 +703,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>();
|
||||
@@ -684,8 +756,8 @@ namespace DepotDownloader
|
||||
|
||||
Console.WriteLine("Processing depot {0}", depot.DepotId);
|
||||
|
||||
ProtoManifest oldProtoManifest = null;
|
||||
ProtoManifest newProtoManifest = null;
|
||||
DepotManifest oldManifest = null;
|
||||
DepotManifest newManifest = null;
|
||||
var configDir = Path.Combine(depot.InstallDir, CONFIG_DIR);
|
||||
|
||||
var lastManifestId = INVALID_MANIFEST_ID;
|
||||
@@ -697,72 +769,28 @@ namespace DepotDownloader
|
||||
|
||||
if (lastManifestId != INVALID_MANIFEST_ID)
|
||||
{
|
||||
var oldManifestFileName = Path.Combine(configDir, string.Format("{0}_{1}.bin", depot.DepotId, lastManifestId));
|
||||
|
||||
if (File.Exists(oldManifestFileName))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
// We only have to show this warning if the old manifest ID was different
|
||||
var badHashWarning = (lastManifestId != depot.ManifestId);
|
||||
oldManifest = Util.LoadManifestFromFile(configDir, depot.DepotId, lastManifestId, badHashWarning);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newManifestFileName = Path.Combine(configDir, string.Format("{0}_{1}.bin", depot.DepotId, depot.ManifestId));
|
||||
if (newManifestFileName != null)
|
||||
{
|
||||
byte[] expectedChecksum;
|
||||
newManifest = Util.LoadManifestFromFile(configDir, depot.DepotId, depot.ManifestId, true);
|
||||
|
||||
try
|
||||
{
|
||||
expectedChecksum = File.ReadAllBytes(newManifestFileName + ".sha");
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
expectedChecksum = null;
|
||||
}
|
||||
|
||||
newProtoManifest = ProtoManifest.LoadFromFile(newManifestFileName, out var currentChecksum);
|
||||
|
||||
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)
|
||||
if (newManifest != null)
|
||||
{
|
||||
Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.DepotId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write("Downloading depot manifest... ");
|
||||
Console.WriteLine($"Downloading depot {depot.DepotId} manifest");
|
||||
|
||||
DepotManifest depotManifest = null;
|
||||
ulong manifestRequestCode = 0;
|
||||
var manifestRequestCodeExpiration = DateTime.MinValue;
|
||||
|
||||
@@ -774,7 +802,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))
|
||||
@@ -800,7 +828,6 @@ namespace DepotDownloader
|
||||
// If we could not get the manifest code, this is a fatal error
|
||||
if (manifestRequestCode == 0)
|
||||
{
|
||||
Console.WriteLine("No manifest request code was returned for {0} {1}", depot.DepotId, depot.ManifestId);
|
||||
cts.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -810,7 +837,7 @@ namespace DepotDownloader
|
||||
depot.ManifestId,
|
||||
connection,
|
||||
cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy");
|
||||
depotManifest = await cdnPool.CDNClient.DownloadManifestAsync(
|
||||
newManifest = await cdnPool.CDNClient.DownloadManifestAsync(
|
||||
depot.DepotId,
|
||||
depot.ManifestId,
|
||||
manifestRequestCode,
|
||||
@@ -862,9 +889,9 @@ namespace DepotDownloader
|
||||
cdnPool.ReturnBrokenConnection(connection);
|
||||
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);
|
||||
cts.Cancel();
|
||||
@@ -873,28 +900,21 @@ namespace DepotDownloader
|
||||
// Throw the cancellation exception if requested so that this task is marked failed
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
newProtoManifest = new ProtoManifest(depotManifest, depot.ManifestId);
|
||||
newProtoManifest.SaveToFile(newManifestFileName, out var checksum);
|
||||
File.WriteAllBytes(newManifestFileName + ".sha", checksum);
|
||||
|
||||
Console.WriteLine(" Done!");
|
||||
Util.SaveManifestToFile(configDir, newManifest);
|
||||
}
|
||||
}
|
||||
|
||||
newProtoManifest.Files.Sort((x, y) => string.Compare(x.FileName, y.FileName, StringComparison.Ordinal));
|
||||
|
||||
Console.WriteLine("Manifest {0} ({1})", depot.ManifestId, newProtoManifest.CreationTime);
|
||||
Console.WriteLine("Manifest {0} ({1})", depot.ManifestId, newManifest.CreationTime);
|
||||
|
||||
if (Config.DownloadManifestOnly)
|
||||
{
|
||||
DumpManifestToTextFile(depot, newProtoManifest);
|
||||
DumpManifestToTextFile(depot, newManifest);
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Pre-process
|
||||
@@ -926,8 +946,8 @@ namespace DepotDownloader
|
||||
depotDownloadInfo = depot,
|
||||
depotCounter = depotCounter,
|
||||
stagingDir = stagingDir,
|
||||
manifest = newProtoManifest,
|
||||
previousManifest = oldProtoManifest,
|
||||
manifest = newManifest,
|
||||
previousManifest = oldManifest,
|
||||
filteredFiles = filesAfterExclusions,
|
||||
allFileNames = allFileNames
|
||||
};
|
||||
@@ -942,20 +962,27 @@ namespace DepotDownloader
|
||||
Console.WriteLine("Downloading depot {0}", depot.DepotId);
|
||||
|
||||
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(
|
||||
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)
|
||||
@@ -996,8 +1023,8 @@ namespace DepotDownloader
|
||||
CancellationTokenSource cts,
|
||||
GlobalDownloadCounter downloadCounter,
|
||||
DepotFilesData depotFilesData,
|
||||
ProtoManifest.FileData file,
|
||||
ConcurrentQueue<(FileStreamData, ProtoManifest.FileData, ProtoManifest.ChunkData)> networkChunkQueue)
|
||||
DepotManifest.FileData file,
|
||||
ConcurrentQueue<(FileStreamData, DepotManifest.FileData, DepotManifest.ChunkData)> networkChunkQueue)
|
||||
{
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -1005,7 +1032,7 @@ namespace DepotDownloader
|
||||
var stagingDir = depotFilesData.stagingDir;
|
||||
var depotDownloadCounter = depotFilesData.depotCounter;
|
||||
var oldProtoManifest = depotFilesData.previousManifest;
|
||||
ProtoManifest.FileData oldManifestFile = null;
|
||||
DepotManifest.FileData oldManifestFile = null;
|
||||
if (oldProtoManifest != null)
|
||||
{
|
||||
oldManifestFile = oldProtoManifest.Files.SingleOrDefault(f => f.FileName == file.FileName);
|
||||
@@ -1020,7 +1047,7 @@ namespace DepotDownloader
|
||||
File.Delete(fileStagingPath);
|
||||
}
|
||||
|
||||
List<ProtoManifest.ChunkData> neededChunks;
|
||||
List<DepotManifest.ChunkData> neededChunks;
|
||||
var fi = new FileInfo(fileFinalPath);
|
||||
var fileDidExist = fi.Exists;
|
||||
if (!fileDidExist)
|
||||
@@ -1038,7 +1065,7 @@ namespace DepotDownloader
|
||||
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
|
||||
{
|
||||
@@ -1082,7 +1109,7 @@ namespace DepotDownloader
|
||||
fsOld.Seek((long)match.OldChunk.Offset, SeekOrigin.Begin);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1114,7 +1141,7 @@ namespace DepotDownloader
|
||||
fsOld.Seek((long)match.OldChunk.Offset, SeekOrigin.Begin);
|
||||
|
||||
var tmp = new byte[match.OldChunk.UncompressedLength];
|
||||
fsOld.Read(tmp, 0, tmp.Length);
|
||||
fsOld.ReadExactly(tmp);
|
||||
|
||||
fs.Seek((long)match.NewChunk.Offset, SeekOrigin.Begin);
|
||||
fs.Write(tmp, 0, tmp.Length);
|
||||
@@ -1201,9 +1228,9 @@ namespace DepotDownloader
|
||||
CancellationTokenSource cts,
|
||||
GlobalDownloadCounter downloadCounter,
|
||||
DepotFilesData depotFilesData,
|
||||
ProtoManifest.FileData file,
|
||||
DepotManifest.FileData file,
|
||||
FileStreamData fileStreamData,
|
||||
ProtoManifest.ChunkData chunk)
|
||||
DepotManifest.ChunkData chunk)
|
||||
{
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -1212,17 +1239,8 @@ namespace DepotDownloader
|
||||
|
||||
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 chunkBuffer = ArrayPool<byte>.Shared.Rent((int)data.UncompressedLength);
|
||||
var chunkBuffer = ArrayPool<byte>.Shared.Rent((int)chunk.UncompressedLength);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1234,7 +1252,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))
|
||||
@@ -1246,7 +1264,7 @@ namespace DepotDownloader
|
||||
DebugLog.WriteLine("ContentDownloader", "Downloading chunk {0} from {1} with {2}", chunkID, connection, cdnPool.ProxyServer != null ? cdnPool.ProxyServer : "no proxy");
|
||||
written = await cdnPool.CDNClient.DownloadDepotChunkAsync(
|
||||
depot.DepotId,
|
||||
data,
|
||||
chunk,
|
||||
connection,
|
||||
chunkBuffer,
|
||||
depot.DepotKey,
|
||||
@@ -1260,6 +1278,7 @@ namespace DepotDownloader
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Console.WriteLine("Connection timeout downloading chunk {0}", chunkID);
|
||||
cdnPool.ReturnBrokenConnection(connection);
|
||||
}
|
||||
catch (SteamKitWebRequestException e)
|
||||
{
|
||||
@@ -1315,7 +1334,7 @@ namespace DepotDownloader
|
||||
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);
|
||||
}
|
||||
finally
|
||||
@@ -1359,44 +1378,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");
|
||||
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($"Manifest ID / date : {depot.ManifestId} / {manifest.CreationTime}");
|
||||
sw.WriteLine($"Manifest ID / date : {depot.ManifestId} / {manifest.CreationTime} ");
|
||||
|
||||
int numFiles = 0, numChunks = 0;
|
||||
ulong uncompressedSize = 0, compressedSize = 0;
|
||||
var uniqueChunks = new HashSet<byte[]>(new ChunkIdComparer());
|
||||
|
||||
foreach (var file in manifest.Files)
|
||||
{
|
||||
if (file.Flags.HasFlag(EDepotFileFlag.Directory))
|
||||
continue;
|
||||
|
||||
numFiles++;
|
||||
numChunks += file.Chunks.Count;
|
||||
|
||||
foreach (var chunk in file.Chunks)
|
||||
{
|
||||
uncompressedSize += chunk.UncompressedLength;
|
||||
compressedSize += chunk.CompressedLength;
|
||||
uniqueChunks.Add(chunk.ChunkID);
|
||||
}
|
||||
}
|
||||
|
||||
sw.WriteLine($"Total number of files : {numFiles}");
|
||||
sw.WriteLine($"Total number of chunks : {numChunks}");
|
||||
sw.WriteLine($"Total bytes on disk : {uncompressedSize}");
|
||||
sw.WriteLine($"Total bytes compressed : {compressedSize}");
|
||||
sw.WriteLine($"Total number of files : {manifest.Files.Count} ");
|
||||
sw.WriteLine($"Total number of chunks : {uniqueChunks.Count} ");
|
||||
sw.WriteLine($"Total bytes on disk : {manifest.TotalUncompressedSize} ");
|
||||
sw.WriteLine($"Total bytes compressed : {manifest.TotalCompressedSize} ");
|
||||
sw.WriteLine();
|
||||
sw.WriteLine();
|
||||
sw.WriteLine(" Size Chunks File SHA Flags Name");
|
||||
|
||||
foreach (var file in manifest.Files)
|
||||
{
|
||||
var sha1Hash = BitConverter.ToString(file.FileHash).Replace("-", "");
|
||||
sw.WriteLine($"{file.TotalSize,14} {file.Chunks.Count,6} {sha1Hash} {file.Flags,5:D} {file.FileName}");
|
||||
var sha1Hash = Convert.ToHexString(file.FileHash).ToLower();
|
||||
sw.WriteLine($"{file.TotalSize,14:d} {file.Chunks.Count,6:d} {sha1Hash} {(int)file.Flags,5:x} {file.FileName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
<Version>2.7.3</Version>
|
||||
<Version>3.4.0</Version>
|
||||
<Description>Steam Downloading Utility</Description>
|
||||
<Authors>SteamRE Team</Authors>
|
||||
<Copyright>Copyright © SteamRE Team 2024</Copyright>
|
||||
<Copyright>Copyright © SteamRE Team 2025</Copyright>
|
||||
<ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon>
|
||||
<Deterministic>true</Deterministic>
|
||||
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -20,12 +21,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.363">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="protobuf-net" Version="3.2.30" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="SteamKit2" Version="3.0.0-Beta.4" />
|
||||
<PackageReference Include="protobuf-net" Version="3.4.30" />
|
||||
<PackageReference Include="QRCoder" Version="1.8.1" />
|
||||
<PackageReference Include="SteamKit2" Version="3.4.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -10,6 +10,7 @@ namespace DepotDownloader
|
||||
{
|
||||
public int CellID { get; set; }
|
||||
public bool DownloadAllPlatforms { get; set; }
|
||||
public bool DownloadAllArchs { get; set; }
|
||||
public bool DownloadAllLanguages { get; set; }
|
||||
public bool DownloadManifestOnly { get; set; }
|
||||
public string InstallDirectory { get; set; }
|
||||
@@ -22,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; }
|
||||
@@ -31,5 +31,6 @@ namespace DepotDownloader
|
||||
public uint? LoginID { get; set; }
|
||||
|
||||
public bool UseQrCode { get; set; }
|
||||
public bool SkipAppConfirmation { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
GetConsoleMode
|
||||
GetConsoleProcessList
|
||||
GetStdHandle
|
||||
MessageBox
|
||||
SetConsoleMode
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
+118
-9
@@ -11,11 +11,14 @@ using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using SteamKit2;
|
||||
using SteamKit2.CDN;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
class Program
|
||||
{
|
||||
private static bool[] consumedArgs;
|
||||
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
@@ -46,6 +49,8 @@ namespace DepotDownloader
|
||||
return 0;
|
||||
}
|
||||
|
||||
consumedArgs = new bool[args.Length];
|
||||
|
||||
if (HasParameter(args, "-debug"))
|
||||
{
|
||||
PrintVersion(true);
|
||||
@@ -63,6 +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 && !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");
|
||||
|
||||
@@ -117,9 +137,24 @@ 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"))
|
||||
{
|
||||
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.MaxServers = Math.Max(ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads);
|
||||
ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null;
|
||||
|
||||
#endregion
|
||||
@@ -137,6 +172,8 @@ namespace DepotDownloader
|
||||
{
|
||||
#region Pubfile Downloading
|
||||
|
||||
PrintUnconsumedArgs(args);
|
||||
|
||||
if (InitializeSteam(username, password))
|
||||
{
|
||||
try
|
||||
@@ -172,6 +209,8 @@ namespace DepotDownloader
|
||||
{
|
||||
#region UGC Downloading
|
||||
|
||||
PrintUnconsumedArgs(args);
|
||||
|
||||
if (InitializeSteam(username, password))
|
||||
{
|
||||
try
|
||||
@@ -208,9 +247,16 @@ 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");
|
||||
|
||||
var os = GetParameter<string>(args, "-os");
|
||||
|
||||
if (ContentDownloader.Config.DownloadAllPlatforms && !string.IsNullOrEmpty(os))
|
||||
@@ -219,8 +265,16 @@ namespace DepotDownloader
|
||||
return 1;
|
||||
}
|
||||
|
||||
ContentDownloader.Config.DownloadAllArchs = HasParameter(args, "-all-archs");
|
||||
|
||||
var arch = GetParameter<string>(args, "-osarch");
|
||||
|
||||
if (ContentDownloader.Config.DownloadAllArchs && !string.IsNullOrEmpty(arch))
|
||||
{
|
||||
Console.WriteLine("Error: Cannot specify -osarch when -all-archs is specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages");
|
||||
var language = GetParameter<string>(args, "-language");
|
||||
|
||||
@@ -253,6 +307,8 @@ namespace DepotDownloader
|
||||
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID)));
|
||||
}
|
||||
|
||||
PrintUnconsumedArgs(args);
|
||||
|
||||
if (InitializeSteam(username, password))
|
||||
{
|
||||
try
|
||||
@@ -294,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);
|
||||
@@ -316,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);
|
||||
}
|
||||
|
||||
@@ -324,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;
|
||||
@@ -347,6 +426,7 @@ namespace DepotDownloader
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (converter != null)
|
||||
{
|
||||
consumedArgs[index + 1] = true;
|
||||
return (T)converter.ConvertFromString(strParam);
|
||||
}
|
||||
|
||||
@@ -372,6 +452,7 @@ namespace DepotDownloader
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (converter != null)
|
||||
{
|
||||
consumedArgs[index] = true;
|
||||
list.Add((T)converter.ConvertFromString(strParam));
|
||||
}
|
||||
|
||||
@@ -381,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
|
||||
@@ -398,9 +499,10 @@ 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)");
|
||||
Console.WriteLine(" -osarch <arch> - the architecture for which to download the game (32 or 64, default: the host's architecture)");
|
||||
Console.WriteLine(" -all-languages - download all language-specific depots when -app is used.");
|
||||
@@ -412,17 +514,24 @@ namespace DepotDownloader
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -username <user> - the username 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. (Use -username <username> -remember-password as login credentials)");
|
||||
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> - a list of files to download (from the manifest). Prefix file path with 'regex:' if you want to match with regex.");
|
||||
Console.WriteLine(" -validate - Include checksum verification of files already downloaded");
|
||||
Console.WriteLine(" -filelist <file.txt> - the name of a local file that contains a list of files to download (from the manifest).");
|
||||
Console.WriteLine(" prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.");
|
||||
Console.WriteLine();
|
||||
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)
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ProtoBuf;
|
||||
using SteamKit2;
|
||||
|
||||
@@ -157,5 +158,39 @@ namespace DepotDownloader
|
||||
using var ds = new DeflateStream(fs, CompressionMode.Compress);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
-184
@@ -39,12 +39,11 @@ namespace DepotDownloader
|
||||
public SteamContent steamContent;
|
||||
readonly SteamApps steamApps;
|
||||
readonly SteamCloud steamCloud;
|
||||
readonly SteamUnifiedMessages.UnifiedService<IPublishedFile> steamPublishedFile;
|
||||
readonly PublishedFile steamPublishedFile;
|
||||
|
||||
readonly CallbackManager callbacks;
|
||||
|
||||
readonly bool authenticatedUser;
|
||||
bool bConnected;
|
||||
bool bConnecting;
|
||||
bool bAborted;
|
||||
bool bExpectingDisconnectRemote;
|
||||
@@ -52,15 +51,12 @@ namespace DepotDownloader
|
||||
bool bIsConnectionRecovery;
|
||||
int connectionBackoff;
|
||||
int seq; // more hack fixes
|
||||
DateTime connectTime;
|
||||
AuthSession authSession;
|
||||
readonly CancellationTokenSource abortedToken = new();
|
||||
|
||||
// input
|
||||
readonly SteamUser.LogOnDetails logonDetails;
|
||||
|
||||
static readonly TimeSpan STEAM3_TIMEOUT = TimeSpan.FromSeconds(30);
|
||||
|
||||
|
||||
public Steam3Session(SteamUser.LogOnDetails details)
|
||||
{
|
||||
this.logonDetails = details;
|
||||
@@ -68,7 +64,7 @@ namespace DepotDownloader
|
||||
|
||||
var clientConfiguration = SteamConfiguration.Create(config =>
|
||||
config
|
||||
.WithHttpClientFactory(HttpClientFactory.CreateHttpClient)
|
||||
.WithHttpClientFactory(static purpose => HttpClientFactory.CreateHttpClient())
|
||||
);
|
||||
|
||||
this.steamClient = new SteamClient(clientConfiguration);
|
||||
@@ -77,7 +73,7 @@ namespace DepotDownloader
|
||||
this.steamApps = this.steamClient.GetHandler<SteamApps>();
|
||||
this.steamCloud = this.steamClient.GetHandler<SteamCloud>();
|
||||
var steamUnifiedMessages = this.steamClient.GetHandler<SteamUnifiedMessages>();
|
||||
this.steamPublishedFile = steamUnifiedMessages.CreateService<IPublishedFile>();
|
||||
this.steamPublishedFile = steamUnifiedMessages.CreateService<PublishedFile>();
|
||||
this.steamContent = this.steamClient.GetHandler<SteamContent>();
|
||||
|
||||
this.callbacks = new CallbackManager(this.steamClient);
|
||||
@@ -93,7 +89,7 @@ namespace DepotDownloader
|
||||
|
||||
public delegate bool WaitCondition();
|
||||
|
||||
private readonly object steamLock = new();
|
||||
private readonly Lock steamLock = new();
|
||||
|
||||
public bool WaitUntilCallback(Action submitter, WaitCondition waiter)
|
||||
{
|
||||
@@ -109,7 +105,7 @@ namespace DepotDownloader
|
||||
{
|
||||
lock (steamLock)
|
||||
{
|
||||
WaitForCallbacks();
|
||||
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
} while (!bAborted && this.seq == seq && !waiter());
|
||||
}
|
||||
@@ -127,36 +123,51 @@ namespace DepotDownloader
|
||||
return IsLoggedOn;
|
||||
}
|
||||
|
||||
public void RequestAppInfo(uint appId, bool bForce = false)
|
||||
public async Task TickCallbacks()
|
||||
{
|
||||
var token = abortedToken.Token;
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
await callbacks.RunWaitCallbackAsync(token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RequestAppInfo(uint appId, bool bForce = false)
|
||||
{
|
||||
if ((AppInfo.ContainsKey(appId) && !bForce) || bAborted)
|
||||
return;
|
||||
|
||||
var completed = false;
|
||||
Action<SteamApps.PICSTokensCallback> cbMethodTokens = appTokens =>
|
||||
var appTokens = await steamApps.PICSGetAccessTokens([appId], []);
|
||||
|
||||
if (appTokens.AppTokensDenied.Contains(appId))
|
||||
{
|
||||
completed = true;
|
||||
if (appTokens.AppTokensDenied.Contains(appId))
|
||||
{
|
||||
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
|
||||
}
|
||||
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
|
||||
}
|
||||
|
||||
foreach (var token_dict in appTokens.AppTokens)
|
||||
{
|
||||
this.AppTokens[token_dict.Key] = token_dict.Value;
|
||||
}
|
||||
};
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
foreach (var token_dict in appTokens.AppTokens)
|
||||
{
|
||||
callbacks.Subscribe(steamApps.PICSGetAccessTokens(new List<uint> { appId }, new List<uint>()), cbMethodTokens);
|
||||
}, () => { return completed; });
|
||||
this.AppTokens[token_dict.Key] = token_dict.Value;
|
||||
}
|
||||
|
||||
completed = false;
|
||||
Action<SteamApps.PICSProductInfoCallback> cbMethod = appInfo =>
|
||||
var request = new SteamApps.PICSRequest(appId);
|
||||
|
||||
if (AppTokens.TryGetValue(appId, out var token))
|
||||
{
|
||||
completed = !appInfo.ResponsePending;
|
||||
request.AccessToken = token;
|
||||
}
|
||||
|
||||
var appInfoMultiple = await steamApps.PICSGetProductInfo([request], []);
|
||||
|
||||
foreach (var appInfo in appInfoMultiple.Results)
|
||||
{
|
||||
foreach (var app_value in appInfo.Apps)
|
||||
{
|
||||
var app = app_value.Value;
|
||||
@@ -169,45 +180,17 @@ namespace DepotDownloader
|
||||
{
|
||||
AppInfo[app] = null;
|
||||
}
|
||||
};
|
||||
|
||||
var request = new SteamApps.PICSRequest(appId);
|
||||
if (AppTokens.TryGetValue(appId, out var token))
|
||||
{
|
||||
request.AccessToken = token;
|
||||
}
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
{
|
||||
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest> { request }, new List<SteamApps.PICSRequest>()), cbMethod);
|
||||
}, () => { return completed; });
|
||||
}
|
||||
|
||||
public void RequestPackageInfo(IEnumerable<uint> packageIds)
|
||||
public async Task RequestPackageInfo(IEnumerable<uint> packageIds)
|
||||
{
|
||||
var packages = packageIds.ToList();
|
||||
packages.RemoveAll(pid => PackageInfo.ContainsKey(pid));
|
||||
packages.RemoveAll(PackageInfo.ContainsKey);
|
||||
|
||||
if (packages.Count == 0 || bAborted)
|
||||
return;
|
||||
|
||||
var completed = false;
|
||||
Action<SteamApps.PICSProductInfoCallback> cbMethod = packageInfo =>
|
||||
{
|
||||
completed = !packageInfo.ResponsePending;
|
||||
|
||||
foreach (var package_value in packageInfo.Packages)
|
||||
{
|
||||
var package = package_value.Value;
|
||||
PackageInfo[package.ID] = package;
|
||||
}
|
||||
|
||||
foreach (var package in packageInfo.UnknownPackages)
|
||||
{
|
||||
PackageInfo[package] = null;
|
||||
}
|
||||
};
|
||||
|
||||
var packageRequests = new List<SteamApps.PICSRequest>();
|
||||
|
||||
foreach (var package in packages)
|
||||
@@ -222,55 +205,53 @@ namespace DepotDownloader
|
||||
packageRequests.Add(request);
|
||||
}
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
var packageInfoMultiple = await steamApps.PICSGetProductInfo([], packageRequests);
|
||||
|
||||
foreach (var packageInfo in packageInfoMultiple.Results)
|
||||
{
|
||||
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest>(), packageRequests), cbMethod);
|
||||
}, () => { return completed; });
|
||||
foreach (var package_value in packageInfo.Packages)
|
||||
{
|
||||
var package = package_value.Value;
|
||||
PackageInfo[package.ID] = package;
|
||||
}
|
||||
|
||||
foreach (var package in packageInfo.UnknownPackages)
|
||||
{
|
||||
PackageInfo[package] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RequestFreeAppLicense(uint appId)
|
||||
public async Task<bool> RequestFreeAppLicense(uint appId)
|
||||
{
|
||||
var success = false;
|
||||
var completed = false;
|
||||
Action<SteamApps.FreeLicenseCallback> cbMethod = resultInfo =>
|
||||
try
|
||||
{
|
||||
completed = true;
|
||||
success = resultInfo.GrantedApps.Contains(appId);
|
||||
};
|
||||
var resultInfo = await steamApps.RequestFreeLicense(appId);
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
return resultInfo.GrantedApps.Contains(appId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
callbacks.Subscribe(steamApps.RequestFreeLicense(appId), cbMethod);
|
||||
}, () => { return completed; });
|
||||
|
||||
return success;
|
||||
Console.WriteLine($"Failed to request FreeOnDemand license for app {appId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestDepotKey(uint depotId, uint appid = 0)
|
||||
public async Task RequestDepotKey(uint depotId, uint appid = 0)
|
||||
{
|
||||
if (DepotKeys.ContainsKey(depotId) || bAborted)
|
||||
return;
|
||||
|
||||
var completed = false;
|
||||
var depotKey = await steamApps.GetDepotDecryptionKey(depotId, appid);
|
||||
|
||||
Action<SteamApps.DepotKeyCallback> cbMethod = depotKey =>
|
||||
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
|
||||
|
||||
if (depotKey.Result != EResult.OK)
|
||||
{
|
||||
completed = true;
|
||||
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (depotKey.Result != EResult.OK)
|
||||
{
|
||||
Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
|
||||
};
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
{
|
||||
callbacks.Subscribe(steamApps.GetDepotDecryptionKey(depotId, appid), cbMethod);
|
||||
}, () => { return completed; });
|
||||
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,9 +262,19 @@ namespace DepotDownloader
|
||||
|
||||
var requestCode = await steamContent.GetManifestRequestCode(depotId, appId, manifestId, branch);
|
||||
|
||||
Console.WriteLine("Got manifest request code for {0} {1} result: {2}",
|
||||
depotId, manifestId,
|
||||
requestCode);
|
||||
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
|
||||
{
|
||||
Console.WriteLine($"Got manifest request code for depot {depotId} from app {appId}, manifest {manifestId}, result: {requestCode}");
|
||||
}
|
||||
|
||||
return requestCode;
|
||||
}
|
||||
@@ -312,86 +303,68 @@ namespace DepotDownloader
|
||||
completion.TrySetResult(cdnAuth);
|
||||
}
|
||||
|
||||
public void CheckAppBetaPassword(uint appid, string password)
|
||||
public async Task CheckAppBetaPassword(uint appid, string password)
|
||||
{
|
||||
var completed = false;
|
||||
Action<SteamApps.CheckAppBetaPasswordCallback> cbMethod = appPassword =>
|
||||
var appPassword = await steamApps.CheckAppBetaPassword(appid, password);
|
||||
|
||||
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
|
||||
|
||||
foreach (var entry in appPassword.BetaPasswords)
|
||||
{
|
||||
completed = true;
|
||||
|
||||
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
|
||||
|
||||
foreach (var entry in appPassword.BetaPasswords)
|
||||
{
|
||||
AppBetaPasswords[entry.Key] = entry.Value;
|
||||
}
|
||||
};
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
{
|
||||
callbacks.Subscribe(steamApps.CheckAppBetaPassword(appid, password), cbMethod);
|
||||
}, () => { return completed; });
|
||||
AppBetaPasswords[entry.Key] = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public PublishedFileDetails GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
|
||||
public async Task<KeyValue> GetPrivateBetaDepotSection(uint appid, string branch)
|
||||
{
|
||||
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
|
||||
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,
|
||||
includechildren = true,
|
||||
};
|
||||
pubFileRequest.publishedfileids.Add(pubFile);
|
||||
|
||||
var completed = false;
|
||||
PublishedFileDetails details = null;
|
||||
var details = await steamPublishedFile.GetDetails(pubFileRequest);
|
||||
|
||||
Action<SteamUnifiedMessages.ServiceMethodResponse> cbMethod = callback =>
|
||||
if (details.Result == EResult.OK)
|
||||
{
|
||||
completed = true;
|
||||
if (callback.Result == EResult.OK)
|
||||
{
|
||||
var response = callback.GetDeserializedResponse<CPublishedFile_GetDetails_Response>();
|
||||
details = response.publishedfiledetails.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving file details for pubfile {pubFile}.");
|
||||
}
|
||||
};
|
||||
return details.Body.publishedfiledetails.FirstOrDefault();
|
||||
}
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
{
|
||||
callbacks.Subscribe(steamPublishedFile.SendMessage(api => api.GetDetails(pubFileRequest)), cbMethod);
|
||||
}, () => { return completed; });
|
||||
|
||||
return details;
|
||||
throw new Exception($"EResult {(int)details.Result} ({details.Result}) while retrieving file details for pubfile {pubFile}.");
|
||||
}
|
||||
|
||||
|
||||
public SteamCloud.UGCDetailsCallback GetUGCDetails(UGCHandle ugcHandle)
|
||||
public async Task<SteamCloud.UGCDetailsCallback> GetUGCDetails(UGCHandle ugcHandle)
|
||||
{
|
||||
var completed = false;
|
||||
SteamCloud.UGCDetailsCallback details = null;
|
||||
var callback = await steamCloud.RequestUGCDetails(ugcHandle);
|
||||
|
||||
Action<SteamCloud.UGCDetailsCallback> cbMethod = callback =>
|
||||
if (callback.Result == EResult.OK)
|
||||
{
|
||||
completed = true;
|
||||
if (callback.Result == EResult.OK)
|
||||
{
|
||||
details = callback;
|
||||
}
|
||||
else if (callback.Result == EResult.FileNotFound)
|
||||
{
|
||||
details = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
|
||||
}
|
||||
};
|
||||
|
||||
WaitUntilCallback(() =>
|
||||
return callback;
|
||||
}
|
||||
else if (callback.Result == EResult.FileNotFound)
|
||||
{
|
||||
callbacks.Subscribe(steamCloud.RequestUGCDetails(ugcHandle), cbMethod);
|
||||
}, () => { return completed; });
|
||||
return null;
|
||||
}
|
||||
|
||||
return details;
|
||||
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
|
||||
}
|
||||
|
||||
private void ResetConnectionFlags()
|
||||
@@ -404,14 +377,11 @@ namespace DepotDownloader
|
||||
void Connect()
|
||||
{
|
||||
bAborted = false;
|
||||
bConnected = false;
|
||||
bConnecting = true;
|
||||
connectionBackoff = 0;
|
||||
authSession = null;
|
||||
|
||||
ResetConnectionFlags();
|
||||
|
||||
this.connectTime = DateTime.Now;
|
||||
this.steamClient.Connect();
|
||||
}
|
||||
|
||||
@@ -428,9 +398,9 @@ namespace DepotDownloader
|
||||
}
|
||||
|
||||
bAborted = true;
|
||||
bConnected = false;
|
||||
bConnecting = false;
|
||||
bIsConnectionRecovery = false;
|
||||
abortedToken.Cancel();
|
||||
steamClient.Disconnect();
|
||||
|
||||
Ansi.Progress(Ansi.ProgressState.Hidden);
|
||||
@@ -448,28 +418,13 @@ namespace DepotDownloader
|
||||
steamClient.Disconnect();
|
||||
}
|
||||
|
||||
private void WaitForCallbacks()
|
||||
{
|
||||
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
|
||||
|
||||
var diff = DateTime.Now - connectTime;
|
||||
|
||||
if (diff > STEAM3_TIMEOUT && !bConnected)
|
||||
{
|
||||
Console.WriteLine("Timeout connecting to Steam3.");
|
||||
Abort();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ConnectedCallback(SteamClient.ConnectedCallback connected)
|
||||
{
|
||||
Console.WriteLine(" Done!");
|
||||
bConnecting = false;
|
||||
bConnected = true;
|
||||
|
||||
// Update our tracking so that we don't time out, even if we need to reconnect multiple times,
|
||||
// e.g. if the authentication phase takes a while and therefore multiple connections.
|
||||
connectTime = DateTime.Now;
|
||||
connectionBackoff = 0;
|
||||
|
||||
if (!authenticatedUser)
|
||||
@@ -491,13 +446,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)
|
||||
@@ -519,8 +475,8 @@ namespace DepotDownloader
|
||||
{
|
||||
var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails
|
||||
{
|
||||
DeviceFriendlyName = nameof(DepotDownloader),
|
||||
IsPersistentSession = ContentDownloader.Config.RememberPassword,
|
||||
Authenticator = new UserConsoleAuthenticator(),
|
||||
});
|
||||
|
||||
authSession = session;
|
||||
@@ -563,11 +519,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();
|
||||
}
|
||||
@@ -610,16 +572,18 @@ namespace DepotDownloader
|
||||
}
|
||||
else if (!bAborted)
|
||||
{
|
||||
connectionBackoff += 1;
|
||||
|
||||
if (bConnecting)
|
||||
{
|
||||
Console.WriteLine("Connection to Steam failed. Trying again");
|
||||
Console.WriteLine($"Connection to Steam failed. Trying again (#{connectionBackoff})...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Lost connection to Steam. Reconnecting");
|
||||
}
|
||||
|
||||
Thread.Sleep(1000 * ++connectionBackoff);
|
||||
Thread.Sleep(1000 * connectionBackoff);
|
||||
|
||||
// Any connection related flags need to be reset here to match the state after Connect
|
||||
ResetConnectionFlags();
|
||||
@@ -746,10 +710,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
-35
@@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SteamKit2;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
@@ -78,16 +79,16 @@ namespace DepotDownloader
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
fs.Seek((long)data.Offset, SeekOrigin.Begin);
|
||||
|
||||
var adler = AdlerHash(fs, (int)data.UncompressedLength);
|
||||
if (!adler.SequenceEqual(data.Checksum))
|
||||
if (!adler.SequenceEqual(BitConverter.GetBytes(data.Checksum)))
|
||||
{
|
||||
neededChunks.Add(data);
|
||||
}
|
||||
@@ -110,6 +111,100 @@ namespace DepotDownloader
|
||||
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)
|
||||
{
|
||||
if (hex == null)
|
||||
@@ -140,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,33 +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-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>` | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex.
|
||||
`-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.
|
||||
`-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
|
||||
|
||||
@@ -99,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?
|
||||
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.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"version": "9.0.100",
|
||||
"rollForward": "latestMinor"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user