Compare commits

...

192 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Pavel Djundik 17d3996588 Add `-branchpassword`, change `-beta` to `-branch` in help text 9 months ago
Pavel Djundik 5d0c4fb050 Bump version to 3.0.0 9 months ago
Pavel Djundik d087678628 Disallow using -remember-password and -qr without -username 9 months ago
Pavel Djundik deac83600c
Merge pull request #565 from tpill90/AddLancacheSupport
Adding support for Lancache
9 months ago
Tim Pilius f567a63d84 Adding log message for when the lancache has been detected. 9 months ago
Tim Pilius e9a5abecb7 Adding support for Lancache with a new optional flag. 9 months ago
Pavel Djundik 5fe8a827d8 Use depotfromapp for getting manifest codes
Partially reverts 3143362d83

Fixes #569
Fixes #570
10 months ago
Pavel Djundik 682bfbac27 Do not abort downloading if one depot fails to get depot key 10 months ago
NicknineTheEagle 1c97b42124 Use Steam manifest format 10 months ago
Pavel Djundik 0fb803b5f6 Catch RequestFreeAppLicense exception 10 months ago
Pavel Djundik 2a939053d0 Add support for FreeToDownload apps 10 months ago
Pavel Djundik 4539666214 Cleanup 10 months ago
Yaakov fac3a22f4f Fix CA2022 warning 10 months ago
Yaakov 6c09739c8c Update to .NET 9 10 months ago
Pavel Djundik ff37997713 Add InvariantGlobalization 10 months ago
Pavel Djundik 72ffd79da6 Update packages 11 months ago
Ethan 2d06bf1c63 Improve `-filelist` comment
Fixes #554
11 months ago
Pavel Djundik 6a9364029b Tick callbacks in an infinite task 11 months ago
Pavel Djundik 9ed8a70508 Update steamkit 11 months ago
Pavel Djundik 247f8324ca
Merge pull request #501 from giacomopc/master
-all-archs: Allow to download all archs
11 months ago
Pavel Djundik a5bdf23880 Use GetStdHandle_SafeHandle 11 months ago
Pavel Djundik 926c41e7e0 Use cswin32 in ansi detector 11 months ago
Pavel Djundik 33738aeb49 Disable progress ansi reporting on linux
Some terminals use this code to show notifications instead
11 months ago
Pavel Djundik 177c44ef4a Let steamkit handle connect timeouts
Ref: #546
11 months ago
Pavel Djundik 9f64bd0439 Await steam jobs instead of WaitUntilCallback 11 months ago
Pavel Djundik e7d637ace6 Change dependabot to weekly 12 months ago
Pavel Djundik 6a0147ee72 Update steamkit, use new cdn auth token function
Fixes #545
12 months ago
Pavel Djundik 5a695e6dd1
Bump version to 2.7.2 12 months ago
Yaakov bffa2d5f50
Merge pull request #541 from SteamRE/dependabot/nuget/SteamKit2-3.0.0-Beta.3
Bump SteamKit2 from 3.0.0-Beta.2 to 3.0.0-Beta.3
1 year ago
dependabot[bot] 48794cfb4d
Bump SteamKit2 from 3.0.0-Beta.2 to 3.0.0-Beta.3
Bumps [SteamKit2](https://github.com/SteamRE/SteamKit) from 3.0.0-Beta.2 to 3.0.0-Beta.3.
- [Release notes](https://github.com/SteamRE/SteamKit/releases)
- [Commits](https://github.com/SteamRE/SteamKit/compare/3.0.0-Beta.2...3.0.0-Beta.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
1 year ago
Giacomo Preciado 39e43a1bf9
Merge branch 'master' into master 1 year ago
Giacomo Preciado b310d804f2 Updated README.md and help output 1 year ago
Pavel Djundik 907b3360cc
Forgot to bump version 1 year ago
Pavel Djundik 8117d8f6c2 Add -V/--version
Fixes #531
1 year ago
Pavel Djundik 53fefe6da2 Change exit code when using no params to zero 1 year ago
Pavel Djundik b9895121a4 Add header 1 year ago
Yaakov 83e2dbb03f Tweak installation instructions and add Homebrew 1 year ago
Pavel Djundik 317122f4aa Update README.md 1 year ago
Pavel Djundik 831bdb6001 Create github action to automatically submit new versions to winget 1 year ago
Pavel Djundik a3725da282
Merge pull request #530 from SteamRE/warnings
Fix warnings, enable Warnings as Errors
1 year ago
Yaakov fa486d1e5f even though we will probably never run on XP, this silences CA1416 1 year ago
Yaakov b1a36462cc Enable warnings as errors in CI 1 year ago
Yaakov 0772448cd9 Add ContinuousIntegrationBuild to CI 1 year ago
Pavel Djundik b5d4926f1e Update bug-report.yml 1 year ago
Pavel Djundik 8ffd52a214
Merge pull request #526 from SteamRE/alert
Display a message box when exe is not lauched from console
1 year ago
Pavel Djundik 92785c522b
Merge pull request #528 from SteamRE/execute
Use File.SetUnixFileMode
1 year ago
Pavel Djundik fec2fe746a Use File.SetUnixFileMode 1 year ago
Pavel Djundik 0ce116fdce Use CsWin32 1 year ago
Pavel Djundik a1f981168c Wrap parameters in code block 1 year ago
Pavel Djundik 240f789872 Update README.md 1 year ago
Pavel Djundik 785b0fe0b0 Update README.md 1 year ago
Pavel Djundik 653506ac14 Automatically upload all artifacts to releases 1 year ago
Pavel Djundik 48c00e5323 Add trim_trailing_whitespace 1 year ago
Pavel Djundik 1620090c19 Make extens stricter 1 year ago
Pavel Djundik 13b396b0d2 Align help output with spaces 1 year ago
Pavel Djundik 4c88a59f17 Display a message box when exe is not lauched from console 1 year ago
Pavel Djundik c62d2d733f Use full matrix name in sk2-ci 1 year ago
Pavel Djundik 75cbd8b12a Enable Deterministic 1 year ago
Pavel Djundik bae332b97a Add license header to all source files
Fixes #55
1 year ago
Pavel Djundik 1b95a6ba47 Copy license file to build output 1 year ago
Pavel Djundik e2194fc873 Attempt to get cdn auth when getting 403 1 year ago
Pavel Djundik 33f0a7fcba Reset ansi progress on disconnect 1 year ago
Pavel Djundik 6edab0c55e Use ReadAllLinesAsync and async main 1 year ago
Pavel Djundik baa64e9e51 Update DownloadDepotChunkAsync to use array pool 1 year ago
Pavel Djundik a3fc9c4c45 Remove inlined adler hash 1 year ago
Pavel Djundik 294dbff40a Optimize calculating adler hashes to not allocate entire chunk in memory 1 year ago
Pavel Djundik e7e2299b70 Update SteamKit 1 year ago
Pavel Djundik 9a3f27ddec
Merge pull request #517 from SteamRE/progress
Use ansi escape code to indicate download progress in taskbar
1 year ago
Pavel Djundik ccbffd1c08 Use ansi escape code to indicate download progress in taskbar 1 year ago
Pavel Djundik ae72ac03c9 Update steamkit, fix line endings 1 year ago
Pavel Djundik c6a5bcf87e
Merge pull request #511 from SteamRE/dependabot/nuget/QRCoder-1.6.0
Bump QRCoder from 1.4.3 to 1.6.0
1 year ago
Pavel Djundik f00ef83029
Merge pull request #484 from er2off/master
Add FreeBSD support
1 year ago
Pavel Djundik 65c07130a1 Fix style violations 1 year ago
Pavel Djundik 9d04652f60 Add SymmetricDecryptECB to util 1 year ago
dependabot[bot] beb8ef8507
Bump QRCoder from 1.4.3 to 1.6.0
Bumps [QRCoder](https://github.com/codebude/QRCoder) from 1.4.3 to 1.6.0.
- [Release notes](https://github.com/codebude/QRCoder/releases)
- [Commits](https://github.com/codebude/QRCoder/compare/v1.4.3...v1.6.0)

---
updated-dependencies:
- dependency-name: QRCoder
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
1 year ago
Giacomo Preciado ddda3924dc new parameter -all-archs downloads all OS archs 1 year ago
Pavel Djundik b96125f9cb Bump version 1 year ago
Pavel Djundik b75d398392 Fix bracket 1 year ago
Pavel Djundik b532f3944f Update to steamkit 3.0.0-Alpha.1 1 year ago
Pavel Djundik 544d19e925 Update github action packages 1 year ago
Er2 d8ccf800ca Add FreeBSD support 2 years ago
Pavel Djundik 9d2446404a
Merge pull request #474 from SteamRE/cleanup
Cleanup
2 years ago
Pavel Djundik 93318f2b11
Merge pull request #473 from SteamRE/tokens
Handle more invalid token eresults
2 years ago
Pavel Djundik 0f00ea592e Remove Steam3Session.Credentials 2 years ago
Pavel Djundik 93de24c7f5 Remove constructor setters 2 years ago
Pavel Djundik 7739fac67c Cleanup 2 years ago
Pavel Djundik fac3ac229e Print the actual result 2 years ago
Pavel Djundik 19feb49bfa Handle more invalid token eresults
Fixes #460
2 years ago
Yaakov 5cb0c17aa3
Update README.md for .NET 8 2 years ago
Yaakov 366f98e80b Merge branch 'dotnet8' 2 years ago
Yaakov 828ba1ecaf Merge branch 'net8' 2 years ago
Yaakov 3429010f50 Fix another instance of new .NET 8 SDK warning 2 years ago
Yaakov bf71c4c0b9 Remove obsolete sentry file code 2 years ago
Yaakov 225a26c321 Fix warning NETSDK1194 about --output from .sln files 2 years ago
Yaakov 09f9674bad Update NuGet packages (uncl SK2) 2 years ago
Yaakov 6c51846c5b fix public types causing xmldoc warnings by making them internal 2 years ago
Yaakov 679e019f27 fix compiler warning re. IDE0005 2 years ago
Yaakov 55fda1a0dd update tfm to net8.0 2 years ago
Yaakov 2027d919f9
Merge pull request #470 from SteamRE/ci/macos-arm
Update CI
2 years ago
Yaakov b75c83cbf1 Update global.json for sdk 8 2 years ago
Yaakov b4a6c70d58 update CI to use .NET 8 SDK 2 years ago
Yaakov 665901cbdc Add new GitHub Actions M1 runners to CI 2 years ago
Antoine Rybacki e81efa7a46 rectify parameters order 2 years ago
Antoine Rybacki 725b5a3955 misc 2 years ago
Antoine Rybacki db3dc55598 fix warning linked to --output 2 years ago
Antoine Rybacki 69707095cd address feedback 2 years ago
Antoine Rybacki 29eed5b9df remove unused using 2 years ago
Antoine Rybacki 4220d76de6 Remove unneeded guarddata file handling 2 years ago
Antoine Rybacki 5b4917eb29 more CA fix 2 years ago
Antoine Rybacki 58182284b2 code analysis fix 2 years ago
Antoine Rybacki bf0f89fed0 Remove Deprecated SentryData and add GuardData 2 years ago
Antoine Rybacki 5077640cf2 fix CA1834 2 years ago
Antoine Rybacki 43e0cccd47 Bump to net8 2 years ago
Pavel Djundik 4c52ad3a4c Upload only a single framework build 2 years ago
Pavel Djundik ec692a8d9e
Bump version to 2.5.0 2 years ago
Pavel Djundik 0122f0f63f
Run automatic CI only on Sundays 2 years ago
Pavel Djundik 5b6315c8ca
Merge pull request #433 from SteamRE/no-old-gids
Remove code to handle older style of gids
2 years ago
Pavel Djundik cc5da551f9 Case-insensitive comparison of `public` branch
Fixes #429
Fixes #428
2 years ago
Pavel Djundik 0e8b662715 Do not try to print machine auth hash 2 years ago
Pavel Djundik 9aaed770d7
Merge pull request #401 from SteamRE/auth/accesstoken
Add support for new Steam authentication
2 years ago
Pavel Djundik 8d31a80c98 Remove code to handle older style of gids 2 years ago
Pavel Djundik 92809ab8eb
Merge pull request #427 from SteamRE/encrypted-gid
Support new encrypted gid key
2 years ago
Pavel Djundik 2cfee72f69 Support new `gid` field in encrypted manifests 2 years ago
Pavel Djundik 4f7b601e2c Remove support for encrypted_gid (v1) 2 years ago
Pavel Djundik 33f78dfff5
Fix formatting
Fixes #426
2 years ago
Pavel Djundik 792e77d91f
Add self-contained builds (#418) 2 years ago
Pavel Djundik d5ce7ee18e
Merge pull request #399 from SteamRE/psy/rip-depot-names
Remove printing of depot name
2 years ago
Pavel Djundik 4b2fb1f7b7
Merge pull request #425 from SteamRE/psy/expanded-branch-format
Add support for appinfo depots section with expanded branch info.
2 years ago
Nicholas Hastings 196e99b30d Add support for appinfo depots section with expanded branch info. 2 years ago
Pavel Djundik 347806b53b
Add icon (#417) 3 years ago
Pavel Djundik dea7a632c1 Add issue templates 3 years ago
Yaakov d73462e077 improve pre-login auth handling (I think?) 3 years ago
Yaakov c36ddcc065 handle reconnects more gracefully 3 years ago
Yaakov 8d6c09e779 dispose of disposables 3 years ago
Yaakov 6ffa0948a4 Implement -qr CLI arg to log in with a QR code 3 years ago
Yaakov 0112ca749d Rudimentary support for new access tokens 3 years ago
Yaakov b31d79f556 Revert "be a little more specific when updating the pkg"
This reverts commit 7b0f3135ff.
3 years ago
Yaakov d071ba04cd Merge branch 'master' of github.com:SteamRE/DepotDownloader 3 years ago
Yaakov 7b0f3135ff be a little more specific when updating the pkg 3 years ago
Yaakov f9d75e05b0
Merge pull request #398 from SteamRE/dependabot/nuget/protobuf-net-3.2.16
Bump protobuf-net from 3.2.0 to 3.2.16
3 years ago
Yaakov f2fe082112 fix yaml? 3 years ago
Yaakov 790b938d76 Add scheduled workflow for nightly builds against latest SK2 3 years ago
Nicholas Hastings 5e414bc48b Remove printing of depot name. It's no longer sent. 3 years ago
dependabot[bot] 3b060a6632
Bump protobuf-net from 3.2.0 to 3.2.16
Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) from 3.2.0 to 3.2.16.
- [Release notes](https://github.com/protobuf-net/protobuf-net/releases)
- [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/protobuf-net/protobuf-net/commits/3.2.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
3 years ago
Yaakov c26eaa60c9
Merge pull request #388 from SteamRE/dependabot/nuget/protobuf-net-3.2.0
Bump protobuf-net from 3.1.17 to 3.2.0
3 years ago
dependabot[bot] bf4df71d64 Bump protobuf-net from 3.1.17 to 3.2.0
Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) from 3.1.17 to 3.2.0.
- [Release notes](https://github.com/protobuf-net/protobuf-net/releases)
- [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/protobuf-net/protobuf-net/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
3 years ago
Yaakov 10cc4c9131 Add global.json, lock to .NET 6.0 SDK 3 years ago
Margen67 ced92a1128 CI updates
Rename matrix.os to runs-on.
Opt out of dotnet telemetry.
Upgrade checkout and upload-artifact to v3.
Upgrade setup-dotnet to v2.
3 years ago
Scott Ehlert 42b8584edf Simplify/refactor PlatformUtilities.SetExecutable and add support for using stat on ARM versions of Linux. 3 years ago
Ryan Kistner 3143362d83 2.4.7 - Don't use depotfromapp to request depot key or manifest request code 3 years ago
Yaakov 4ee9db7565
Merge pull request #335 from SteamRE/dependabot/nuget/protobuf-net-3.1.17
Bump protobuf-net from 3.0.101 to 3.1.17
3 years ago
dependabot[bot] b65ea1b1ec
Bump protobuf-net from 3.0.101 to 3.1.17
Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) from 3.0.101 to 3.1.17.
- [Release notes](https://github.com/protobuf-net/protobuf-net/releases)
- [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/protobuf-net/protobuf-net/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
3 years ago

@ -6,11 +6,16 @@ indent_style = space
charset = utf-8
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
@ -95,7 +100,6 @@ dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_s
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
@ -124,26 +128,9 @@ dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Async methods should have "Async" suffix
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
dotnet_naming_rule.async_methods_end_in_async.severity = warning
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
dotnet_naming_symbols.any_async_methods.required_modifiers = async
dotnet_naming_style.end_in_async.required_prefix =
dotnet_naming_style.end_in_async.required_suffix = Async
dotnet_naming_style.end_in_async.capitalization = pascal_case
dotnet_naming_style.end_in_async.word_separator =
# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
dotnet_diagnostic.RS2008.severity = none
# IDE0005: Remove unnecessary import
dotnet_diagnostic.IDE0005.severity = warning
# IDE0007: Use `var` instead of explicit type
dotnet_diagnostic.IDE0007.severity = warning
@ -161,6 +148,11 @@ 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

2
.gitattributes vendored

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

@ -0,0 +1,60 @@
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

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

@ -0,0 +1,36 @@
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,5 +3,13 @@ updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: daily
interval: weekly
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,28 +15,156 @@ on:
jobs:
build:
name: .NET on ${{ matrix.os }} (${{ matrix.configuration }})
runs-on: ${{ matrix.os }}
name: .NET on ${{ matrix.runs-on }} (${{ matrix.configuration }})
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: [macos-latest, macos-14, ubuntu-latest, windows-latest]
configuration: [Release, Debug]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
uses: actions/setup-dotnet@v5
- name: Build
run: dotnet publish -c ${{ matrix.configuration }} -o artifacts
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
- name: Upload artifact
uses: actions/upload-artifact@v2
if: matrix.configuration == 'Release'
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
with:
name: DepotDownloader-${{ runner.os }}
name: DepotDownloader-framework
path: artifacts
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 }}

@ -0,0 +1,39 @@
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

@ -0,0 +1,33 @@
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*
.*crunch*.local.xml
# Installshield output folder
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
@ -117,4 +117,5 @@ protobuf/
cryptopp/
# misc
Thumbs.db
Thumbs.db
launchSettings.json

@ -1,4 +1,7 @@
using System;
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -11,22 +14,26 @@ namespace DepotDownloader
[ProtoContract]
class AccountSettingsStore
{
[ProtoMember(1, IsRequired = false)]
public Dictionary<string, byte[]> SentryData { get; private set; }
// Member 1 was a Dictionary<string, byte[]> for SentryData.
[ProtoMember(2, IsRequired = false)]
public ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; }
[ProtoMember(3, IsRequired = false)]
public Dictionary<string, string> LoginKeys { get; private set; }
// Member 3 was a Dictionary<string, string> for LoginKeys.
[ProtoMember(4, IsRequired = false)]
public Dictionary<string, string> LoginTokens { get; private set; }
[ProtoMember(5, IsRequired = false)]
public Dictionary<string, string> GuardData { get; private set; }
string FileName;
AccountSettingsStore()
{
SentryData = new Dictionary<string, byte[]>();
ContentServerPenalty = new ConcurrentDictionary<string, int>();
LoginKeys = new Dictionary<string, string>();
LoginTokens = new(StringComparer.OrdinalIgnoreCase);
GuardData = new(StringComparer.OrdinalIgnoreCase);
}
static bool Loaded
@ -46,11 +53,9 @@ namespace DepotDownloader
{
try
{
using (var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
{
Instance = Serializer.Deserialize<AccountSettingsStore>(ds);
}
using var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read);
using var ds = new DeflateStream(fs, CompressionMode.Decompress);
Instance = Serializer.Deserialize<AccountSettingsStore>(ds);
}
catch (IOException ex)
{
@ -73,11 +78,9 @@ namespace DepotDownloader
try
{
using (var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
{
Serializer.Serialize(ds, Instance);
}
using var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write);
using var ds = new DeflateStream(fs, CompressionMode.Compress);
Serializer.Serialize(ds, Instance);
}
catch (IOException ex)
{

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

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

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

File diff suppressed because it is too large Load Diff

@ -1,4 +1,7 @@
using System;
// 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.Collections.Generic;
using System.IO;
using System.IO.Compression;
@ -16,7 +19,7 @@ namespace DepotDownloader
DepotConfigStore()
{
InstalledManifestIDs = new Dictionary<uint, ulong>();
InstalledManifestIDs = [];
}
static bool Loaded
@ -33,9 +36,9 @@ namespace DepotDownloader
if (File.Exists(filename))
{
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
Instance = Serializer.Deserialize<DepotConfigStore>(ds);
using var fs = File.Open(filename, FileMode.Open);
using var ds = new DeflateStream(fs, CompressionMode.Decompress);
Instance = Serializer.Deserialize<DepotConfigStore>(ds);
}
else
{
@ -50,9 +53,9 @@ namespace DepotDownloader
if (!Loaded)
throw new Exception("Saved config before loading");
using (var fs = File.Open(Instance.FileName, FileMode.Create))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
Serializer.Serialize(ds, Instance);
using var fs = File.Open(Instance.FileName, FileMode.Create);
using var ds = new DeflateStream(fs, CompressionMode.Compress);
Serializer.Serialize(ds, Instance);
}
}
}

@ -1,17 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RollForward>LatestMajor</RollForward>
<Version>2.4.6</Version>
<Version>3.4.0</Version>
<Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2021</Copyright>
<Copyright>Copyright © SteamRE Team 2025</Copyright>
<ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon>
<Deterministic>true</Deterministic>
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="protobuf-net" Version="3.0.101" />
<PackageReference Include="SteamKit2" Version="2.4.1" />
<None Include="..\LICENSE" Link="LICENSE">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<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,4 +1,7 @@
using System.Collections.Generic;
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace DepotDownloader
@ -7,6 +10,7 @@ namespace DepotDownloader
{
public int CellID { get; set; }
public bool DownloadAllPlatforms { get; set; }
public bool DownloadAllArchs { get; set; }
public bool DownloadAllLanguages { get; set; }
public bool DownloadManifestOnly { get; set; }
public string InstallDirectory { get; set; }
@ -19,13 +23,14 @@ namespace DepotDownloader
public bool VerifyAll { get; set; }
public int MaxServers { get; set; }
public int MaxDownloads { get; set; }
public string SuppliedPassword { get; set; }
public bool RememberPassword { 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,4 +1,7 @@
using System.IO;
// 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;
@ -29,8 +32,10 @@ namespace DepotDownloader
// 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);
socket.NoDelay = true;
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
try
{

@ -1,4 +1,7 @@
using System;
// 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;
@ -35,7 +38,7 @@ namespace DepotDownloader
}
}
sb.Append(")");
sb.Append(')');
Console.WriteLine(sb.ToString());
}
}

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

@ -1,84 +1,51 @@
using System;
// 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
{
public static class PlatformUtilities
static class PlatformUtilities
{
private const int ModeExecuteOwner = 0x0040;
private const int ModeExecuteGroup = 0x0008;
private const int ModeExecuteOther = 0x0001;
private const int ModeExecute = ModeExecuteOwner | ModeExecuteGroup | ModeExecuteOther;
[StructLayout(LayoutKind.Explicit, Size = 144)]
private readonly struct StatLinux
{
[FieldOffset(24)] public readonly uint st_mode;
}
[StructLayout(LayoutKind.Explicit, Size = 144)]
private readonly struct StatOSX
public static void SetExecutable(string path, bool value)
{
[FieldOffset(4)] public readonly ushort st_mode;
}
[DllImport("libc", EntryPoint = "__xstat", SetLastError = true)]
private static extern int statLinux(int version, string path, out StatLinux statLinux);
[DllImport("libc", EntryPoint = "stat", SetLastError = true)]
private static extern int statOSX(string path, out StatOSX stat);
[DllImport("libc", EntryPoint = "stat$INODE64", SetLastError = true)]
private static extern int statOSXCompat(string path, out StatOSX stat);
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string path, uint mode);
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string path, ushort mode);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
private static extern IntPtr strerror(int errno);
const UnixFileMode ModeExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
private static void ThrowIf(int i)
{
if (i == -1)
var mode = File.GetUnixFileMode(path);
var hasExecuteMask = (mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
var errno = Marshal.GetLastWin32Error();
throw new Exception(Marshal.PtrToStringAnsi(strerror(errno)));
File.SetUnixFileMode(path, value
? mode | ModeExecute
: mode & ~ModeExecute);
}
}
public static void SetExecutable(string path, bool value)
[SupportedOSPlatform("windows5.0")]
public static void VerifyConsoleLaunch()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
ThrowIf(statLinux(1, path, out var stat));
// Reference: https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922
var processList = new uint[2];
var processCount = Windows.Win32.PInvoke.GetConsoleProcessList(processList);
var hasExecuteMask = (stat.st_mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
ThrowIf(chmod(path, (uint)(value
? stat.st_mode | ModeExecute
: stat.st_mode & ~ModeExecute)));
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
if (processCount != 1)
{
StatOSX stat;
ThrowIf(RuntimeInformation.ProcessArchitecture == Architecture.Arm64
? statOSX(path, out stat)
: statOSXCompat(path, out stat));
var hasExecuteMask = (stat.st_mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
ThrowIf(chmod(path, (ushort)(value
? stat.st_mode | ModeExecute
: stat.st_mode & ~ModeExecute)));
}
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,3 +1,6 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Collections.Generic;
using System.ComponentModel;
@ -8,30 +11,50 @@ using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SteamKit2;
using SteamKit2.CDN;
namespace DepotDownloader
{
class Program
{
static int Main(string[] args)
=> MainAsync(args).GetAwaiter().GetResult();
private static bool[] consumedArgs;
static async Task<int> MainAsync(string[] args)
static async Task<int> Main(string[] args)
{
if (args.Length == 0)
{
PrintVersion();
PrintUsage();
return 1;
if (OperatingSystem.IsWindowsVersionAtLeast(5, 0))
{
PlatformUtilities.VerifyConsoleLaunch();
}
return 0;
}
Ansi.Init();
DebugLog.Enabled = false;
AccountSettingsStore.LoadFromFile("account.config");
#region Common Options
// Not using HasParameter because it is case insensitive
if (args.Length == 1 && (args[0] == "-V" || args[0] == "--version"))
{
PrintVersion(true);
return 0;
}
consumedArgs = new bool[args.Length];
if (HasParameter(args, "-debug"))
{
PrintVersion(true);
DebugLog.Enabled = true;
DebugLog.AddListener((category, message) =>
{
@ -39,14 +62,27 @@ namespace DepotDownloader
});
var httpEventListener = new HttpDiagnosticEventListener();
DebugLog.WriteLine("DepotDownloader", "Version: {0}", Assembly.GetExecutingAssembly().GetName().Version);
DebugLog.WriteLine("DepotDownloader", "Runtime: {0}", RuntimeInformation.FrameworkDescription);
}
var username = GetParameter<string>(args, "-username") ?? GetParameter<string>(args, "-user");
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass");
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password");
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr");
ContentDownloader.Config.SkipAppConfirmation = HasParameter(args, "-no-mobile");
if (username == null)
{
if (ContentDownloader.Config.RememberPassword && !ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -remember-password can not be used without -username or -qr.");
return 1;
}
}
else if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -qr can not be used with -username.");
return 1;
}
ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only");
@ -62,20 +98,26 @@ namespace DepotDownloader
if (fileList != null)
{
const string RegexPrefix = "regex:";
try
{
var fileListData = await File.ReadAllTextAsync(fileList);
var files = fileListData.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
ContentDownloader.Config.UsingFileList = true;
ContentDownloader.Config.FilesToDownload = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
ContentDownloader.Config.FilesToDownloadRegex = new List<Regex>();
ContentDownloader.Config.FilesToDownloadRegex = [];
var files = await File.ReadAllLinesAsync(fileList);
foreach (var fileEntry in files)
{
if (fileEntry.StartsWith("regex:"))
if (string.IsNullOrWhiteSpace(fileEntry))
{
continue;
}
if (fileEntry.StartsWith(RegexPrefix))
{
var rgx = new Regex(fileEntry.Substring(6), RegexOptions.Compiled | RegexOptions.IgnoreCase);
var rgx = new Regex(fileEntry[RegexPrefix.Length..], RegexOptions.Compiled | RegexOptions.IgnoreCase);
ContentDownloader.Config.FilesToDownloadRegex.Add(rgx);
}
else
@ -95,9 +137,24 @@ namespace DepotDownloader
ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir");
ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate");
ContentDownloader.Config.MaxServers = GetParameter(args, "-max-servers", 20);
if (HasParameter(args, "-use-lancache"))
{
await Client.DetectLancacheServerAsync();
if (Client.UseLancacheServer)
{
Console.WriteLine("Detected Lancache server! Downloads will be directed through the Lancache.");
// Increasing the number of concurrent downloads when the cache is detected since the downloads will likely
// be served much faster than over the internet. Steam internally has this behavior as well.
if (!HasParameter(args, "-max-downloads"))
{
ContentDownloader.Config.MaxDownloads = 25;
}
}
}
ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8);
ContentDownloader.Config.MaxServers = Math.Max(ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads);
ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null;
#endregion
@ -115,6 +172,8 @@ namespace DepotDownloader
{
#region Pubfile Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -150,6 +209,8 @@ namespace DepotDownloader
{
#region UGC Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -186,23 +247,38 @@ namespace DepotDownloader
#region App downloading
var branch = GetParameter<string>(args, "-branch") ?? GetParameter<string>(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH;
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-betapassword");
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-branchpassword") ?? GetParameter<string>(args, "-betapassword");
if (!string.IsNullOrEmpty(ContentDownloader.Config.BetaPassword) && string.IsNullOrEmpty(branch))
{
Console.WriteLine("Error: Cannot specify -branchpassword when -branch is not specified.");
return 1;
}
ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms");
var os = GetParameter<string>(args, "-os");
if (ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty(os))
if (ContentDownloader.Config.DownloadAllPlatforms && !string.IsNullOrEmpty(os))
{
Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified.");
return 1;
}
ContentDownloader.Config.DownloadAllArchs = HasParameter(args, "-all-archs");
var arch = GetParameter<string>(args, "-osarch");
if (ContentDownloader.Config.DownloadAllArchs && !string.IsNullOrEmpty(arch))
{
Console.WriteLine("Error: Cannot specify -osarch when -all-archs is specified.");
return 1;
}
ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages");
var language = GetParameter<string>(args, "-language");
if (ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty(language))
if (ContentDownloader.Config.DownloadAllLanguages && !string.IsNullOrEmpty(language))
{
Console.WriteLine("Error: Cannot specify -language when -all-languages is specified.");
return 1;
@ -231,6 +307,8 @@ namespace DepotDownloader
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID)));
}
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -268,31 +346,51 @@ namespace DepotDownloader
static bool InitializeSteam(string username, string password)
{
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey(username)))
if (!ContentDownloader.Config.UseQrCode)
{
do
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username)))
{
Console.Write("Enter account password for \"{0}\": ", username);
if (Console.IsInputRedirected)
if (AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))
{
password = Console.ReadLine();
Console.WriteLine($"Account \"{username}\" has stored credentials. Did you forget to specify -remember-password?");
}
else
do
{
// Avoid console echoing of password
password = Util.ReadPassword();
}
Console.Write("Enter account password for \"{0}\": ", username);
if (Console.IsInputRedirected)
{
password = Console.ReadLine();
}
else
{
// Avoid console echoing of password
password = Util.ReadPassword();
}
Console.WriteLine();
} while (string.Empty == password);
Console.WriteLine();
} while (string.Empty == password);
}
else if (username == null)
{
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription.");
}
}
else if (username == null)
if (!string.IsNullOrEmpty(password))
{
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription.");
}
const int MAX_PASSWORD_SIZE = 64;
// capture the supplied password in case we need to re-use it after checking the login key
ContentDownloader.Config.SuppliedPassword = password;
if (password.Length > MAX_PASSWORD_SIZE)
{
Console.Error.WriteLine($"Warning: Password is longer than {MAX_PASSWORD_SIZE} characters, which is not supported by Steam.");
}
if (!password.All(char.IsAscii))
{
Console.Error.WriteLine("Warning: Password contains non-ASCII characters, which is not supported by Steam.");
}
}
return ContentDownloader.InitializeSteam3(username, password);
}
@ -302,7 +400,10 @@ namespace DepotDownloader
for (var x = 0; x < args.Length; ++x)
{
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
{
consumedArgs[x] = true;
return x;
}
}
return -1;
@ -313,7 +414,7 @@ namespace DepotDownloader
return IndexOfParam(args, param) > -1;
}
static T GetParameter<T>(string[] args, string param, T defaultValue = default(T))
static T GetParameter<T>(string[] args, string param, T defaultValue = default)
{
var index = IndexOfParam(args, param);
@ -325,10 +426,11 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index + 1] = true;
return (T)converter.ConvertFromString(strParam);
}
return default(T);
return default;
}
static List<T> GetParameterList<T>(string[] args, string param)
@ -350,6 +452,7 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index] = true;
list.Add((T)converter.ConvertFromString(strParam));
}
@ -359,47 +462,89 @@ namespace DepotDownloader
return list;
}
static void PrintUnconsumedArgs(string[] args)
{
var printError = false;
for (var index = 0; index < consumedArgs.Length; index++)
{
if (!consumedArgs[index])
{
printError = true;
Console.Error.WriteLine($"Argument #{index + 1} {args[index]} was not used.");
}
}
if (printError)
{
Console.Error.WriteLine("Make sure you specified the arguments correctly. Check --help for correct arguments.");
Console.Error.WriteLine();
}
}
static void PrintUsage()
{
// Do not use tabs to align parameters here because tab size may differ
Console.WriteLine();
Console.WriteLine("Usage - downloading one or all depots for an app:");
Console.WriteLine("\tdepotdownloader -app <id> [-depot <id> [-manifest <id>]]");
Console.WriteLine("\t\t[-username <username> [-password <password>]] [other options]");
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("\tdepotdownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]");
Console.WriteLine("Usage - downloading a workshop item using ugc id");
Console.WriteLine("\tdepotdownloader -app <id> -ugc <id> [-username <username> [-password <password>]]");
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("\t-app <#>\t\t\t\t- the AppID to download.");
Console.WriteLine("\t-depot <#>\t\t\t\t- the DepotID to download.");
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("\t-all-platforms\t\t\t- downloads all platform-specific depots when -app is used.");
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)");
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("\t-lowviolence\t\t\t\t- download low violence depots when -app is used.");
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("\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)");
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("\t-username <user>\t\t- the username of the account to login to for restricted content.");
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. (Use -username <username> -remember-password as login credentials)");
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("\t-dir <installdir>\t\t- the directory in which to place downloaded files.");
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(" -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("\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.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.");
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}");
}
}
}

