Compare commits

..

No commits in common. 'master' and 'DepotDownloader_2.4.1' have entirely different histories.

@ -1,222 +1,7 @@
# top-most EditorConfig file ; EditorConfig: http://EditorConfig.org
root = true root = true
[*] [*]
indent_style = space indent_style = space
charset = utf-8 indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Code files
[*.{cs, csx, vb, vbx}]
indent_size = 4
# Github yaml files
[*.yml]
indent_size = 2
# XML project files
[*.{csproj, vbproj, vcxproj, vcxproj.filters, proj, projitems, shproj}]
indent_size = 2
# XML config files
[*.{props, targets, ruleset, config, nuspec, resx, vsixmanifest, vsct}]
indent_size = 2
# Dotnet code style settings:
[*.{cs, vb}]
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Suggest more modern language features when available
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
# Non-private static fields are PascalCase
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
# Non-private readonly fields are PascalCase
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
# Constants are PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_style.constant_style.capitalization = pascal_case
# Static readonly fields are PascalCase
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static, readonly
dotnet_naming_style.static_field_style.capitalization = pascal_case
# Instance fields are camelCase and start with _
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
dotnet_naming_style.camel_case_style.capitalization = camel_case
# Local functions are PascalCase
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_style.local_function_style.capitalization = pascal_case
# By default, name items with PascalCase
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
dotnet_diagnostic.RS2008.severity = none
# IDE0007: Use `var` instead of explicit type
dotnet_diagnostic.IDE0007.severity = warning
# IDE0035: Remove unreachable code
dotnet_diagnostic.IDE0035.severity = warning
# IDE0036: Order modifiers
dotnet_diagnostic.IDE0036.severity = warning
# IDE0043: Format string contains invalid placeholder
dotnet_diagnostic.IDE0043.severity = warning
# IDE0044: Make field readonly
dotnet_diagnostic.IDE0044.severity = warning
# CSharp code style settings:
[*.cs]
# Require file header OR A source file contains a header that does not match the required text
file_header_template = This file is subject to the terms and conditions defined\nin file 'LICENSE', which is part of this source code package.
dotnet_diagnostic.IDE0073.severity = error
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
# csharp_new_line_before_members_in_object_initializers = true TODO seems like Rider/ReSharper has the value inverted, uncomment when its fixed
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Blocks are allowed
csharp_prefer_braces = true:silent
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true

4
.gitattributes vendored

@ -1,4 +0,0 @@
*.cs text eol=lf
*.csproj text eol=lf
*.config eol=lf
*.json eol=lf

@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-should-have-happened
attributes:
label: What did you expect to happen?
placeholder: I expected that...
validations:
required: true
- type: textarea
id: what-actually-happened
attributes:
label: Instead of that, what actually happened?
placeholder: ... but instead, what happened was...
validations:
required: true
- type: dropdown
id: operating-system
attributes:
label: Which operating system are you running on?
options:
- Linux
- macOS
- Windows
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of DepotDownloader are using?
validations:
required: true
- type: input
id: command
attributes:
label: Command
description: Specify the full command you used (except for username and password)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Run with `-debug` parameter to get additional output.
render: shell
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Is there anything else that you think we should know?
validations:
required: false

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/SteamRE/DepotDownloader/discussions/new
about: Please ask and answer questions here.

@ -1,36 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks, we appreciate good ideas!
- type: textarea
id: problem-area
attributes:
label: What problem is this feature trying to solve?
placeholder: I'm really frustrated when...
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: How would you like it to be solved?
placeholder: I think that it could be solved by...
validations:
required: true
- type: textarea
id: alternative-solutions
attributes:
label: Have you considered any alternative solutions
placeholder: I did think that that it also could be solved by ..., but...
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Is there anything else that you think we should know?
validations:
required: false

@ -3,13 +3,5 @@ updates:
- package-ecosystem: nuget - package-ecosystem: nuget
directory: "/" directory: "/"
schedule: schedule:
interval: weekly interval: daily
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]