@ -1,7 +1,12 @@
using System;
// 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.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using ProtoBuf;
using SteamKit2;
@ -13,7 +18,7 @@ namespace DepotDownloader
// Proto ctor
private ProtoManifest()
{
Files = new List<FileData>();
Files = [];
}
public ProtoManifest(DepotManifest sourceManifest, ulong id) : this()
@ -29,7 +34,7 @@ namespace DepotDownloader
// Proto ctor
private FileData()
{
Chunks = new List<ChunkData>();
Chunks = [];
}
public FileData(DepotManifest.FileData sourceData) : this()
@ -75,7 +80,7 @@ namespace DepotDownloader
public ChunkData(DepotManifest.ChunkData sourceChunk)
{
ChunkID = sourceChunk.ChunkID;
Checksum = sourceChunk.Checksum;
Checksum = BitConverter.GetBytes(sourceChunk.Checksum);
Offset = sourceChunk.Offset;
CompressedLength = sourceChunk.CompressedLength;
UncompressedLength = sourceChunk.UncompressedLength;
@ -129,33 +134,63 @@ namespace DepotDownloader
return null;
}
using (var ms = new MemoryStream())
{
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
ds.CopyTo(ms);
using var ms = new MemoryStream();
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
ds.CopyTo(ms);
checksum = Util.SHAHash(ms.ToArray());
checksum = SHA1.HashData(ms.ToArray());
ms.Seek(0, SeekOrigin.Begin);
return Serializer.Deserialize<ProtoManifest>(ms);
}
ms.Seek(0, SeekOrigin.Begin);
return Serializer.Deserialize<ProtoManifest>(ms);
}
public void SaveToFile(string filename, out byte[] checksum)
{
using (var ms = new MemoryStream())
using var ms = new MemoryStream();
Serializer.Serialize(ms, this);
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)
{
Serializer.Serialize(ms, this);
var fileNameHash = SHA1.HashData(Encoding.UTF8.GetBytes(file.FileName.Replace('/', '\\').ToLowerInvariant()));
var newFile = new DepotManifest.FileData(file.FileName, fileNameHash, file.Flags, file.TotalSize, file.FileHash, null, false, file.Chunks.Count);
checksum = Util.SHAHash(ms.ToArray());
foreach (var chunk in file.Chunks)
{
var newChunk = new DepotManifest.ChunkData(chunk.ChunkID, BitConverter.ToUInt32(chunk.Checksum, 0), chunk.Offset, chunk.CompressedLength, chunk.UncompressedLength);
newFile.Chunks.Add(newChunk);
ms.Seek(0, SeekOrigin.Begin);
uncompressedSize += chunk.UncompressedLength;
compressedSize += chunk.CompressedLength;
}
using (var fs = File.Open(filename, FileMode.Create))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
ms.CopyTo(ds);
newManifest.Files.Add(newFile);
}
newManifest.FilenamesEncrypted = false;
newManifest.DepotID = depotId;
newManifest.ManifestGID = ID;
newManifest.CreationTime = CreationTime;
newManifest.TotalUncompressedSize = uncompressedSize;
newManifest.TotalCompressedSize = compressedSize;
newManifest.EncryptedCRC = 0;
return newManifest;
}
}
}

@ -1,28 +1,24 @@
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using QRCoder;
using SteamKit2;
using SteamKit2.Authentication;
using SteamKit2.CDN;
using SteamKit2.Internal;
namespace DepotDownloader
{
class Steam3Session
{
public class Credentials
{
public bool LoggedOn { get; set; }
public ulong SessionToken { get; set; }
public bool IsValid
{
get { return LoggedOn; }
}
}
public bool IsLoggedOn { get; private set; }
public ReadOnlyCollection<SteamApps.LicenseListCallback.License> Licenses
{
@ -30,69 +26,45 @@ namespace DepotDownloader
private set;
}
public Dictionary<uint, ulong> AppTokens { get; private set; }
public Dictionary<uint, ulong> PackageTokens { get; private set; }
public Dictionary<uint, byte[]> DepotKeys { get; private set; }
public ConcurrentDictionary<string, TaskCompletionSource<SteamApps.CDNAuthTokenCallback>> CDNAuthTokens { get; private set; }
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> AppInfo { get; private set; }
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> PackageInfo { get; private set; }
public Dictionary<string, byte[]> AppBetaPasswords { get; private set; }
public Dictionary<uint, ulong> AppTokens { get; } = [];
public Dictionary<uint, ulong> PackageTokens { get; } = [];
public Dictionary<uint, byte[]> DepotKeys { get; } = [];
public ConcurrentDictionary<(uint, string), TaskCompletionSource<SteamContent.CDNAuthToken>> CDNAuthTokens { get; } = [];
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> AppInfo { get; } = [];
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> PackageInfo { get; } = [];
public Dictionary<string, byte[]> AppBetaPasswords { get; } = [];
public SteamClient steamClient;
public SteamUser steamUser;
public SteamContent steamContent;
readonly SteamApps steamApps;
readonly SteamCloud steamCloud;
readonly SteamUnifiedMessages.UnifiedService<IPublishedFile> steamPublishedFile;
readonly PublishedFile steamPublishedFile;
readonly CallbackManager callbacks;
readonly bool authenticatedUser;
bool bConnected;
bool bConnecting;
bool bAborted;
bool bExpectingDisconnectRemote;
bool bDidDisconnect;
bool bDidReceiveLoginKey;
bool bIsConnectionRecovery;
int connectionBackoff;
int seq; // more hack fixes
DateTime connectTime;
AuthSession authSession;
readonly CancellationTokenSource abortedToken = new();
// input
readonly SteamUser.LogOnDetails logonDetails;
// output
readonly Credentials credentials;
static readonly TimeSpan STEAM3_TIMEOUT = TimeSpan.FromSeconds(30);
public Steam3Session(SteamUser.LogOnDetails details)
{
this.logonDetails = details;
this.authenticatedUser = details.Username != null;
this.credentials = new Credentials();
this.bConnected = false;
this.bConnecting = false;
this.bAborted = false;
this.bExpectingDisconnectRemote = false;
this.bDidDisconnect = false;
this.bDidReceiveLoginKey = false;
this.seq = 0;
this.AppTokens = new Dictionary<uint, ulong>();
this.PackageTokens = new Dictionary<uint, ulong>();
this.DepotKeys = new Dictionary<uint, byte[]>();
this.CDNAuthTokens = new ConcurrentDictionary<string, TaskCompletionSource<SteamApps.CDNAuthTokenCallback>>();
this.AppInfo = new Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo>();
this.PackageInfo = new Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo>();
this.AppBetaPasswords = new Dictionary<string, byte[]>();
this.authenticatedUser = details.Username != null || ContentDownloader.Config.UseQrCode;
var clientConfiguration = SteamConfiguration.Create(config =>
config
.WithHttpClientFactory(HttpClientFactory.CreateHttpClient)
.WithHttpClientFactory(static purpose => HttpClientFactory.CreateHttpClient())
);
this.steamClient = new SteamClient(clientConfiguration);
@ -101,7 +73,7 @@ namespace DepotDownloader
this.steamApps = this.steamClient.GetHandler<SteamApps>();
this.steamCloud = this.steamClient.GetHandler<SteamCloud>();
var steamUnifiedMessages = this.steamClient.GetHandler<SteamUnifiedMessages>();
this.steamPublishedFile = steamUnifiedMessages.CreateService<IPublishedFile>();
this.steamPublishedFile = steamUnifiedMessages.CreateService<PublishedFile>();
this.steamContent = this.steamClient.GetHandler<SteamContent>();
this.callbacks = new CallbackManager(this.steamClient);
@ -109,35 +81,15 @@ namespace DepotDownloader
this.callbacks.Subscribe<SteamClient.ConnectedCallback>(ConnectedCallback);
this.callbacks.Subscribe<SteamClient.DisconnectedCallback>(DisconnectedCallback);
this.callbacks.Subscribe<SteamUser.LoggedOnCallback>(LogOnCallback);
this.callbacks.Subscribe<SteamUser.SessionTokenCallback>(SessionTokenCallback);
this.callbacks.Subscribe<SteamApps.LicenseListCallback>(LicenseListCallback);
this.callbacks.Subscribe<SteamUser.UpdateMachineAuthCallback>(UpdateMachineAuthCallback);
this.callbacks.Subscribe<SteamUser.LoginKeyCallback>(LoginKeyCallback);
Console.Write("Connecting to Steam3...");
if (authenticatedUser)
{
var fi = new FileInfo(String.Format("{0}.sentryFile", logonDetails.Username));
if (AccountSettingsStore.Instance.SentryData != null && AccountSettingsStore.Instance.SentryData.ContainsKey(logonDetails.Username))
{
logonDetails.SentryFileHash = Util.SHAHash(AccountSettingsStore.Instance.SentryData[logonDetails.Username]);
}
else if (fi.Exists && fi.Length > 0)
{
var sentryData = File.ReadAllBytes(fi.FullName);
logonDetails.SentryFileHash = Util.SHAHash(sentryData);
AccountSettingsStore.Instance.SentryData[logonDetails.Username] = sentryData;
AccountSettingsStore.Save();
}
}
Connect();
}
public delegate bool WaitCondition();
private readonly object steamLock = new object();
private readonly Lock steamLock = new();
public bool WaitUntilCallback(Action submitter, WaitCondition waiter)
{
@ -153,7 +105,7 @@ namespace DepotDownloader
{
lock (steamLock)
{
WaitForCallbacks();
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
}
} while (!bAborted && this.seq == seq && !waiter());
}
@ -161,46 +113,61 @@ namespace DepotDownloader
return bAborted;
}
public Credentials WaitForCredentials()
public bool WaitForCredentials()
{
if (credentials.IsValid || bAborted)
return credentials;
if (IsLoggedOn || bAborted)
return IsLoggedOn;
WaitUntilCallback(() => { }, () => { return credentials.IsValid; });
WaitUntilCallback(() => { }, () => IsLoggedOn);
return credentials;
return IsLoggedOn;
}
public void RequestAppInfo(uint appId, bool bForce = false)
public async Task TickCallbacks()
{
if ((AppInfo.ContainsKey(appId) && !bForce) || bAborted)
return;
var token = abortedToken.Token;
var completed = false;
Action<SteamApps.PICSTokensCallback> cbMethodTokens = appTokens =>
try
{
completed = true;
if (appTokens.AppTokensDenied.Contains(appId))
while (!token.IsCancellationRequested)
{
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
await callbacks.RunWaitCallbackAsync(token);
}
}
catch (OperationCanceledException)
{
//
}
}
foreach (var token_dict in appTokens.AppTokens)
{
this.AppTokens[token_dict.Key] = token_dict.Value;
}
};
public async Task RequestAppInfo(uint appId, bool bForce = false)
{
if ((AppInfo.ContainsKey(appId) && !bForce) || bAborted)
return;
var appTokens = await steamApps.PICSGetAccessTokens([appId], []);
WaitUntilCallback(() =>
if (appTokens.AppTokensDenied.Contains(appId))
{
callbacks.Subscribe(steamApps.PICSGetAccessTokens(new List<uint> { appId }, new List<uint>()), cbMethodTokens);
}, () => { return completed; });
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
}
completed = false;
Action<SteamApps.PICSProductInfoCallback> cbMethod = appInfo =>
foreach (var token_dict in appTokens.AppTokens)
{
completed = !appInfo.ResponsePending;
this.AppTokens[token_dict.Key] = token_dict.Value;
}
var request = new SteamApps.PICSRequest(appId);
if (AppTokens.TryGetValue(appId, out var token))
{
request.AccessToken = token;
}
var appInfoMultiple = await steamApps.PICSGetProductInfo([request], []);
foreach (var appInfo in appInfoMultiple.Results)
{
foreach (var app_value in appInfo.Apps)
{
var app = app_value.Value;
@ -213,45 +180,17 @@ namespace DepotDownloader
{
AppInfo[app] = null;
}
};
var request = new SteamApps.PICSRequest(appId);
if (AppTokens.ContainsKey(appId))
{
request.AccessToken = AppTokens[appId];
}
WaitUntilCallback(() =>
{
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest> { request }, new List<SteamApps.PICSRequest>()), cbMethod);
}, () => { return completed; });
}
public void RequestPackageInfo(IEnumerable<uint> packageIds)
public async Task RequestPackageInfo(IEnumerable<uint> packageIds)
{
var packages = packageIds.ToList();
packages.RemoveAll(pid => PackageInfo.ContainsKey(pid));
packages.RemoveAll(PackageInfo.ContainsKey);
if (packages.Count == 0 || bAborted)
return;
var completed = false;
Action<SteamApps.PICSProductInfoCallback> cbMethod = packageInfo =>
{
completed = !packageInfo.ResponsePending;
foreach (var package_value in packageInfo.Packages)
{
var package = package_value.Value;
PackageInfo[package.ID] = package;
}
foreach (var package in packageInfo.UnknownPackages)
{
PackageInfo[package] = null;
}
};
var packageRequests = new List<SteamApps.PICSRequest>();
foreach (var package in packages)
@ -266,55 +205,53 @@ namespace DepotDownloader
packageRequests.Add(request);
}
WaitUntilCallback(() =>
var packageInfoMultiple = await steamApps.PICSGetProductInfo([], packageRequests);
foreach (var packageInfo in packageInfoMultiple.Results)
{
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest>(), packageRequests), cbMethod);
}, () => { return completed; });
foreach (var package_value in packageInfo.Packages)
{
var package = package_value.Value;
PackageInfo[package.ID] = package;
}
foreach (var package in packageInfo.UnknownPackages)
{
PackageInfo[package] = null;
}
}
}
public bool RequestFreeAppLicense(uint appId)
public async Task<bool> RequestFreeAppLicense(uint appId)
{
var success = false;
var completed = false;
Action<SteamApps.FreeLicenseCallback> cbMethod = resultInfo =>
try
{
completed = true;
success = resultInfo.GrantedApps.Contains(appId);
};
var resultInfo = await steamApps.RequestFreeLicense(appId);
WaitUntilCallback(() =>
return resultInfo.GrantedApps.Contains(appId);
}
catch (Exception ex)
{
callbacks.Subscribe(steamApps.RequestFreeLicense(appId), cbMethod);
}, () => { return completed; });
return success;
Console.WriteLine($"Failed to request FreeOnDemand license for app {appId}: {ex.Message}");
return false;
}
}
public void RequestDepotKey(uint depotId, uint appid = 0)
public async Task RequestDepotKey(uint depotId, uint appid = 0)
{
if (DepotKeys.ContainsKey(depotId) || bAborted)
return;
var completed = false;
Action<SteamApps.DepotKeyCallback> cbMethod = depotKey =>
{
completed = true;
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
if (depotKey.Result != EResult.OK)
{
Abort();
return;
}
var depotKey = await steamApps.GetDepotDecryptionKey(depotId, appid);
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
};
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
WaitUntilCallback(() =>
if (depotKey.Result != EResult.OK)
{
callbacks.Subscribe(steamApps.GetDepotDecryptionKey(depotId, appid), cbMethod);
}, () => { return completed; });
return;
}
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
}
@ -325,93 +262,105 @@ namespace DepotDownloader
var requestCode = await steamContent.GetManifestRequestCode(depotId, appId, manifestId, branch);
Console.WriteLine("Got manifest request code for {0} {1} result: {2}",
depotId, manifestId,
requestCode);
if (requestCode == 0)
{
Console.WriteLine($"No manifest request code was returned for depot {depotId} from app {appId}, manifest {manifestId}");
if (!authenticatedUser)
{
Console.WriteLine("Suggestion: Try logging in with -username as old manifests may not be available for anonymous accounts.");
}
}
else
{
Console.WriteLine($"Got manifest request code for depot {depotId} from app {appId}, manifest {manifestId}, result: {requestCode}");
}
return requestCode;
}
public void CheckAppBetaPassword(uint appid, string password)
public async Task RequestCDNAuthToken(uint appid, uint depotid, Server server)
{
var completed = false;
Action<SteamApps.CheckAppBetaPasswordCallback> cbMethod = appPassword =>
var cdnKey = (depotid, server.Host);
var completion = new TaskCompletionSource<SteamContent.CDNAuthToken>();
if (bAborted || !CDNAuthTokens.TryAdd(cdnKey, completion))
{
completed = true;
return;
}
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
DebugLog.WriteLine(nameof(Steam3Session), $"Requesting CDN auth token for {server.Host}");
foreach (var entry in appPassword.BetaPasswords)
{
AppBetaPasswords[entry.Key] = entry.Value;
}
};
var cdnAuth = await steamContent.GetCDNAuthToken(appid, depotid, server.Host);
Console.WriteLine($"Got CDN auth token for {server.Host} result: {cdnAuth.Result} (expires {cdnAuth.Expiration})");
WaitUntilCallback(() =>
if (cdnAuth.Result != EResult.OK)
{
callbacks.Subscribe(steamApps.CheckAppBetaPassword(appid, password), cbMethod);
}, () => { return completed; });
return;
}
completion.TrySetResult(cdnAuth);
}
public PublishedFileDetails GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
public async Task CheckAppBetaPassword(uint appid, string password)
{
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
pubFileRequest.publishedfileids.Add(pubFile);
var appPassword = await steamApps.CheckAppBetaPassword(appid, password);
var completed = false;
PublishedFileDetails details = null;
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
Action<SteamUnifiedMessages.ServiceMethodResponse> cbMethod = callback =>
foreach (var entry in appPassword.BetaPasswords)
{
completed = true;
if (callback.Result == EResult.OK)
{
var response = callback.GetDeserializedResponse<CPublishedFile_GetDetails_Response>();
details = response.publishedfiledetails.FirstOrDefault();
}
else
{
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving file details for pubfile {pubFile}.");
}
};
AppBetaPasswords[entry.Key] = entry.Value;
}
}
WaitUntilCallback(() =>
public async Task<KeyValue> GetPrivateBetaDepotSection(uint appid, string branch)
{
if (!AppBetaPasswords.TryGetValue(branch, out var branchPassword)) // Should be filled by CheckAppBetaPassword
{
callbacks.Subscribe(steamPublishedFile.SendMessage(api => api.GetDetails(pubFileRequest)), cbMethod);
}, () => { return completed; });
return new KeyValue();
}
return details;
}
AppTokens.TryGetValue(appid, out var accessToken); // Should be filled by RequestAppInfo
var privateBeta = await steamApps.PICSGetPrivateBeta(appid, accessToken, branch, branchPassword);
Console.WriteLine($"Retrieved private beta depot section for {appid} with result: {privateBeta.Result}");
public SteamCloud.UGCDetailsCallback GetUGCDetails(UGCHandle ugcHandle)
return privateBeta.DepotSection;
}
public async Task<PublishedFileDetails> GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
{
var completed = false;
SteamCloud.UGCDetailsCallback details = null;
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
pubFileRequest.publishedfileids.Add(pubFile);
Action<SteamCloud.UGCDetailsCallback> cbMethod = callback =>
var details = await steamPublishedFile.GetDetails(pubFileRequest);
if (details.Result == EResult.OK)
{
completed = true;
if (callback.Result == EResult.OK)
{
details = callback;
}
else if (callback.Result == EResult.FileNotFound)
{
details = null;
}
else
{
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
}
};
return details.Body.publishedfiledetails.FirstOrDefault();
}
WaitUntilCallback(() =>
throw new Exception($"EResult {(int)details.Result} ({details.Result}) while retrieving file details for pubfile {pubFile}.");
}
public async Task<SteamCloud.UGCDetailsCallback> GetUGCDetails(UGCHandle ugcHandle)
{
var callback = await steamCloud.RequestUGCDetails(ugcHandle);
if (callback.Result == EResult.OK)
{
return callback;
}
else if (callback.Result == EResult.FileNotFound)
{
callbacks.Subscribe(steamCloud.RequestUGCDetails(ugcHandle), cbMethod);
}, () => { return completed; });
return null;
}
return details;
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
}
private void ResetConnectionFlags()
@ -419,19 +368,16 @@ namespace DepotDownloader
bExpectingDisconnectRemote = false;
bDidDisconnect = false;
bIsConnectionRecovery = false;
bDidReceiveLoginKey = false;
}
void Connect()
{
bAborted = false;
bConnected = false;
bConnecting = true;
connectionBackoff = 0;
authSession = null;
ResetConnectionFlags();
this.connectTime = DateTime.Now;
this.steamClient.Connect();
}
@ -448,11 +394,13 @@ namespace DepotDownloader
}
bAborted = true;
bConnected = false;
bConnecting = false;
bIsConnectionRecovery = false;
abortedToken.Cancel();
steamClient.Disconnect();
Ansi.Progress(Ansi.ProgressState.Hidden);
// flush callbacks until our disconnected event
while (!bDidDisconnect)
{
@ -466,41 +414,15 @@ namespace DepotDownloader
steamClient.Disconnect();
}
public void TryWaitForLoginKey()
{
if (logonDetails.Username == null || !credentials.LoggedOn || !ContentDownloader.Config.RememberPassword) return;
var totalWaitPeriod = DateTime.Now.AddSeconds(3);
while (true)
{
var now = DateTime.Now;
if (now >= totalWaitPeriod) break;
if (bDidReceiveLoginKey) break;
callbacks.RunWaitAllCallbacks(TimeSpan.FromMilliseconds(100));
}
}
private void WaitForCallbacks()
{
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
var diff = DateTime.Now - connectTime;
if (diff > STEAM3_TIMEOUT && !bConnected)
{
Console.WriteLine("Timeout connecting to Steam3.");
Abort();
}
}
private void ConnectedCallback(SteamClient.ConnectedCallback connected)
private async void ConnectedCallback(SteamClient.ConnectedCallback connected)
{
Console.WriteLine(" Done!");
bConnecting = false;
bConnected = true;
// Update our tracking so that we don't time out, even if we need to reconnect multiple times,
// e.g. if the authentication phase takes a while and therefore multiple connections.
connectionBackoff = 0;
if (!authenticatedUser)
{
Console.Write("Logging anonymously into Steam3...");
@ -508,7 +430,119 @@ namespace DepotDownloader
}
else
{
Console.Write("Logging '{0}' into Steam3...", logonDetails.Username);
if (logonDetails.Username != null)
{
Console.WriteLine("Logging '{0}' into Steam3...", logonDetails.Username);
}
if (authSession is null)
{
if (logonDetails.Username != null && logonDetails.Password != null && logonDetails.AccessToken is null)
{
try
{
_ = AccountSettingsStore.Instance.GuardData.TryGetValue(logonDetails.Username, out var guarddata);
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
Username = logonDetails.Username,
Password = logonDetails.Password,
IsPersistentSession = ContentDownloader.Config.RememberPassword,
GuardData = guarddata,
Authenticator = new ConsoleAuthenticator(),
});
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
}
else if (logonDetails.AccessToken is null && ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Logging in with QR code...");
try
{
var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
IsPersistentSession = ContentDownloader.Config.RememberPassword,
});
authSession = session;
// Steam will periodically refresh the challenge url, so we need a new QR code.
session.ChallengeURLChanged = () =>
{
Console.WriteLine();
Console.WriteLine("The QR code has changed:");
DisplayQrCode(session.ChallengeURL);
};
// Draw initial QR code immediately
DisplayQrCode(session.ChallengeURL);
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
}
}
if (authSession != null)
{
try
{
var result = await authSession.PollingWaitForResultAsync();
logonDetails.Username = result.AccountName;
logonDetails.Password = null;
logonDetails.AccessToken = result.RefreshToken;
if (result.NewGuardData != null)
{
AccountSettingsStore.Instance.GuardData[result.AccountName] = result.NewGuardData;
if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine($"Success! Next time you can login with -username {result.AccountName} -remember-password instead of -qr.");
}
}
else
{
AccountSettingsStore.Instance.GuardData.Remove(result.AccountName);
}
AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken;
AccountSettingsStore.Save();
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
authSession = null;
}
steamUser.LogOn(logonDetails);
}
}
@ -517,6 +551,8 @@ namespace DepotDownloader
{
bDidDisconnect = true;
DebugLog.WriteLine(nameof(Steam3Session), $"Disconnected: bIsConnectionRecovery = {bIsConnectionRecovery}, UserInitiated = {disconnected.UserInitiated}, bExpectingDisconnectRemote = {bExpectingDisconnectRemote}");
// When recovering the connection, we want to reconnect even if the remote disconnects us
if (!bIsConnectionRecovery && (disconnected.UserInitiated || bExpectingDisconnectRemote))
{
@ -532,16 +568,18 @@ namespace DepotDownloader
}
else if (!bAborted)
{
connectionBackoff += 1;
if (bConnecting)
{
Console.WriteLine("Connection to Steam failed. Trying again");
Console.WriteLine($"Connection to Steam failed. Trying again (#{connectionBackoff})...");
}
else
{
Console.WriteLine("Lost connection to Steam. Reconnecting");
}
Thread.Sleep(1000 * ++connectionBackoff);
Thread.Sleep(1000 * connectionBackoff);
// Any connection related flags need to be reset here to match the state after Connect
ResetConnectionFlags();
@ -553,14 +591,19 @@ namespace DepotDownloader
{
var isSteamGuard = loggedOn.Result == EResult.AccountLogonDenied;
var is2FA = loggedOn.Result == EResult.AccountLoginDeniedNeedTwoFactor;
var isLoginKey = ContentDownloader.Config.RememberPassword && logonDetails.LoginKey != null && loggedOn.Result == EResult.InvalidPassword;
var isAccessToken = ContentDownloader.Config.RememberPassword && logonDetails.AccessToken != null &&
loggedOn.Result is EResult.InvalidPassword
or EResult.InvalidSignature
or EResult.AccessDenied
or EResult.Expired
or EResult.Revoked;
if (isSteamGuard || is2FA || isLoginKey)
if (isSteamGuard || is2FA || isAccessToken)
{
bExpectingDisconnectRemote = true;
Abort(false);
if (!isLoginKey)
if (!isAccessToken)
{
Console.WriteLine("This account is protected by Steam Guard.");
}
@ -571,25 +614,17 @@ namespace DepotDownloader
{
Console.Write("Please enter your 2 factor auth code from your authenticator app: ");
logonDetails.TwoFactorCode = Console.ReadLine();
} while (String.Empty == logonDetails.TwoFactorCode);
} while (string.Empty == logonDetails.TwoFactorCode);
}
else if (isLoginKey)
else if (isAccessToken)
{
AccountSettingsStore.Instance.LoginKeys.Remove(logonDetails.Username);
AccountSettingsStore.Instance.LoginTokens.Remove(logonDetails.Username);
AccountSettingsStore.Save();
logonDetails.LoginKey = null;
if (ContentDownloader.Config.SuppliedPassword != null)
{
Console.WriteLine("Login key was expired. Connecting with supplied password.");
logonDetails.Password = ContentDownloader.Config.SuppliedPassword;
}
else
{
Console.Write("Login key was expired. Please enter your password: ");
logonDetails.Password = Util.ReadPassword();
}
// TODO: Handle gracefully by falling back to password prompt?
Console.WriteLine($"Access token was rejected ({loggedOn.Result}).");
Abort(false);
return;
}
else
{
@ -634,7 +669,7 @@ namespace DepotDownloader
Console.WriteLine(" Done!");
this.seq++;
credentials.LoggedOn = true;
IsLoggedOn = true;
if (ContentDownloader.Config.CellID == 0)
{
@ -643,12 +678,6 @@ namespace DepotDownloader
}
}
private void SessionTokenCallback(SteamUser.SessionTokenCallback sessionToken)
{
Console.WriteLine("Got session token!");
credentials.SessionToken = sessionToken.SessionToken;
}
private void LicenseListCallback(SteamApps.LicenseListCallback licenseList)
{
if (licenseList.Result != EResult.OK)
@ -671,45 +700,20 @@ namespace DepotDownloader
}
}
private void UpdateMachineAuthCallback(SteamUser.UpdateMachineAuthCallback machineAuth)
private static void DisplayQrCode(string challengeUrl)
{
var hash = Util.SHAHash(machineAuth.Data);
Console.WriteLine("Got Machine Auth: {0} {1} {2} {3}", machineAuth.FileName, machineAuth.Offset, machineAuth.BytesToWrite, machineAuth.Data.Length, hash);
// Encode the link as a QR code
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L);
using var qrCode = new AsciiQRCode(qrCodeData);
var qrCodeAsAsciiArt = qrCode.GetLineByLineGraphic(1, drawQuietZones: true);
AccountSettingsStore.Instance.SentryData[logonDetails.Username] = machineAuth.Data;
AccountSettingsStore.Save();
Console.WriteLine("Use the Steam Mobile App to sign in with this QR code:");
var authResponse = new SteamUser.MachineAuthDetails
foreach (var line in qrCodeAsAsciiArt)
{
BytesWritten = machineAuth.BytesToWrite,
FileName = machineAuth.FileName,
FileSize = machineAuth.BytesToWrite,
Offset = machineAuth.Offset,
SentryFileHash = hash, // should be the sha1 hash of the sentry file we just wrote
OneTimePassword = machineAuth.OneTimePassword, // not sure on this one yet, since we've had no examples of steam using OTPs
LastError = 0, // result from win32 GetLastError
Result = EResult.OK, // if everything went okay, otherwise ~who knows~
JobID = machineAuth.JobID, // so we respond to the correct server job
};
// send off our response
steamUser.SendMachineAuthResponse(authResponse);
}
private void LoginKeyCallback(SteamUser.LoginKeyCallback loginKey)
{
Console.WriteLine("Accepted new login key for account {0}", logonDetails.Username);
AccountSettingsStore.Instance.LoginKeys[logonDetails.Username] = loginKey.LoginKey;
AccountSettingsStore.Save();
steamUser.AcceptNewLoginKey(loginKey);
bDidReceiveLoginKey = true;
Console.WriteLine(line);
}
}
}
}