@ -15,156 +15,23 @@ on:
jobs: jobs:
build: build:
name: .NET on ${{ matrix.runs-on }} (${{ matrix.configuration }}) name: .NET on ${{ matrix.os }} (${{ matrix.configuration }})
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
runs-on: [macos-latest, macos-14, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
configuration: [Release, Debug] configuration: [Release, Debug]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v1
- name: Build - name: Build
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true run: dotnet publish -c ${{ matrix.configuration }} -o artifacts
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest' if: matrix.configuration == 'Release'
with: with:
name: DepotDownloader-framework name: DepotDownloader-${{ runner.os }}
path: artifacts path: artifacts
if-no-files-found: error if-no-files-found: error
- name: Publish Windows-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime win-x64 --output selfcontained-win-x64
- name: Publish Windows-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime win-arm64 --output selfcontained-win-arm64
- name: Publish Linux-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-x64 --output selfcontained-linux-x64
- name: Publish Linux-arm
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-arm --output selfcontained-linux-arm
- name: Publish Linux-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-arm64 --output selfcontained-linux-arm64
- name: Publish macOS-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime osx-x64 --output selfcontained-osx-x64
- name: Publish macOS-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-14'
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
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
with:
name: DepotDownloader-windows-x64
path: selfcontained-win-x64
if-no-files-found: error
- name: Upload Windows-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
with:
name: DepotDownloader-windows-arm64
path: selfcontained-win-arm64
if-no-files-found: error
- name: Upload Linux-x64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-x64
path: selfcontained-linux-x64
if-no-files-found: error
- name: Upload Linux-arm
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-arm
path: selfcontained-linux-arm
if-no-files-found: error
- name: Upload Linux-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-arm64
path: selfcontained-linux-arm64
if-no-files-found: error
- name: Upload macOS-x64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-latest'
with:
name: DepotDownloader-macos-x64
path: selfcontained-osx-x64
if-no-files-found: error
- name: Upload macOS-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-14'
with:
name: DepotDownloader-macos-arm64
path: selfcontained-osx-arm64
if-no-files-found: error
release:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display artifacts folder structure
run: ls -Rl
working-directory: artifacts
- name: Create release files
run: |
set -eux
mkdir release
chmod +x artifacts/DepotDownloader-linux-x64/DepotDownloader
chmod +x artifacts/DepotDownloader-linux-arm/DepotDownloader
chmod +x artifacts/DepotDownloader-linux-arm64/DepotDownloader
chmod +x artifacts/DepotDownloader-macos-x64/DepotDownloader
chmod +x artifacts/DepotDownloader-macos-arm64/DepotDownloader
zip -9j release/DepotDownloader-framework.zip artifacts/DepotDownloader-framework/*
zip -9j release/DepotDownloader-windows-x64.zip artifacts/DepotDownloader-windows-x64/*
zip -9j release/DepotDownloader-windows-arm64.zip artifacts/DepotDownloader-windows-arm64/*
zip -9j release/DepotDownloader-linux-x64.zip artifacts/DepotDownloader-linux-x64/*
zip -9j release/DepotDownloader-linux-arm.zip artifacts/DepotDownloader-linux-arm/*
zip -9j release/DepotDownloader-linux-arm64.zip artifacts/DepotDownloader-linux-arm64/*
zip -9j release/DepotDownloader-macos-x64.zip artifacts/DepotDownloader-macos-x64/*
zip -9j release/DepotDownloader-macos-arm64.zip artifacts/DepotDownloader-macos-arm64/*
- name: Display structure of archived files
run: ls -Rl
working-directory: release
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: true
files: release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -1,39 +0,0 @@
name: SteamKit2 Continuous Integration
on:
schedule:
- cron: '0 1 * * SUN'
workflow_dispatch:
jobs:
build:
name: .NET on ${{ matrix.runs-on }} (${{ matrix.configuration }})
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
runs-on: [ macos-latest, macos-14, ubuntu-latest, windows-latest ]
configuration: [ Release, Debug ]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v5
- name: Setup .NET Core
uses: actions/setup-dotnet@v5
- name: Configure NuGet
run: |
dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/SteamRE/index.json"
dotnet add DepotDownloader/DepotDownloader.csproj package SteamKit2 --prerelease
- name: Build
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
- name: Upload artifact
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release'
with:
name: DepotDownloader-${{ matrix.runs-on }}
path: artifacts
if-no-files-found: error

@ -1,33 +0,0 @@
name: WinGet submission on release
on:
workflow_dispatch:
release:
types: [published]
jobs:
winget:
name: Publish winget package
runs-on: windows-latest
steps:
- name: Submit package to Windows Package Manager Community Repository
run: |
$wingetPackage = "SteamRE.DepotDownloader"
$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
$zipX64Url = $assets | Where-Object -Property name -match 'DepotDownloader-windows-x64.zip' | Select -ExpandProperty browser_download_url
$zipArm64Url = $assets | Where-Object -Property name -match 'DepotDownloader-windows-arm64.zip' | Select -ExpandProperty browser_download_url
$ver = $targetRelease.tag_name -ireplace '^(DepotDownloader[ _])?v?'
# getting latest wingetcreate file
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
# how to create a token: https://github.com/microsoft/winget-create?tab=readme-ov-file#github-personal-access-token-classic-permissions
.\wingetcreate.exe update $wingetPackage --submit --version $ver --urls "$zipX64Url" "$zipArm64Url" --token "${{ secrets.PT_WINGET }}"

5
.gitignore vendored

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

@ -1,39 +1,34 @@
// This file is subject to the terms and conditions defined using System;
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using ProtoBuf;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.IO.IsolatedStorage; using System.IO.IsolatedStorage;
using ProtoBuf; using System.Linq;
using SteamKit2;
using SteamKit2.Discovery;
namespace DepotDownloader namespace DepotDownloader
{ {
[ProtoContract] [ProtoContract]
class AccountSettingsStore class AccountSettingsStore
{ {
// Member 1 was a Dictionary<string, byte[]> for SentryData. [ProtoMember(1, IsRequired=false)]
public Dictionary<string, byte[]> SentryData { get; private set; }
[ProtoMember(2, IsRequired = false)] [ProtoMember(2, IsRequired = false)]
public ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; } public System.Collections.Concurrent.ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; }
// Member 3 was a Dictionary<string, string> for LoginKeys.
[ProtoMember(4, IsRequired = false)] [ProtoMember(3, IsRequired = false)]
public Dictionary<string, string> LoginTokens { get; private set; } public Dictionary<string, string> LoginKeys { get; private set; }
[ProtoMember(5, IsRequired = false)] string FileName = null;
public Dictionary<string, string> GuardData { get; private set; }
string FileName;
AccountSettingsStore() AccountSettingsStore()
{ {
ContentServerPenalty = new ConcurrentDictionary<string, int>(); SentryData = new Dictionary<string, byte[]>();
LoginTokens = new(StringComparer.OrdinalIgnoreCase); ContentServerPenalty = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
GuardData = new(StringComparer.OrdinalIgnoreCase); LoginKeys = new Dictionary<string, string>();
} }
static bool Loaded static bool Loaded
@ -41,7 +36,7 @@ namespace DepotDownloader
get { return Instance != null; } get { return Instance != null; }
} }
public static AccountSettingsStore Instance; public static AccountSettingsStore Instance = null;
static readonly IsolatedStorageFile IsolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly(); static readonly IsolatedStorageFile IsolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly();
public static void LoadFromFile(string filename) public static void LoadFromFile(string filename)
@ -53,9 +48,11 @@ namespace DepotDownloader
{ {
try try
{ {
using var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read); using (var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read))
using var ds = new DeflateStream(fs, CompressionMode.Decompress); using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
Instance = Serializer.Deserialize<AccountSettingsStore>(ds); {
Instance = ProtoBuf.Serializer.Deserialize<AccountSettingsStore>(ds);
}
} }
catch (IOException ex) catch (IOException ex)
{ {
@ -78,9 +75,11 @@ namespace DepotDownloader
try try
{ {
using var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write); using (var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write))
using var ds = new DeflateStream(fs, CompressionMode.Compress); using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
Serializer.Serialize(ds, Instance); {
ProtoBuf.Serializer.Serialize<AccountSettingsStore>(ds, Instance);
}
} }
catch (IOException ex) catch (IOException ex)
{ {

@ -1,59 +0,0 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using Spectre.Console;
namespace DepotDownloader;
static class Ansi
{
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
// https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
public enum ProgressState
{
Hidden = 0,
Default = 1,
Error = 2,
Indeterminate = 3,
Warning = 4,
}
const char ESC = (char)0x1B;
const char BEL = (char)0x07;
private static bool useProgress;
public static void Init()
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
{
return;
}
if (OperatingSystem.IsLinux())
{
return;
}
var (supportsAnsi, legacyConsole) = AnsiDetector.Detect(stdError: false, upgrade: true);
useProgress = supportsAnsi && !legacyConsole;
}
public static void Progress(ulong downloaded, ulong total)
{
var progress = (byte)MathF.Round(downloaded / (float)total * 100.0f);
Progress(ProgressState.Default, progress);
}
public static void Progress(ProgressState state, byte progress = 0)
{
if (!useProgress)
{
return;
}
Console.Write($"{ESC}]9;4;{(byte)state};{progress}{BEL}");
}
}

@ -1,117 +0,0 @@
// Copied from https://github.com/spectreconsole/spectre.console/blob/d79e6adc5f8e637fb35c88f987023ffda6707243/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
// MIT License - Copyright(c) 2020 Patrik Svensson, Phil Scott, Nils Andresen
// which is partially based on https://github.com/keqingrong/supports-ansi/blob/master/index.js
// <auto-generated/>
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Windows.Win32;
using Windows.Win32.System.Console;
namespace Spectre.Console;
internal static class AnsiDetector
{
private static readonly Regex[] _regexes =
[
new("^xterm"), // xterm, PuTTY, Mintty
new("^rxvt"), // RXVT
new("^eterm"), // Eterm
new("^screen"), // GNU screen, tmux
new("tmux"), // tmux
new("^vt100"), // DEC VT series
new("^vt102"), // DEC VT series
new("^vt220"), // DEC VT series
new("^vt320"), // DEC VT series
new("ansi"), // ANSI
new("scoansi"), // SCO ANSI
new("cygwin"), // Cygwin, MinGW
new("linux"), // Linux console
new("konsole"), // Konsole
new("bvterm"), // Bitvise SSH Client
new("^st-256color"), // Suckless Simple Terminal, st
new("alacritty"), // Alacritty
];
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
{
// Running on Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Running under ConEmu?
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
{
return (true, false);
}
var supportsAnsi = WindowsSupportsAnsi(upgrade, stdError, out var legacyConsole);
return (supportsAnsi, legacyConsole);
}
return DetectFromTerm();
}
private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
{
// Check if the terminal is of type ANSI/VT100/xterm compatible.
var term = Environment.GetEnvironmentVariable("TERM");
if (!string.IsNullOrWhiteSpace(term))
{
if (_regexes.Any(regex => regex.IsMatch(term)))
{
return (true, false);
}
}
return (false, true);
}
private static bool WindowsSupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
{
isLegacy = false;
try
{
var @out = PInvoke.GetStdHandle_SafeHandle(stdError ? STD_HANDLE.STD_ERROR_HANDLE :STD_HANDLE.STD_OUTPUT_HANDLE);
if (!PInvoke.GetConsoleMode(@out, out var mode))
{
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
return ansiFromTerm;
}
if ((mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0||true)
{
isLegacy = true;
if (!upgrade)
{
return false;
}
// Try enable ANSI support.
mode |= CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING | CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN;
if (!PInvoke.SetConsoleMode(@out, mode))
{
// Enabling failed.
return false;
}
isLegacy = false;
}
return true;
}
catch
{
// All we know here is that we don't support ANSI.
return false;
}
}
}

@ -1,11 +1,11 @@
// This file is subject to the terms and conditions defined using SteamKit2;
// in file 'LICENSE', which is part of this source code package.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SteamKit2.CDN;
namespace DepotDownloader namespace DepotDownloader
{ {
@ -14,80 +14,175 @@ namespace DepotDownloader
/// </summary> /// </summary>
class CDNClientPool class CDNClientPool
{ {
private const int ServerEndpointMinimumSize = 8;
private readonly Steam3Session steamSession; private readonly Steam3Session steamSession;
private readonly uint appId; private readonly uint appId;
public Client CDNClient { get; } public CDNClient CDNClient { get; }
public Server ProxyServer { get; private set; } public CDNClient.Server ProxyServer { get; private set; }
private readonly ConcurrentStack<CDNClient.Server> activeConnectionPool;
private readonly BlockingCollection<CDNClient.Server> availableServerEndpoints;
private readonly List<Server> servers = []; private readonly AutoResetEvent populatePoolEvent;
private int nextServer; private readonly Task monitorTask;
private readonly CancellationTokenSource shutdownToken;
public CancellationTokenSource ExhaustedToken { get; set; }
public CDNClientPool(Steam3Session steamSession, uint appId) public CDNClientPool(Steam3Session steamSession, uint appId)
{ {
this.steamSession = steamSession; this.steamSession = steamSession;
this.appId = appId; this.appId = appId;
CDNClient = new Client(steamSession.steamClient); CDNClient = new CDNClient(steamSession.steamClient);
activeConnectionPool = new ConcurrentStack<CDNClient.Server>();
availableServerEndpoints = new BlockingCollection<CDNClient.Server>();
populatePoolEvent = new AutoResetEvent(true);
shutdownToken = new CancellationTokenSource();
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync).Unwrap();
} }
public async Task UpdateServerList() public void Shutdown()
{ {
var servers = await this.steamSession.steamContent.GetServersForSteamPipe(); shutdownToken.Cancel();
monitorTask.Wait();
}
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault(); private async Task<IReadOnlyCollection<CDNClient.Server>> FetchBootstrapServerListAsync()
{
var backoffDelay = 0;
var weightedCdnServers = servers while (!shutdownToken.IsCancellationRequested)
.Where(server => {
try
{ {
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId); var cdnServers = await ContentServerDirectoryService.LoadAsync(this.steamSession.steamClient.Configuration, ContentDownloader.Config.CellID, shutdownToken.Token);
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN"); if (cdnServers != null)
}) {
.Select(server => return cdnServers;
}
}
catch (Exception ex)
{ {
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty); Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
if (ex is SteamKitWebRequestException e && e.StatusCode == (HttpStatusCode)429)
{
// If we're being throttled, add a delay to the next request
backoffDelay = Math.Min(5, ++backoffDelay);
await Task.Delay(TimeSpan.FromSeconds(backoffDelay));
}
}
}
return (server, penalty); return null;
}) }
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
private async Task ConnectionPoolMonitorAsync()
{
bool didPopulate = false;
foreach (var (server, weight) in weightedCdnServers) while (!shutdownToken.IsCancellationRequested)
{ {
for (var i = 0; i < server.NumEntries; i++) 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 == null || 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)
{ {
this.servers.Add(server); ExhaustedToken?.Cancel();
return;
} }
} }
}
private CDNClient.Server BuildConnection(CancellationToken token)
{
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
{
populatePoolEvent.Set();
}
return availableServerEndpoints.Take(token);
}
if (this.servers.Count == 0) public CDNClient.Server GetConnection(CancellationToken token)
{
if (!activeConnectionPool.TryPop(out var connection))
{ {
throw new Exception("Failed to retrieve any download servers."); connection = BuildConnection(token);
} }
return connection;
} }
public Server GetConnection() public async Task<string> AuthenticateConnection(uint appId, uint depotId, CDNClient.Server server)
{ {
return servers[nextServer % servers.Count]; var host = steamSession.ResolveCDNTopLevelHost(server.Host);
var cdnKey = $"{depotId:D}:{host}";
steamSession.RequestCDNAuthToken(appId, depotId, host, cdnKey);
if (steamSession.CDNAuthTokens.TryGetValue(cdnKey, out var authTokenCallbackPromise))
{
var result = await authTokenCallbackPromise.Task;
return result.Token;
}
else
{
throw new Exception($"Failed to retrieve CDN token for server {server.Host} depot {depotId}");
}
} }
public void ReturnConnection(Server server) public void ReturnConnection(CDNClient.Server server)
{ {
if (server == null) return; if (server == null) return;
// nothing to do, maybe remove from ContentServerPenalty? activeConnectionPool.Push(server);
} }
public void ReturnBrokenConnection(Server server) public void ReturnBrokenConnection(CDNClient.Server server)
{ {
if (server == null) return; if (server == null) return;
lock (servers) // Broken connections are not returned to the pool
{
if (servers[nextServer % servers.Count] == server)
{
nextServer++;
// TODO: Add server to ContentServerPenalty
}
}
} }
} }
} }

@ -1,76 +0,0 @@
// 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);
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,11 +1,8 @@
// This file is subject to the terms and conditions defined using System;
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using ProtoBuf;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using ProtoBuf;
namespace DepotDownloader namespace DepotDownloader
{ {
@ -15,11 +12,11 @@ namespace DepotDownloader
[ProtoMember(1)] [ProtoMember(1)]
public Dictionary<uint, ulong> InstalledManifestIDs { get; private set; } public Dictionary<uint, ulong> InstalledManifestIDs { get; private set; }
string FileName; string FileName = null;
DepotConfigStore() DepotConfigStore()
{ {
InstalledManifestIDs = []; InstalledManifestIDs = new Dictionary<uint, ulong>();
} }
static bool Loaded static bool Loaded
@ -27,7 +24,7 @@ namespace DepotDownloader
get { return Instance != null; } get { return Instance != null; }
} }
public static DepotConfigStore Instance; public static DepotConfigStore Instance = null;
public static void LoadFromFile(string filename) public static void LoadFromFile(string filename)
{ {
@ -36,9 +33,9 @@ namespace DepotDownloader
if (File.Exists(filename)) if (File.Exists(filename))
{ {
using var fs = File.Open(filename, FileMode.Open); using (FileStream fs = File.Open(filename, FileMode.Open))
using var ds = new DeflateStream(fs, CompressionMode.Decompress); using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
Instance = Serializer.Deserialize<DepotConfigStore>(ds); Instance = ProtoBuf.Serializer.Deserialize<DepotConfigStore>(ds);
} }
else else
{ {
@ -53,9 +50,9 @@ namespace DepotDownloader
if (!Loaded) if (!Loaded)
throw new Exception("Saved config before loading"); throw new Exception("Saved config before loading");
using var fs = File.Open(Instance.FileName, FileMode.Create); using (FileStream fs = File.Open(Instance.FileName, FileMode.Create))
using var ds = new DeflateStream(fs, CompressionMode.Compress); using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
Serializer.Serialize(ds, Instance); ProtoBuf.Serializer.Serialize<DepotConfigStore>(ds, Instance);
} }
} }
} }

@ -1,32 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RollForward>LatestMajor</RollForward>
<Version>3.4.0</Version>
<Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2025</Copyright>
<ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon>
<Deterministic>true</Deterministic>
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\LICENSE" Link="LICENSE"> <PackageReference Include="protobuf-net" Version="3.0.101" />
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <PackageReference Include="SteamKit2" Version="2.4.0-Alpha.2" />
</None>
</ItemGroup> </ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="protobuf-net" Version="3.2.52" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="SteamKit2" Version="3.3.0" />
</ItemGroup>
</Project>

@ -1,36 +1,31 @@
// This file is subject to the terms and conditions defined using System.Collections.Generic;
// in file 'LICENSE', which is part of this source code package. using System.Text.RegularExpressions;
using System.Collections.Generic; namespace DepotDownloader
using System.Text.RegularExpressions; {
class DownloadConfig
namespace DepotDownloader {
{ public int CellID { get; set; }
class DownloadConfig public bool DownloadAllPlatforms { get; set; }
{ public bool DownloadAllLanguages { get; set; }
public int CellID { get; set; } public bool DownloadManifestOnly { get; set; }
public bool DownloadAllPlatforms { get; set; } public string InstallDirectory { get; set; }
public bool DownloadAllArchs { get; set; }
public bool DownloadAllLanguages { get; set; } public bool UsingFileList { get; set; }
public bool DownloadManifestOnly { get; set; } public HashSet<string> FilesToDownload { get; set; }
public string InstallDirectory { get; set; } public List<Regex> FilesToDownloadRegex { get; set; }
public bool UsingFileList { get; set; } public string BetaPassword { get; set; }
public HashSet<string> FilesToDownload { get; set; }
public List<Regex> FilesToDownloadRegex { get; set; } public bool VerifyAll { get; set; }
public string BetaPassword { get; set; } public int MaxServers { get; set; }
public int MaxDownloads { get; set; }
public bool VerifyAll { get; set; }
public string SuppliedPassword { get; set; }
public int MaxDownloads { get; set; } public bool RememberPassword { get; set; }
public bool RememberPassword { get; set; } // A Steam LoginID to allow multiple concurrent connections
public uint? LoginID {get; set; }
// A Steam LoginID to allow multiple concurrent connections }
public uint? LoginID { get; set; } }
public bool UseQrCode { get; set; }
public bool SkipAppConfirmation { get; set; }
}
}

@ -1,52 +0,0 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace DepotDownloader
{
// This is based on the dotnet issue #44686 and its workaround at https://github.com/dotnet/runtime/issues/44686#issuecomment-733797994
// We don't know if the IPv6 stack is functional.
class HttpClientFactory
{
public static HttpClient CreateHttpClient()
{
var client = new HttpClient(new SocketsHttpHandler
{
ConnectCallback = IPv4ConnectAsync
});
var assemblyVersion = typeof(HttpClientFactory).Assembly.GetName().Version.ToString(fieldCount: 3);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DepotDownloader", assemblyVersion));
return client;
}
static async ValueTask<Stream> IPv4ConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// By default, we create dual-mode sockets:
// Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}
}

@ -1,45 +0,0 @@
// 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.Diagnostics.Tracing;
using System.Text;
namespace DepotDownloader
{
internal sealed class HttpDiagnosticEventListener : EventListener
{
public const EventKeywords TasksFlowActivityIds = (EventKeywords)0x80;
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == "System.Net.Http" ||
eventSource.Name == "System.Net.Sockets" ||
eventSource.Name == "System.Net.Security" ||
eventSource.Name == "System.Net.NameResolution")
{
EnableEvents(eventSource, EventLevel.LogAlways);
}
else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
EnableEvents(eventSource, EventLevel.LogAlways, TasksFlowActivityIds);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
var sb = new StringBuilder().Append($"{eventData.TimeStamp:HH:mm:ss.fffffff} {eventData.EventSource.Name}.{eventData.EventName}(");
for (var i = 0; i < eventData.Payload?.Count; i++)
{
sb.Append(eventData.PayloadNames?[i]).Append(": ").Append(eventData.Payload[i]);
if (i < eventData.Payload?.Count - 1)
{
sb.Append(", ");
}
}
sb.Append(')');
Console.WriteLine(sb.ToString());
}
}
}

@ -1,5 +0,0 @@
GetConsoleMode
GetConsoleProcessList
GetStdHandle
MessageBox
SetConsoleMode

@ -1,51 +0,0 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace DepotDownloader
{
static class PlatformUtilities
{
public static void SetExecutable(string path, bool value)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
const UnixFileMode ModeExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
var mode = File.GetUnixFileMode(path);
var hasExecuteMask = (mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
File.SetUnixFileMode(path, value
? mode | ModeExecute
: mode & ~ModeExecute);
}
}
[SupportedOSPlatform("windows5.0")]
public static void VerifyConsoleLaunch()
{
// Reference: https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922
var processList = new uint[2];
var processCount = Windows.Win32.PInvoke.GetConsoleProcessList(processList);
if (processCount != 1)
{
return;
}
_ = Windows.Win32.PInvoke.MessageBox(
Windows.Win32.Foundation.HWND.Null,
"Depot Downloader is a console application; there is no GUI.\n\nIf you do not pass any command line parameters, it prints usage info and exits.\n\nYou must use this from a terminal/console.",
"Depot Downloader",
Windows.Win32.UI.WindowsAndMessaging.MESSAGEBOX_STYLE.MB_OK | Windows.Win32.UI.WindowsAndMessaging.MESSAGEBOX_STYLE.MB_ICONWARNING
);
}
}
}

@ -1,550 +1,396 @@
// This file is subject to the terms and conditions defined using System;
// in file 'LICENSE', which is part of this source code package. using System.Collections.Generic;
using System.IO;
using System; using System.Text.RegularExpressions;
using System.Collections.Generic; using SteamKit2;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.Threading.Tasks;
using System.Linq; using System.Runtime.InteropServices;
using System.Reflection; using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions; namespace DepotDownloader
using System.Threading.Tasks; {
using SteamKit2; class Program
using SteamKit2.CDN; {
static int Main( string[] args )
namespace DepotDownloader => MainAsync( args ).GetAwaiter().GetResult();
{
class Program static async Task<int> MainAsync( string[] args )
{ {
private static bool[] consumedArgs; if ( args.Length == 0 )
{
static async Task<int> Main(string[] args) PrintUsage();
{ return 1;
if (args.Length == 0) }
{
PrintVersion(); DebugLog.Enabled = false;
PrintUsage();
AccountSettingsStore.LoadFromFile( "account.config" );
if (OperatingSystem.IsWindowsVersionAtLeast(5, 0))
{ #region Common Options
PlatformUtilities.VerifyConsoleLaunch();
} if ( HasParameter( args, "-debug" ) )
{
return 0; DebugLog.Enabled = true;
} DebugLog.AddListener( ( category, message ) =>
{
Ansi.Init(); Console.WriteLine( "[{0}] {1}", category, message );
});
DebugLog.Enabled = false; }
AccountSettingsStore.LoadFromFile("account.config"); string username = GetParameter<string>( args, "-username" ) ?? GetParameter<string>( args, "-user" );
string password = GetParameter<string>( args, "-password" ) ?? GetParameter<string>( args, "-pass" );
#region Common Options ContentDownloader.Config.RememberPassword = HasParameter( args, "-remember-password" );
// Not using HasParameter because it is case insensitive ContentDownloader.Config.DownloadManifestOnly = HasParameter( args, "-manifest-only" );
if (args.Length == 1 && (args[0] == "-V" || args[0] == "--version"))
{ int cellId = GetParameter<int>( args, "-cellid", -1 );
PrintVersion(true); if ( cellId == -1 )
return 0; {
} cellId = 0;
}
consumedArgs = new bool[args.Length];
ContentDownloader.Config.CellID = cellId;
if (HasParameter(args, "-debug"))
{ string fileList = GetParameter<string>( args, "-filelist" );
PrintVersion(true);
if ( fileList != null )
DebugLog.Enabled = true; {
DebugLog.AddListener((category, message) => try
{ {
Console.WriteLine("[{0}] {1}", category, message); string fileListData = await File.ReadAllTextAsync( fileList );
}); var files = fileListData.Split( new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries );
var httpEventListener = new HttpDiagnosticEventListener(); ContentDownloader.Config.UsingFileList = true;
} ContentDownloader.Config.FilesToDownload = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
ContentDownloader.Config.FilesToDownloadRegex = new List<Regex>();
var username = GetParameter<string>(args, "-username") ?? GetParameter<string>(args, "-user");
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass"); foreach ( var fileEntry in files )
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password"); {
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr"); if ( fileEntry.StartsWith( "regex:" ) )
ContentDownloader.Config.SkipAppConfirmation = HasParameter(args, "-no-mobile"); {
Regex rgx = new Regex( fileEntry.Substring( 6 ), RegexOptions.Compiled | RegexOptions.IgnoreCase );
if (username == null) ContentDownloader.Config.FilesToDownloadRegex.Add( rgx );
{ }
if (ContentDownloader.Config.RememberPassword && !ContentDownloader.Config.UseQrCode) else
{ {
Console.WriteLine("Error: -remember-password can not be used without -username or -qr."); ContentDownloader.Config.FilesToDownload.Add( fileEntry.Replace( '\\', '/' ) );
return 1; }
} }
}
else if (ContentDownloader.Config.UseQrCode) Console.WriteLine( "Using filelist: '{0}'.", fileList );
{ }
Console.WriteLine("Error: -qr can not be used with -username."); catch (Exception ex)
return 1; {
} Console.WriteLine( "Warning: Unable to load filelist: {0}", ex.ToString() );
}
ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only"); }
var cellId = GetParameter(args, "-cellid", -1); ContentDownloader.Config.InstallDirectory = GetParameter<string>( args, "-dir" );
if (cellId == -1)
{ ContentDownloader.Config.VerifyAll = HasParameter( args, "-verify-all" ) || HasParameter( args, "-verify_all" ) || HasParameter( args, "-validate" );
cellId = 0; ContentDownloader.Config.MaxServers = GetParameter<int>( args, "-max-servers", 20 );
} ContentDownloader.Config.MaxDownloads = GetParameter<int>( args, "-max-downloads", 8 );
ContentDownloader.Config.MaxServers = Math.Max( ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads );
ContentDownloader.Config.CellID = cellId; ContentDownloader.Config.LoginID = HasParameter( args, "-loginid" ) ? (uint?)GetParameter<uint>( args, "-loginid" ) : null;
var fileList = GetParameter<string>(args, "-filelist"); #endregion
if (fileList != null) uint appId = GetParameter<uint>( args, "-app", ContentDownloader.INVALID_APP_ID );
{ if ( appId == ContentDownloader.INVALID_APP_ID )
const string RegexPrefix = "regex:"; {
Console.WriteLine( "Error: -app not specified!" );
try return 1;
{ }
ContentDownloader.Config.UsingFileList = true;
ContentDownloader.Config.FilesToDownload = new HashSet<string>(StringComparer.OrdinalIgnoreCase); ulong pubFile = GetParameter<ulong>( args, "-pubfile", ContentDownloader.INVALID_MANIFEST_ID );
ContentDownloader.Config.FilesToDownloadRegex = []; ulong ugcId = GetParameter<ulong>( args, "-ugc", ContentDownloader.INVALID_MANIFEST_ID );
if ( pubFile != ContentDownloader.INVALID_MANIFEST_ID )
var files = await File.ReadAllLinesAsync(fileList); {
#region Pubfile Downloading
foreach (var fileEntry in files)
{ if ( InitializeSteam( username, password ) )
if (string.IsNullOrWhiteSpace(fileEntry)) {
{ try
continue; {
} await ContentDownloader.DownloadPubfileAsync( appId, pubFile ).ConfigureAwait( false );
}
if (fileEntry.StartsWith(RegexPrefix)) catch ( Exception ex ) when (
{ ex is ContentDownloaderException
var rgx = new Regex(fileEntry[RegexPrefix.Length..], RegexOptions.Compiled | RegexOptions.IgnoreCase); || ex is OperationCanceledException )
ContentDownloader.Config.FilesToDownloadRegex.Add(rgx); {
} Console.WriteLine( ex.Message );
else return 1;
{ }
ContentDownloader.Config.FilesToDownload.Add(fileEntry.Replace('\\', '/')); catch ( Exception e )
} {
} Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
throw;
Console.WriteLine("Using filelist: '{0}'.", fileList); }
} finally
catch (Exception ex) {
{ ContentDownloader.ShutdownSteam3();
Console.WriteLine("Warning: Unable to load filelist: {0}", ex); }
} }
} else
{
ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir"); Console.WriteLine( "Error: InitializeSteam failed" );
return 1;
ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate"); }
if (HasParameter(args, "-use-lancache")) #endregion
{ }
await Client.DetectLancacheServerAsync(); else if ( ugcId != ContentDownloader.INVALID_MANIFEST_ID )
if (Client.UseLancacheServer) {
{ #region UGC Downloading
Console.WriteLine("Detected Lancache server! Downloads will be directed through the Lancache.");
if ( InitializeSteam( username, password ) )
// 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. try
if (!HasParameter(args, "-max-downloads")) {
{ await ContentDownloader.DownloadUGCAsync( appId, ugcId ).ConfigureAwait( false );
ContentDownloader.Config.MaxDownloads = 25; }
} catch ( Exception ex ) when (
} ex is ContentDownloaderException
} || ex is OperationCanceledException )
{
ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8); Console.WriteLine( ex.Message );
ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null; return 1;
}
#endregion catch ( Exception e )
{
var appId = GetParameter(args, "-app", ContentDownloader.INVALID_APP_ID); Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
if (appId == ContentDownloader.INVALID_APP_ID) throw;
{ }
Console.WriteLine("Error: -app not specified!"); finally
return 1; {
} ContentDownloader.ShutdownSteam3();
}
var pubFile = GetParameter(args, "-pubfile", ContentDownloader.INVALID_MANIFEST_ID); }
var ugcId = GetParameter(args, "-ugc", ContentDownloader.INVALID_MANIFEST_ID); else
if (pubFile != ContentDownloader.INVALID_MANIFEST_ID) {
{ Console.WriteLine( "Error: InitializeSteam failed" );
#region Pubfile Downloading return 1;
}
PrintUnconsumedArgs(args);
#endregion
if (InitializeSteam(username, password)) }
{ else
try {
{ #region App downloading
await ContentDownloader.DownloadPubfileAsync(appId, pubFile).ConfigureAwait(false);
} string branch = GetParameter<string>( args, "-branch" ) ?? GetParameter<string>( args, "-beta" ) ?? ContentDownloader.DEFAULT_BRANCH;
catch (Exception ex) when ( ContentDownloader.Config.BetaPassword = GetParameter<string>( args, "-betapassword" );
ex is ContentDownloaderException
|| ex is OperationCanceledException) ContentDownloader.Config.DownloadAllPlatforms = HasParameter( args, "-all-platforms" );
{ string os = GetParameter<string>( args, "-os", null );
Console.WriteLine(ex.Message);
return 1; if ( ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty( os ) )
} {
catch (Exception e) Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified.");
{ return 1;
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); }
throw;
} string arch = GetParameter<string>( args, "-osarch", null );
finally
{ ContentDownloader.Config.DownloadAllLanguages = HasParameter( args, "-all-languages" );
ContentDownloader.ShutdownSteam3(); string language = GetParameter<string>( args, "-language", null );
}
} if ( ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty( language ) )
else {
{ Console.WriteLine( "Error: Cannot specify -language when -all-languages is specified." );
Console.WriteLine("Error: InitializeSteam failed"); return 1;
return 1; }
}
bool lv = HasParameter( args, "-lowviolence" );
#endregion
} List<(uint, ulong)> depotManifestIds = new List<(uint, ulong)>();
else if (ugcId != ContentDownloader.INVALID_MANIFEST_ID) bool isUGC = false;
{
#region UGC Downloading List<uint> depotIdList = GetParameterList<uint>( args, "-depot" );
List<ulong> manifestIdList = GetParameterList<ulong>( args, "-manifest" );
PrintUnconsumedArgs(args); if ( manifestIdList.Count > 0 )
{
if (InitializeSteam(username, password)) if ( depotIdList.Count != manifestIdList.Count )
{ {
try Console.WriteLine( "Error: -manifest requires one id for every -depot specified" );
{ return 1;
await ContentDownloader.DownloadUGCAsync(appId, ugcId).ConfigureAwait(false); }
}
catch (Exception ex) when ( var zippedDepotManifest = depotIdList.Zip( manifestIdList, ( depotId, manifestId ) => ( depotId, manifestId ) );
ex is ContentDownloaderException depotManifestIds.AddRange( zippedDepotManifest );
|| ex is OperationCanceledException) }
{ else
Console.WriteLine(ex.Message); {
return 1; depotManifestIds.AddRange( depotIdList.Select( depotId => ( depotId, ContentDownloader.INVALID_MANIFEST_ID ) ) );
} }
catch (Exception e)
{ if ( InitializeSteam( username, password ) )
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); {
throw; try
} {
finally await ContentDownloader.DownloadAppAsync( appId, depotManifestIds, branch, os, arch, language, lv, isUGC ).ConfigureAwait( false );
{ }
ContentDownloader.ShutdownSteam3(); catch ( Exception ex ) when (
} ex is ContentDownloaderException
} || ex is OperationCanceledException )
else {
{ Console.WriteLine( ex.Message );
Console.WriteLine("Error: InitializeSteam failed"); return 1;
return 1; }
} catch ( Exception e )
{
#endregion Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
} throw;
else }
{ finally
#region App downloading {
ContentDownloader.ShutdownSteam3();
var branch = GetParameter<string>(args, "-branch") ?? GetParameter<string>(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH; }
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-branchpassword") ?? GetParameter<string>(args, "-betapassword"); }
else
if (!string.IsNullOrEmpty(ContentDownloader.Config.BetaPassword) && string.IsNullOrEmpty(branch)) {
{ Console.WriteLine( "Error: InitializeSteam failed" );
Console.WriteLine("Error: Cannot specify -branchpassword when -branch is not specified."); return 1;
return 1; }
}
#endregion
ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms"); }
var os = GetParameter<string>(args, "-os"); return 0;
}
if (ContentDownloader.Config.DownloadAllPlatforms && !string.IsNullOrEmpty(os))
{ static bool InitializeSteam( string username, string password )
Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified."); {
return 1; if ( username != null && password == null && ( !ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey( username ) ) )
} {
do
ContentDownloader.Config.DownloadAllArchs = HasParameter(args, "-all-archs"); {
Console.Write( "Enter account password for \"{0}\": ", username );
var arch = GetParameter<string>(args, "-osarch"); if ( Console.IsInputRedirected )
{
if (ContentDownloader.Config.DownloadAllArchs && !string.IsNullOrEmpty(arch)) password = Console.ReadLine();
{ }
Console.WriteLine("Error: Cannot specify -osarch when -all-archs is specified."); else
return 1; {
} // Avoid console echoing of password
password = Util.ReadPassword();
ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages"); }
var language = GetParameter<string>(args, "-language"); Console.WriteLine();
} while ( String.Empty == password );
if (ContentDownloader.Config.DownloadAllLanguages && !string.IsNullOrEmpty(language)) }
{ else if ( username == null )
Console.WriteLine("Error: Cannot specify -language when -all-languages is specified."); {
return 1; Console.WriteLine( "No username given. Using anonymous account with dedicated server subscription." );
} }
var lv = HasParameter(args, "-lowviolence"); // capture the supplied password in case we need to re-use it after checking the login key
ContentDownloader.Config.SuppliedPassword = password;
var depotManifestIds = new List<(uint, ulong)>();
var isUGC = false; return ContentDownloader.InitializeSteam3( username, password );
}
var depotIdList = GetParameterList<uint>(args, "-depot");
var manifestIdList = GetParameterList<ulong>(args, "-manifest"); static int IndexOfParam( string[] args, string param )
if (manifestIdList.Count > 0) {
{ for ( int x = 0; x < args.Length; ++x )
if (depotIdList.Count != manifestIdList.Count) {
{ if ( args[ x ].Equals( param, StringComparison.OrdinalIgnoreCase ) )
Console.WriteLine("Error: -manifest requires one id for every -depot specified"); return x;
return 1; }
} return -1;
}
var zippedDepotManifest = depotIdList.Zip(manifestIdList, (depotId, manifestId) => (depotId, manifestId)); static bool HasParameter( string[] args, string param )
depotManifestIds.AddRange(zippedDepotManifest); {
} return IndexOfParam( args, param ) > -1;
else }
{
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); static T GetParameter<T>( string[] args, string param, T defaultValue = default( T ) )
} {
int index = IndexOfParam( args, param );
PrintUnconsumedArgs(args);
if ( index == -1 || index == ( args.Length - 1 ) )
if (InitializeSteam(username, password)) return defaultValue;
{
try string strParam = args[ index + 1 ];
{
await ContentDownloader.DownloadAppAsync(appId, depotManifestIds, branch, os, arch, language, lv, isUGC).ConfigureAwait(false); var converter = TypeDescriptor.GetConverter( typeof( T ) );
} if ( converter != null )
catch (Exception ex) when ( {
ex is ContentDownloaderException return ( T )converter.ConvertFromString( strParam );
|| ex is OperationCanceledException) }
{
Console.WriteLine(ex.Message); return default( T );
return 1; }
}
catch (Exception e) static List<T> GetParameterList<T>(string[] args, string param)
{ {
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); List<T> list = new List<T>();
throw; int index = IndexOfParam(args, param);
}
finally if (index == -1 || index == (args.Length - 1))
{ return list;
ContentDownloader.ShutdownSteam3();
} index++;
}
else while (index < args.Length)
{ {
Console.WriteLine("Error: InitializeSteam failed"); string strParam = args[index];
return 1;
} if (strParam[0] == '-') break;
#endregion var converter = TypeDescriptor.GetConverter(typeof(T));
} if (converter != null)
{
return 0; list.Add((T)converter.ConvertFromString(strParam));
} }
static bool InitializeSteam(string username, string password) index++;
{ }
if (!ContentDownloader.Config.UseQrCode)
{ return list;
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))) }
{
if (AccountSettingsStore.Instance.LoginTokens.ContainsKey(username)) static void PrintUsage()
{ {
Console.WriteLine($"Account \"{username}\" has stored credentials. Did you forget to specify -remember-password?"); Console.WriteLine();
} Console.WriteLine( "Usage - downloading one or all depots for an app:" );
Console.WriteLine( "\tdepotdownloader -app <id> [-depot <id> [-manifest <id>]]" );
do Console.WriteLine( "\t\t[-username <username> [-password <password>]] [other options]" );
{ Console.WriteLine();
Console.Write("Enter account password for \"{0}\": ", username); Console.WriteLine("Usage - downloading a workshop item using pubfile id");
if (Console.IsInputRedirected) Console.WriteLine( "\tdepotdownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]" );
{ Console.WriteLine("Usage - downloading a workshop item using ugc id");
password = Console.ReadLine(); Console.WriteLine("\tdepotdownloader -app <id> -ugc <id> [-username <username> [-password <password>]]");
} Console.WriteLine();
else Console.WriteLine( "Parameters:" );
{ Console.WriteLine( "\t-app <#>\t\t\t\t- the AppID to download." );
// Avoid console echoing of password Console.WriteLine( "\t-depot <#>\t\t\t\t- the DepotID to download." );
password = Util.ReadPassword(); Console.WriteLine( "\t-manifest <id>\t\t\t- manifest id of content to download (requires -depot, default: current for branch)." );
} Console.WriteLine( "\t-beta <branchname>\t\t\t- download from specified branch if available (default: Public)." );
Console.WriteLine( "\t-betapassword <pass>\t\t- branch password if applicable." );
Console.WriteLine(); Console.WriteLine( "\t-all-platforms\t\t\t- downloads all platform-specific depots when -app is used." );
} while (string.Empty == password); Console.WriteLine( "\t-os <os>\t\t\t\t- the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)" );
} Console.WriteLine( "\t-osarch <arch>\t\t\t\t- the architecture for which to download the game (32 or 64, default: the host's architecture)" );
else if (username == null) Console.WriteLine( "\t-all-languages\t\t\t\t- download all language-specific depots when -app is used." );
{ Console.WriteLine( "\t-language <lang>\t\t\t\t- the language for which to download the game (default: english)" );
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription."); Console.WriteLine( "\t-lowviolence\t\t\t\t- download low violence depots when -app is used." );
} Console.WriteLine();
} Console.WriteLine( "\t-ugc <#>\t\t\t\t- the UGC ID to download." );
Console.WriteLine( "\t-pubfile <#>\t\t\t- the PublishedFileId to download. (Will automatically resolve to UGC id)" );
if (!string.IsNullOrEmpty(password)) Console.WriteLine();
{ Console.WriteLine( "\t-username <user>\t\t- the username of the account to login to for restricted content.");
const int MAX_PASSWORD_SIZE = 64; Console.WriteLine( "\t-password <pass>\t\t- the password of the account to login to for restricted content." );
Console.WriteLine( "\t-remember-password\t\t- if set, remember the password for subsequent logins of this user." );
if (password.Length > MAX_PASSWORD_SIZE) Console.WriteLine();
{ Console.WriteLine( "\t-dir <installdir>\t\t- the directory in which to place downloaded files." );
Console.Error.WriteLine($"Warning: Password is longer than {MAX_PASSWORD_SIZE} characters, which is not supported by Steam."); Console.WriteLine( "\t-filelist <file.txt>\t- a list of files to download (from the manifest). Prefix file path with 'regex:' if you want to match with regex." );
} Console.WriteLine( "\t-validate\t\t\t\t- Include checksum verification of files already downloaded" );
Console.WriteLine();
if (!password.All(char.IsAscii)) Console.WriteLine( "\t-manifest-only\t\t\t- downloads a human readable manifest for any depots that would be downloaded." );
{ Console.WriteLine( "\t-cellid <#>\t\t\t\t- the overridden CellID of the content server to download from." );
Console.Error.WriteLine("Warning: Password contains non-ASCII characters, which is not supported by Steam."); Console.WriteLine( "\t-max-servers <#>\t\t- maximum number of content servers to use. (default: 20)." );
} Console.WriteLine( "\t-max-downloads <#>\t\t- maximum number of chunks to download concurrently. (default: 8)." );
} Console.WriteLine( "\t-loginid <#>\t\t- a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently." );
}
return ContentDownloader.InitializeSteam3(username, password); }
} }
static int IndexOfParam(string[] args, string param)
{
for (var x = 0; x < args.Length; ++x)
{
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
{
consumedArgs[x] = true;
return x;
}
}
return -1;
}
static bool HasParameter(string[] args, string param)
{
return IndexOfParam(args, param) > -1;
}
static T GetParameter<T>(string[] args, string param, T defaultValue = default)
{
var index = IndexOfParam(args, param);
if (index == -1 || index == (args.Length - 1))
return defaultValue;
var strParam = args[index + 1];
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index + 1] = true;
return (T)converter.ConvertFromString(strParam);
}
return default;
}
static List<T> GetParameterList<T>(string[] args, string param)
{
var list = new List<T>();
var index = IndexOfParam(args, param);
if (index == -1 || index == (args.Length - 1))
return list;
index++;
while (index < args.Length)
{
var strParam = args[index];
if (strParam[0] == '-') break;
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index] = true;
list.Add((T)converter.ConvertFromString(strParam));
}
index++;
}
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
Console.WriteLine();
Console.WriteLine("Usage: downloading one or all depots for an app:");
Console.WriteLine(" depotdownloader -app <id> [-depot <id> [-manifest <id>]]");
Console.WriteLine(" [-username <username> [-password <password>]] [other options]");
Console.WriteLine();
Console.WriteLine("Usage: downloading a workshop item using pubfile id");
Console.WriteLine(" depotdownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]");
Console.WriteLine("Usage: downloading a workshop item using ugc id");
Console.WriteLine(" depotdownloader -app <id> -ugc <id> [-username <username> [-password <password>]]");
Console.WriteLine();
Console.WriteLine("Parameters:");
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($" -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.");
Console.WriteLine(" -language <lang> - the language for which to download the game (default: english)");
Console.WriteLine(" -lowviolence - download low violence depots when -app is used.");
Console.WriteLine();
Console.WriteLine(" -ugc <#> - the UGC ID to download.");
Console.WriteLine(" -pubfile <#> - the PublishedFileId to download. (Will automatically resolve to UGC id)");
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.");
Console.WriteLine(" use -username <username> -remember-password as login credentials.");
Console.WriteLine(" -qr - display a login QR code to be scanned with the Steam mobile app");
Console.WriteLine(" -no-mobile - prefer entering a 2FA code instead of prompting to accept in the Steam mobile app");
Console.WriteLine();
Console.WriteLine(" -dir <installdir> - the directory in which to place downloaded files.");
Console.WriteLine(" -filelist <file.txt> - the name of a local file that contains a list of files to download (from the manifest).");
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-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)
{
var version = typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
Console.WriteLine($"DepotDownloader v{version}");
if (!printExtra)
{
return;
}
Console.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.OSDescription}");
}
}
}

@ -0,0 +1,34 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle( "DepotDownloader" )]
[assembly: AssemblyDescription("Steam Downloading Utility")]
[assembly: AssemblyConfiguration( "" )]
[assembly: AssemblyCompany("SteamRE Team")]
[assembly: AssemblyProduct( "DepotDownloader" )]
[assembly: AssemblyCopyright("Copyright © SteamRE Team 2017")]
[assembly: AssemblyTrademark( "" )]
[assembly: AssemblyCulture( "" )]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible( false )]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid( "df2ab32a-923c-46e3-a1b4-c901ee92ec94" )]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.4.1.0")]

@ -0,0 +1,7 @@
{
"profiles": {
"DepotDownloader": {
"commandName": "Project"
}
}
}

@ -1,24 +1,20 @@
// This file is subject to the terms and conditions defined using System;
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using ProtoBuf; using ProtoBuf;
using SteamKit2; using SteamKit2;
namespace DepotDownloader namespace DepotDownloader
{ {
[ProtoContract] [ProtoContract()]
class ProtoManifest class ProtoManifest
{ {
// Proto ctor // Proto ctor
private ProtoManifest() private ProtoManifest()
{ {
Files = []; Files = new List<FileData>();
} }
public ProtoManifest(DepotManifest sourceManifest, ulong id) : this() public ProtoManifest(DepotManifest sourceManifest, ulong id) : this()
@ -28,13 +24,13 @@ namespace DepotDownloader
CreationTime = sourceManifest.CreationTime; CreationTime = sourceManifest.CreationTime;
} }
[ProtoContract] [ProtoContract()]
public class FileData public class FileData
{ {
// Proto ctor // Proto ctor
private FileData() private FileData()
{ {
Chunks = []; Chunks = new List<ChunkData>();
} }
public FileData(DepotManifest.FileData sourceData) : this() public FileData(DepotManifest.FileData sourceData) : this()
@ -80,7 +76,7 @@ namespace DepotDownloader
public ChunkData(DepotManifest.ChunkData sourceChunk) public ChunkData(DepotManifest.ChunkData sourceChunk)
{ {
ChunkID = sourceChunk.ChunkID; ChunkID = sourceChunk.ChunkID;
Checksum = BitConverter.GetBytes(sourceChunk.Checksum); Checksum = sourceChunk.Checksum;
Offset = sourceChunk.Offset; Offset = sourceChunk.Offset;
CompressedLength = sourceChunk.CompressedLength; CompressedLength = sourceChunk.CompressedLength;
UncompressedLength = sourceChunk.UncompressedLength; UncompressedLength = sourceChunk.UncompressedLength;
@ -134,63 +130,34 @@ namespace DepotDownloader
return null; return null;
} }
using var ms = new MemoryStream(); using (MemoryStream ms = new MemoryStream())
using (var fs = File.Open(filename, FileMode.Open)) {
using (var ds = new DeflateStream(fs, CompressionMode.Decompress)) using (FileStream fs = File.Open(filename, FileMode.Open))
ds.CopyTo(ms); using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
ds.CopyTo(ms);
checksum = SHA1.HashData(ms.ToArray()); checksum = Util.SHAHash(ms.ToArray());
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
return Serializer.Deserialize<ProtoManifest>(ms); return ProtoBuf.Serializer.Deserialize<ProtoManifest>(ms);
}
} }
public void SaveToFile(string filename, out byte[] checksum) public void SaveToFile(string filename, out byte[] checksum)
{ {
using var ms = new MemoryStream();
Serializer.Serialize(ms, this); using (MemoryStream ms = new MemoryStream())
checksum = SHA1.HashData(ms.ToArray());
ms.Seek(0, SeekOrigin.Begin);
using var fs = File.Open(filename, FileMode.Create);
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())); ProtoBuf.Serializer.Serialize<ProtoManifest>(ms, this);
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) checksum = Util.SHAHash(ms.ToArray());
{
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; ms.Seek(0, SeekOrigin.Begin);
compressedSize += chunk.CompressedLength;
}
newManifest.Files.Add(newFile); using (FileStream fs = File.Open(filename, FileMode.Create))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
ms.CopyTo(ds);
} }
newManifest.FilenamesEncrypted = false;
newManifest.DepotID = depotId;
newManifest.ManifestGID = ID;
newManifest.CreationTime = CreationTime;
newManifest.TotalUncompressedSize = uncompressedSize;
newManifest.TotalCompressedSize = compressedSize;
newManifest.EncryptedCRC = 0;
return newManifest;
} }
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,239 +1,170 @@
// This file is subject to the terms and conditions defined using System;
// in file 'LICENSE', which is part of this source code package. using System.Collections.Generic;
using System.IO;
using System; using System.Linq;
using System.Collections.Generic; using System.Runtime.InteropServices;
using System.IO; using System.Security.Cryptography;
using System.Linq; using System.Text;
using System.Runtime.InteropServices; using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Text; namespace DepotDownloader
using System.Threading.Tasks; {
using SteamKit2; static class Util
{
namespace DepotDownloader public static string GetSteamOS()
{ {
static class Util if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
public static string GetSteamOS() return "windows";
{ }
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
return "windows"; return "macos";
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
{ return "linux";
return "macos"; }
}
return "unknown";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) }
{
return "linux"; public static string GetSteamArch()
} {
return Environment.Is64BitOperatingSystem ? "64" : "32";
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) }
{
// Return linux as freebsd steam client doesn't exist yet public static string ReadPassword()
return "linux"; {
} ConsoleKeyInfo keyInfo;
StringBuilder password = new StringBuilder();
return "unknown";
} do
{
public static string GetSteamArch() keyInfo = Console.ReadKey( true );
{
return Environment.Is64BitOperatingSystem ? "64" : "32"; if ( keyInfo.Key == ConsoleKey.Backspace )
} {
if ( password.Length > 0 )
public static string ReadPassword() password.Remove( password.Length - 1, 1 );
{ continue;
ConsoleKeyInfo keyInfo; }
var password = new StringBuilder();
/* Printable ASCII characters only */
do char c = keyInfo.KeyChar;
{ if ( c >= ' ' && c <= '~' )
keyInfo = Console.ReadKey(true); password.Append( c );
} while ( keyInfo.Key != ConsoleKey.Enter );
if (keyInfo.Key == ConsoleKey.Backspace)
{ return password.ToString();
if (password.Length > 0) }
{
password.Remove(password.Length - 1, 1); // Validate a file against Steam3 Chunk data
Console.Write("\b \b"); public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata)
} {
var neededChunks = new List<ProtoManifest.ChunkData>();
continue; int read;
}
foreach (var data in chunkdata)
/* Printable ASCII characters only */ {
var c = keyInfo.KeyChar; byte[] chunk = new byte[data.UncompressedLength];
if (c >= ' ' && c <= '~') fs.Seek((long)data.Offset, SeekOrigin.Begin);
{ read = fs.Read(chunk, 0, (int)data.UncompressedLength);
password.Append(c);
Console.Write('*'); byte[] tempchunk;
} if (read < data.UncompressedLength)
} while (keyInfo.Key != ConsoleKey.Enter); {
tempchunk = new byte[read];
return password.ToString(); Array.Copy(chunk, 0, tempchunk, 0, read);
} }
else
// Validate a file against Steam3 Chunk data {
public static List<DepotManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, DepotManifest.ChunkData[] chunkdata) tempchunk = chunk;
{ }
var neededChunks = new List<DepotManifest.ChunkData>();
byte[] adler = AdlerHash(tempchunk);
foreach (var data in chunkdata) if (!adler.SequenceEqual(data.Checksum))
{ {
fs.Seek((long)data.Offset, SeekOrigin.Begin); neededChunks.Add(data);
}
var adler = AdlerHash(fs, (int)data.UncompressedLength); }
if (!adler.SequenceEqual(BitConverter.GetBytes(data.Checksum)))
{ return neededChunks;
neededChunks.Add(data); }
}
} public static byte[] AdlerHash(byte[] input)
{
return neededChunks; uint a = 0, b = 0;
} for (int i = 0; i < input.Length; i++)
{
public static byte[] AdlerHash(Stream stream, int length) a = (a + input[i]) % 65521;
{ b = (b + a) % 65521;
uint a = 0, b = 0; }
for (var i = 0; i < length; i++) return BitConverter.GetBytes(a | (b << 16));
{ }
var c = (uint)stream.ReadByte();
public static byte[] SHAHash( byte[] input )
a = (a + c) % 65521; {
b = (b + a) % 65521; using (var sha = SHA1.Create())
} {
var output = sha.ComputeHash( input );
return BitConverter.GetBytes(a | (b << 16));
} return output;
}
public static byte[] FileSHAHash(string filename) }
{
using (var fs = File.Open(filename, FileMode.Open)) public static byte[] DecodeHexString( string hex )
using (var sha = SHA1.Create()) {
{ if ( hex == null )
var output = sha.ComputeHash(fs); return null;
return output; int chars = hex.Length;
} byte[] bytes = new byte[ chars / 2 ];
}
for ( int i = 0 ; i < chars ; i += 2 )
public static DepotManifest LoadManifestFromFile(string directory, uint depotId, ulong manifestId, bool badHashWarning) bytes[ i / 2 ] = Convert.ToByte( hex.Substring( i, 2 ), 16 );
{
// Try loading Steam format manifest first. return bytes;
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", depotId, manifestId)); }
if (File.Exists(filename)) public static string EncodeHexString( byte[] input )
{ {
byte[] expectedChecksum; return input.Aggregate( new StringBuilder(),
( sb, v ) => sb.Append( v.ToString( "x2" ) )
try ).ToString();
{ }
expectedChecksum = File.ReadAllBytes(filename + ".sha");
} public static async Task InvokeAsync(IEnumerable<Func<Task>> taskFactories, int maxDegreeOfParallelism)
catch (IOException) {
{ if (taskFactories == null) throw new ArgumentNullException(nameof(taskFactories));
expectedChecksum = null; if (maxDegreeOfParallelism <= 0) throw new ArgumentException(nameof(maxDegreeOfParallelism));
}
Func<Task>[] queue = taskFactories.ToArray();
var currentChecksum = FileSHAHash(filename);
if (queue.Length == 0)
if (expectedChecksum != null && expectedChecksum.SequenceEqual(currentChecksum)) {
{ return;
return DepotManifest.LoadFromFile(filename); }
}
else if (badHashWarning) List<Task> tasksInFlight = new List<Task>(maxDegreeOfParallelism);
{ int index = 0;
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
} do
} {
while (tasksInFlight.Count < maxDegreeOfParallelism && index < queue.Length)
// Try converting legacy manifest format. {
filename = Path.Combine(directory, string.Format("{0}_{1}.bin", depotId, manifestId)); Func<Task> taskFactory = queue[index++];
if (File.Exists(filename)) tasksInFlight.Add(taskFactory());
{ }
byte[] expectedChecksum;
Task completedTask = await Task.WhenAny(tasksInFlight).ConfigureAwait(false);
try
{ await completedTask.ConfigureAwait(false);
expectedChecksum = File.ReadAllBytes(filename + ".sha");
} tasksInFlight.Remove(completedTask);
catch (IOException) }
{ while (index < queue.Length || tasksInFlight.Count != 0);
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)
return null;
var chars = hex.Length;
var bytes = new byte[chars / 2];
for (var i = 0; i < chars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
/// <summary>
/// Decrypts using AES/ECB/PKCS7
/// </summary>
public static byte[] SymmetricDecryptECB(byte[] input, byte[] key)
{
using var aes = Aes.Create();
aes.BlockSize = 128;
aes.KeySize = 256;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
using var aesTransform = aes.CreateDecryptor(key, null);
var output = aesTransform.TransformFinalBlock(input, 0, input.Length);
return output;
}
}
}

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2.0.0.640" newVersion="2.0.0.640"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -0,0 +1,3 @@
{
"rollForwardOnNoCandidateFx": 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 65 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="B" transform="matrix(1,0,0,1,0.5,0.5)">
<g>
<path d="M1.305,41.202C5.259,54.386 17.488,64 31.959,64C49.632,64 63.959,49.673 63.959,32C63.959,14.327 49.632,0 31.959,0C15.001,0 1.124,13.193 0.028,29.874C2.102,33.351 2.907,35.502 1.303,41.202L1.305,41.202Z" style="fill:rgb(56,61,72);fill-rule:nonzero;"/>
<g transform="matrix(-0.984183,-1.19698e-16,9.80712e-17,-0.795301,95.4776,96.4352)">
<g id="Arrow">
<path d="M45.613,41.896C45.811,41.658 46.125,41.658 46.322,41.896C48.069,44.002 55.776,53.297 58.421,56.487C58.569,56.666 58.615,56.938 58.537,57.174C58.459,57.411 58.273,57.566 58.067,57.566C55.616,57.566 50.417,57.566 50.417,57.566L50.417,80.833L41.519,90.628L41.519,57.566L33.869,57.566C33.662,57.566 33.476,57.411 33.399,57.174C33.321,56.938 33.367,56.666 33.515,56.487C36.16,53.297 43.867,44.002 45.613,41.896Z" style="fill:white;"/>
<path d="M44.904,40.996C43.158,43.102 35.45,52.397 32.805,55.587C32.361,56.122 32.224,56.937 32.457,57.648C32.691,58.359 33.249,58.824 33.869,58.824L40.502,58.824C40.502,58.824 40.502,90.628 40.502,90.628L42.194,91.567L51.433,81.398L51.433,58.824C51.433,58.824 58.067,58.824 58.067,58.824C58.687,58.824 59.245,58.359 59.478,57.648C59.712,56.937 59.574,56.122 59.13,55.587C56.485,52.397 48.778,43.102 47.032,40.996C46.44,40.282 45.496,40.282 44.904,40.996L44.904,40.996ZM45.613,41.896C45.811,41.658 46.125,41.658 46.322,41.896C48.069,44.002 55.776,53.297 58.421,56.487C58.569,56.666 58.615,56.938 58.537,57.174C58.459,57.411 58.273,57.566 58.067,57.566C55.616,57.566 50.417,57.566 50.417,57.566L50.417,80.833L41.519,90.628L41.519,57.566L33.869,57.566C33.662,57.566 33.476,57.411 33.399,57.174C33.321,56.938 33.367,56.666 33.515,56.487C36.16,53.297 43.867,44.002 45.613,41.896Z" style="fill:rgb(56,61,72);"/>
</g>
</g>
<path id="Steam" d="M30.31,23.985L30.313,24.143L22.483,35.518C21.215,35.46 19.943,35.683 18.735,36.18C18.209,36.394 17.707,36.662 17.237,36.98L0.042,29.893C0.042,29.893 -0.356,36.439 1.302,41.317L13.458,46.333C14.058,49.061 15.938,51.453 18.7,52.603C23.194,54.47 28.43,52.313 30.303,47.821C30.786,46.664 31.019,45.418 30.987,44.165L42.18,36.16L42.455,36.165C49.16,36.165 54.61,30.699 54.61,23.985C54.61,17.271 49.17,11.825 42.455,11.811C35.753,11.811 30.3,17.271 30.3,23.985L30.31,23.985ZM28.43,47.035C26.976,50.535 22.964,52.182 19.477,50.729C17.93,50.079 16.675,48.882 15.953,47.367L19.91,49.007C20.524,49.262 21.182,49.394 21.847,49.394C23.879,49.394 25.721,48.165 26.501,46.288C27.565,43.733 26.34,40.754 23.786,39.687L19.686,37.992C21.264,37.392 23.058,37.372 24.736,38.069C26.436,38.772 27.736,40.096 28.432,41.789C29.128,43.482 29.124,45.349 28.422,47.035M42.466,32.1C38.022,32.088 34.372,28.431 34.368,23.987C34.373,19.544 38.023,15.888 42.466,15.876C46.91,15.887 50.561,19.543 50.566,23.987C50.562,28.431 46.91,32.089 42.466,32.1M36.398,23.974C36.395,20.635 39.139,17.884 42.478,17.879C45.833,17.879 48.562,20.609 48.562,23.974C48.564,27.314 45.818,30.064 42.478,30.067C39.139,30.062 36.395,27.313 36.397,23.974L36.398,23.974Z" style="fill:rgb(175,212,255);fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,131 +1,55 @@
DepotDownloader DepotDownloader
=============== ===============
Steam depot downloader utilizing the SteamKit2 library. Supports .NET 8.0 Steam depot downloader utilizing the SteamKit2 library. Supports .NET 5.0
This program must be run from a console, it has no GUI.
## Installation
### Directly from GitHub
Download a binary from [the releases page](https://github.com/SteamRE/DepotDownloader/releases/latest).
### via Windows Package Manager CLI (aka winget)
On Windows, [winget](https://github.com/microsoft/winget-cli) users can download and install
the latest Terminal release by installing the `SteamRE.DepotDownloader`
package:
```powershell
winget install --exact --id SteamRE.DepotDownloader
```
### via Homebrew
On macOS, [Homebrew](https://brew.sh) users can download and install that latest release by running the following commands:
```shell
brew tap steamre/tools
brew install depotdownloader
```
## Usage
### Downloading one or all depots for an app ### Downloading one or all depots for an app
```powershell ```
./DepotDownloader -app <id> [-depot <id> [-manifest <id>]] dotnet DepotDownloader.dll -app <id> [-depot <id> [-manifest <id>]]
[-username <username> [-password <password>]] [other options] [-username <username> [-password <password>]] [other options]
``` ```
For example: `./DepotDownloader -app 730 -depot 731 -manifest 7617088375292372759` For example: `dotnet DepotDownloader.dll -app 730 -depot 731 -manifest 7617088375292372759`
By default it will use anonymous account ([view which apps are available on it here](https://steamdb.info/sub/17906/)).
To use your account, specify the `-username <username>` parameter. Password will be asked interactively if you do
not use specify the `-password` parameter.
### Downloading a workshop item using pubfile id ### Downloading a workshop item using pubfile id
```powershell ```
./DepotDownloader -app <id> -pubfile <id> [-username <username> [-password <password>]] dotnet DepotDownloader.dll -app <id> -pubfile <id> [-username <username> [-password <password>]]
``` ```
For example: `./DepotDownloader -app 730 -pubfile 1885082371` For example: `dotnet DepotDownloader.dll -app 730 -pubfile 1885082371`
### Downloading a workshop item using ugc id ### Downloading a workshop item using ugc id
```powershell ```
./DepotDownloader -app <id> -ugc <id> [-username <username> [-password <password>]] dotnet DepotDownloader.dll -app <id> -ugc <id> [-username <username> [-password <password>]]
``` ```
For example: `./DepotDownloader -app 730 -ugc 770604181014286929` For example: `dotnet DepotDownloader.dll -app 730 -ugc 770604181014286929`
## Parameters ## Parameters
#### Authentication Parameter | Description
--------- | -----------
Parameter | Description -app \<#> | the AppID to download.
----------------------- | ----------- -depot \<#> | the DepotID to download.
`-username <user>` | the username of the account to login to for restricted content. -manifest \<id> | manifest id of content to download (requires -depot, default: current for branch).
`-password <pass>` | the password of the account to login to for restricted content. -ugc \<#> | the UGC ID to download.
`-remember-password` | if set, remember the password for subsequent logins of this user. (Use `-username <username> -remember-password` as login credentials) -beta \<branchname> | download from specified branch if available (default: Public).
`-qr` | display a login QR code to be scanned with the Steam mobile app -betapassword \<pass> | branch password if applicable.
`-no-mobile` | prefer entering a 2FA code instead of prompting to accept in the Steam mobile app. -all-platforms | downloads all platform-specific depots when -app is used.
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently. -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)
#### Downloading -all-languages | download all language-specific depots when -app is used.
-language \<lang> | the language for which to download the game (default: english)
Parameter | Description -lowviolence | download low violence depots when -app is used.
------------------------ | ----------- -pubfile \<#> | the PublishedFileId to download. (Will automatically resolve to UGC id)
`-app <#>` | the AppID to download. -username \<user> | the username of the account to login to for restricted content.
`-depot <#>` | the DepotID to download. -password \<pass> | the password of the account to login to for restricted content.
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch). -remember-password | if set, remember the password for subsequent logins of this user.
`-ugc <#>` | the UGC ID to download. -dir \<installdir> | the directory in which to place downloaded files.
`-pubfile <#>` | the PublishedFileId to download. (Will automatically resolve to UGC id) -filelist \<file.txt> | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex.
`-branch <branchname>` | download from specified branch if available (default: Public). -validate | Include checksum verification of files already downloaded
`-branchpassword <pass>` | branch password if applicable. -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.
#### Download configuration -max-servers \<#> | maximum number of content servers to use. (default: 20).
-max-downloads \<#> | maximum number of chunks to download concurrently. (default: 8).
Parameter | Description -loginid \<#> | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
----------------------- | -----------
`-all-platforms` | downloads all platform-specific depots when `-app` is used.
`-os <os>` | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
`-osarch <arch>` | the architecture for which to download the game (32 or 64, default: the host's architecture)
`-all-archs` | download all architecture-specific depots when `-app` is used.
`-all-languages` | download all language-specific depots when `-app` is used.
`-language <lang>` | the language for which to download the game (default: english)
`-lowviolence` | download low violence depots when `-app` is used.
`-dir <installdir>` | the directory in which to place downloaded files.
`-filelist <file.txt>` | the name of a local file that contains a list of files to download (from the manifest). prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.
`-validate` | include checksum verification of files already downloaded.
`-manifest-only` | downloads a human readable manifest for any depots that would be downloaded.
`-cellid <#>` | the overridden CellID of the content server to download from.
`-max-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
### Why am I prompted to enter a 2-factor code every time I run the app?
Your 2-factor code authenticates a Steam session. You need to "remember" your session with `-remember-password` which persists the login key for your Steam session.
### Can I run DepotDownloader while an account is already connected to Steam?
Any connection to Steam will be closed if they share a LoginID. You can specify a different LoginID with `-loginid`.
### 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,6 +0,0 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestMinor"
}
}
Loading…
Cancel
Save