@ -1,4 +1,7 @@
using System;
// 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.Collections.Generic;
using System.IO;
using System.Linq;
@ -6,6 +9,7 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using SteamKit2;
namespace DepotDownloader
{
@ -28,6 +32,12 @@ namespace DepotDownloader
return "linux";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
// Return linux as freebsd steam client doesn't exist yet
return "linux";
}
return "unknown";
}
@ -69,30 +79,16 @@ namespace DepotDownloader
}
// Validate a file against Steam3 Chunk data
public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata)
public static List<DepotManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, DepotManifest.ChunkData[] chunkdata)
{
var neededChunks = new List<ProtoManifest.ChunkData>();
int read;
var neededChunks = new List<DepotManifest.ChunkData>();
foreach (var data in chunkdata)
{
var chunk = new byte[data.UncompressedLength];
fs.Seek((long)data.Offset, SeekOrigin.Begin);
read = fs.Read(chunk, 0, (int)data.UncompressedLength);
byte[] tempchunk;
if (read < data.UncompressedLength)
{
tempchunk = new byte[read];
Array.Copy(chunk, 0, tempchunk, 0, read);
}
else
{
tempchunk = chunk;
}
var adler = AdlerHash(tempchunk);
if (!adler.SequenceEqual(data.Checksum))
var adler = AdlerHash(fs, (int)data.UncompressedLength);
if (!adler.SequenceEqual(BitConverter.GetBytes(data.Checksum)))
{
neededChunks.Add(data);
}
@ -101,28 +97,114 @@ namespace DepotDownloader
return neededChunks;
}
public static byte[] AdlerHash(byte[] input)
public static byte[] AdlerHash(Stream stream, int length)
{
uint a = 0, b = 0;
for (var i = 0; i < input.Length; i++)
for (var i = 0; i < length; i++)
{
a = (a + input[i]) % 65521;
var c = (uint)stream.ReadByte();
a = (a + c) % 65521;
b = (b + a) % 65521;
}
return BitConverter.GetBytes(a | (b << 16));
}
public static byte[] SHAHash(byte[] input)
public static byte[] FileSHAHash(string filename)
{
using (var fs = File.Open(filename, FileMode.Open))
using (var sha = SHA1.Create())
{
var output = sha.ComputeHash(input);
var output = sha.ComputeHash(fs);
return output;
}
}
public static DepotManifest LoadManifestFromFile(string directory, uint depotId, ulong manifestId, bool badHashWarning)
{
// Try loading Steam format manifest first.
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
var currentChecksum = FileSHAHash(filename);
if (expectedChecksum != null && expectedChecksum.SequenceEqual(currentChecksum))
{
return DepotManifest.LoadFromFile(filename);
}
else if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
// Try converting legacy manifest format.
filename = Path.Combine(directory, string.Format("{0}_{1}.bin", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
byte[] currentChecksum;
var oldManifest = ProtoManifest.LoadFromFile(filename, out currentChecksum);
if (oldManifest != null && (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum)))
{
oldManifest = null;
if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
if (oldManifest != null)
{
return oldManifest.ConvertToSteamManifest(depotId);
}
}
return null;
}
public static bool SaveManifestToFile(string directory, DepotManifest manifest)
{
try
{
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", manifest.DepotID, manifest.ManifestGID));
manifest.SaveToFile(filename);
File.WriteAllBytes(filename + ".sha", FileSHAHash(filename));
return true; // If serialization completes without throwing an exception, return true
}
catch (Exception)
{
return false; // Return false if an error occurs
}
}
public static byte[] DecodeHexString(string hex)
{
if (hex == null)
@ -137,43 +219,21 @@ namespace DepotDownloader
return bytes;
}
public static string EncodeHexString(byte[] input)
{
return input.Aggregate(new StringBuilder(),
(sb, v) => sb.Append(v.ToString("x2"))
).ToString();
}
public static async Task InvokeAsync(IEnumerable<Func<Task>> taskFactories, int maxDegreeOfParallelism)
/// <summary>
/// Decrypts using AES/ECB/PKCS7
/// </summary>
public static byte[] SymmetricDecryptECB(byte[] input, byte[] key)
{
if (taskFactories == null) throw new ArgumentNullException(nameof(taskFactories));
if (maxDegreeOfParallelism <= 0) throw new ArgumentException(nameof(maxDegreeOfParallelism));
var queue = taskFactories.ToArray();
if (queue.Length == 0)
{
return;
}
var tasksInFlight = new List<Task>(maxDegreeOfParallelism);
var index = 0;
do
{
while (tasksInFlight.Count < maxDegreeOfParallelism && index < queue.Length)
{
var taskFactory = queue[index++];
tasksInFlight.Add(taskFactory());
}
var completedTask = await Task.WhenAny(tasksInFlight).ConfigureAwait(false);
using var aes = Aes.Create();
aes.BlockSize = 128;
aes.KeySize = 256;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
await completedTask.ConfigureAwait(false);
using var aesTransform = aes.CreateDecryptor(key, null);
var output = aesTransform.TransformFinalBlock(input, 0, input.Length);
tasksInFlight.Remove(completedTask);
} while (index < queue.Length || tasksInFlight.Count != 0);
return output;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,16 @@
<?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>

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,59 +1,114 @@
DepotDownloader
===============
Steam depot downloader utilizing the SteamKit2 library. Supports .NET 6.0
Steam depot downloader utilizing the SteamKit2 library. Supports .NET 8.0
### Downloading one or all depots for an app
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
```
dotnet DepotDownloader.dll -app <id> [-depot <id> [-manifest <id>]]
[-username <username> [-password <password>]] [other options]
### 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
```
For example: `dotnet DepotDownloader.dll -app 730 -depot 731 -manifest 7617088375292372759`
## Usage
### Downloading a workshop item using pubfile id
### Downloading one or all depots for an app
```powershell
./DepotDownloader -app <id> [-depot <id> [-manifest <id>]]
[-username <username> [-password <password>]] [other options]
```
dotnet DepotDownloader.dll -app <id> -pubfile <id> [-username <username> [-password <password>]]
For example: `./DepotDownloader -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
```powershell
./DepotDownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]
```
For example: `dotnet DepotDownloader.dll -app 730 -pubfile 1885082371`
For example: `./DepotDownloader -app 730 -pubfile 1885082371`
### Downloading a workshop item using ugc id
```
dotnet DepotDownloader.dll -app <id> -ugc <id> [-username <username> [-password <password>]]
```powershell
./DepotDownloader -app <id> -ugc <id> [-username <username> [-password <password>]]
```
For example: `dotnet DepotDownloader.dll -app 730 -ugc 770604181014286929`
For example: `./DepotDownloader -app 730 -ugc 770604181014286929`
## Parameters
Parameter | Description
--------- | -----------
-app \<#> | the AppID to download.
-depot \<#> | the DepotID to download.
-manifest \<id> | manifest id of content to download (requires -depot, default: current for branch).
-ugc \<#> | the UGC ID to download.
-beta \<branchname> | download from specified branch if available (default: Public).
-betapassword \<pass> | branch password if applicable.
-all-platforms | downloads all platform-specific depots when -app is used.
-os \<os> | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
-osarch \<arch> | the architecture for which to download the game (32 or 64, default: the host's architecture)
-all-languages | download all language-specific depots when -app is used.
-language \<lang> | the language for which to download the game (default: english)
-lowviolence | download low violence depots when -app is used.
-pubfile \<#> | the PublishedFileId to download. (Will automatically resolve to UGC id)
-username \<user> | the username of the account to login to for restricted content.
-password \<pass> | the password of the account to login to for restricted content.
-remember-password | if set, remember the password for subsequent logins of this user. (Use -username <username> -remember-password as login credentials)
-dir \<installdir> | the directory in which to place downloaded files.
-filelist \<file.txt> | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex.
-validate | Include checksum verification of files already downloaded
-manifest-only | downloads a human readable manifest for any depots that would be downloaded.
-cellid \<#> | the overridden CellID of the content server to download from.
-max-servers \<#> | maximum number of content servers to use. (default: 20).
-max-downloads \<#> | maximum number of chunks to download concurrently. (default: 8).
-loginid \<#> | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
#### Authentication
Parameter | Description
----------------------- | -----------
`-username <user>` | the username of the account to login to for restricted content.
`-password <pass>` | the password of the account to login to for restricted content.
`-remember-password` | if set, remember the password for subsequent logins of this user. (Use `-username <username> -remember-password` as login credentials)
`-qr` | display a login QR code to be scanned with the Steam mobile app
`-no-mobile` | prefer entering a 2FA code instead of prompting to accept in the Steam mobile app.
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
#### Downloading
Parameter | Description
------------------------ | -----------
`-app <#>` | the AppID to download.
`-depot <#>` | the DepotID to download.
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch).
`-ugc <#>` | the UGC ID to download.
`-pubfile <#>` | the PublishedFileId to download. (Will automatically resolve to UGC id)
`-branch <branchname>` | download from specified branch if available (default: Public).
`-branchpassword <pass>` | branch password if applicable.
#### Download configuration
Parameter | Description
----------------------- | -----------
`-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
@ -65,3 +120,12 @@ Any connection to Steam will be closed if they share a LoginID. You can specify
### Why doesn't my password containing special characters work? Do I have to specify the password on the command line?
If you pass the `-password` parameter with a password that contains special characters, you will need to escape the command appropriately for the shell you are using. You do not have to include the `-password` parameter on the command line as long as you include a `-username`. You will be prompted to enter your password interactively.
### I am getting error 401 or no manifest code returned for old manifest ids
Try logging in with a Steam account, this may happen when using anonymous account.
Steam allows developers to block downloading old manifests, in which case no manifest code is returned even when parameters appear correct.
### Why am I getting slow download speeds and frequent connection timeouts?
When downloading old builds, cache server may not have the chunks readily available which makes downloading slower.
Try increasing `-max-downloads` to saturate the network more.

@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestMinor"
}
}
Loading…
Cancel
Save