diff --git a/DepotDownloader/ContentDownloader.cs b/DepotDownloader/ContentDownloader.cs index caa50eba..16f8004d 100644 --- a/DepotDownloader/ContentDownloader.cs +++ b/DepotDownloader/ContentDownloader.cs @@ -47,30 +47,120 @@ namespace DepotDownloader public byte[] DepotKey { get; } = depotKey; } - static bool CreateDirectories(uint depotId, uint depotVersion, out string installDir) + static bool CreateDirectories(uint depotId, ulong depotVersion, out string installDir) { installDir = null; try { + if (Config.AppId == 0 || Config.AppId == INVALID_APP_ID) + { + return false; + } + + uint appId = Config.AppId; + if (string.IsNullOrWhiteSpace(Config.InstallDirectory)) { Directory.CreateDirectory(DEFAULT_DOWNLOAD_DIR); - var depotPath = Path.Combine(DEFAULT_DOWNLOAD_DIR, depotId.ToString()); - Directory.CreateDirectory(depotPath); + if (Config.UsePubOrUgcDirectories) + { + var appPath = Path.Combine(DEFAULT_DOWNLOAD_DIR, appId.ToString()); + Directory.CreateDirectory(appPath); + + if (Config.PublishedFileId != 0) + { + var pubPath = Path.Combine(appPath, Config.PublishedFileId.ToString()); + Directory.CreateDirectory(pubPath); + + installDir = Path.Combine(pubPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + else if (Config.UgcId != 0) + { + var ugcPath = Path.Combine(appPath, Config.UgcId.ToString()); + Directory.CreateDirectory(ugcPath); - installDir = Path.Combine(depotPath, depotVersion.ToString()); - Directory.CreateDirectory(installDir); + installDir = Path.Combine(ugcPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + else + { + // No publishedFileId or ugcId + var depotPath = Path.Combine(appPath, depotId.ToString()); + Directory.CreateDirectory(depotPath); + installDir = Path.Combine(depotPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + } + else + { + // No pub/ugc directories, just create app/depot/version structure + var appPath = Path.Combine(DEFAULT_DOWNLOAD_DIR, appId.ToString()); + Directory.CreateDirectory(appPath); + + var depotPath = Path.Combine(appPath, depotId.ToString()); + Directory.CreateDirectory(depotPath); + + installDir = Path.Combine(depotPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + + // Config and staging directories Directory.CreateDirectory(Path.Combine(installDir, CONFIG_DIR)); Directory.CreateDirectory(Path.Combine(installDir, STAGING_DIR)); } else { + // Using custom installation directory provided via -dir Directory.CreateDirectory(Config.InstallDirectory); - installDir = Config.InstallDirectory; + if (Config.UsePubOrUgcDirectories) + { + var appPath = Path.Combine(Config.InstallDirectory, appId.ToString()); + Directory.CreateDirectory(appPath); + if (Config.PublishedFileId != 0) + { + var pubPath = Path.Combine(appPath, Config.PublishedFileId.ToString()); + Directory.CreateDirectory(pubPath); + + installDir = Path.Combine(pubPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + else if (Config.UgcId != 0) + { + var ugcPath = Path.Combine(appPath, Config.UgcId.ToString()); + Directory.CreateDirectory(ugcPath); + + installDir = Path.Combine(ugcPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + else + { + // No publishedFileId or ugcId + var depotPath = Path.Combine(appPath, depotId.ToString()); + Directory.CreateDirectory(depotPath); + + installDir = Path.Combine(depotPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + } + else + { + // Just create an app/depot/version hierarchy under the custom dir + var appPath = Path.Combine(Config.InstallDirectory, appId.ToString()); + Directory.CreateDirectory(appPath); + + var depotPath = Path.Combine(appPath, depotId.ToString()); + Directory.CreateDirectory(depotPath); + + installDir = Path.Combine(depotPath, depotVersion.ToString()); + Directory.CreateDirectory(installDir); + } + + // Config and staging directories Directory.CreateDirectory(Path.Combine(installDir, CONFIG_DIR)); Directory.CreateDirectory(Path.Combine(installDir, STAGING_DIR)); } @@ -83,6 +173,8 @@ namespace DepotDownloader return true; } + + static bool TestIsFileIncluded(string filename) { if (!Config.UsingFileList) @@ -343,6 +435,11 @@ namespace DepotDownloader { var details = await steam3.GetPublishedFileDetails(appId, publishedFileId); + // Set the current appId and publishedFileId in the config + Config.AppId = appId; // Make sure AppId is a field in Config as well + Config.PublishedFileId = publishedFileId; + Config.UgcId = 0; // Reset ugcId + if (!string.IsNullOrEmpty(details?.file_url)) { await DownloadWebFile(appId, details.filename, details.file_url); @@ -355,12 +452,21 @@ namespace DepotDownloader { Console.WriteLine("Unable to locate manifest ID for published file {0}", publishedFileId); } + + // Reset after download if you prefer a clean slate + Config.PublishedFileId = 0; } + public static async Task DownloadUGCAsync(uint appId, ulong ugcId) { SteamCloud.UGCDetailsCallback details = null; + // Set the current appId and ugcId in the config + Config.AppId = appId; + Config.UgcId = ugcId; + Config.PublishedFileId = 0; // Reset publishedFileId + if (steam3.steamUser.SteamID.AccountType != EAccountType.AnonUser) { details = await steam3.GetUGCDetails(ugcId); @@ -378,11 +484,15 @@ namespace DepotDownloader { await DownloadAppAsync(appId, [(appId, ugcId)], DEFAULT_BRANCH, null, null, null, false, true); } + + // Reset after download if you prefer a clean slate + Config.UgcId = 0; } + private static async Task DownloadWebFile(uint appId, string fileName, string url) { - if (!CreateDirectories(appId, 0, out var installDir)) + if (!CreateDirectories(0, 0, out var installDir)) { Console.WriteLine("Error: Unable to create install directories!"); return; @@ -416,14 +526,17 @@ namespace DepotDownloader cdnPool = new CDNClientPool(steam3, appId); // Load our configuration data containing the depots currently installed - var configPath = Config.InstallDirectory; - if (string.IsNullOrWhiteSpace(configPath)) + if (DepotConfigStore.Instance == null) { - configPath = DEFAULT_DOWNLOAD_DIR; - } + var configPath = Config.InstallDirectory; + if (string.IsNullOrWhiteSpace(configPath)) + { + configPath = DEFAULT_DOWNLOAD_DIR; + } - Directory.CreateDirectory(Path.Combine(configPath, CONFIG_DIR)); - DepotConfigStore.LoadFromFile(Path.Combine(configPath, CONFIG_DIR, "depot.config")); + Directory.CreateDirectory(Path.Combine(configPath, CONFIG_DIR)); + DepotConfigStore.LoadFromFile(Path.Combine(configPath, CONFIG_DIR, "depot.config")); + } await steam3?.RequestAppInfo(appId); @@ -487,8 +600,27 @@ namespace DepotDownloader !string.IsNullOrWhiteSpace(depotConfig["oslist"].Value)) { var oslist = depotConfig["oslist"].Value.Split(','); - if (Array.IndexOf(oslist, os ?? Util.GetSteamOS()) == -1) + + // Special case: empty oslist and appId + 1 + if (oslist.Length == 0 && id == appId + 1) + { + Console.WriteLine($"Warning: Depot {id} has an empty oslist and is being downloaded."); + depotIdsFound.Add(id); + depotManifestIds.Add((id, INVALID_MANIFEST_ID)); + continue; + } + + var osMatches = os != null + ? oslist.Contains(os, StringComparer.OrdinalIgnoreCase) + : oslist.Contains(Util.GetSteamOS(), StringComparer.OrdinalIgnoreCase); + + // If no match, skip this depot + if (!osMatches) + { + Console.WriteLine($"No matching OS found for depot {depotSection.Name} under AppID: {appId}."); continue; + } + } if (!Config.DownloadAllArchs && @@ -595,9 +727,8 @@ namespace DepotDownloader return null; } - var uVersion = GetSteam3AppBuildNumber(appId, branch); - - if (!CreateDirectories(depotId, uVersion, out var installDir)) + Config.AppId = appId; + if (!CreateDirectories(depotId, manifestId, out var installDir)) { Console.WriteLine("Error: Unable to create install directories!"); return null; diff --git a/DepotDownloader/DownloadConfig.cs b/DepotDownloader/DownloadConfig.cs index 4985c446..d9d368a1 100644 --- a/DepotDownloader/DownloadConfig.cs +++ b/DepotDownloader/DownloadConfig.cs @@ -31,5 +31,12 @@ namespace DepotDownloader public uint? LoginID { get; set; } public bool UseQrCode { get; set; } + + public ulong PublishedFileId { get; set; } = 0; + public ulong UgcId { get; set; } = 0; + public uint AppId { get; set; } = 0; + + // Helper property to determine if we're using pubfile/ugc directories + public bool UsePubOrUgcDirectories => (PublishedFileId != 0 || UgcId != 0); } } diff --git a/DepotDownloader/Program.cs b/DepotDownloader/Program.cs index 89130b19..6f0f2092 100644 --- a/DepotDownloader/Program.cs +++ b/DepotDownloader/Program.cs @@ -155,24 +155,36 @@ namespace DepotDownloader #endregion - var appId = GetParameter(args, "-app", ContentDownloader.INVALID_APP_ID); - if (appId == ContentDownloader.INVALID_APP_ID) + var appIdList = GetParameterList(args, "-app"); + if (appIdList.Count == 0) { Console.WriteLine("Error: -app not specified!"); return 1; } - var pubFile = GetParameter(args, "-pubfile", ContentDownloader.INVALID_MANIFEST_ID); - var ugcId = GetParameter(args, "-ugc", ContentDownloader.INVALID_MANIFEST_ID); - if (pubFile != ContentDownloader.INVALID_MANIFEST_ID) + + var pubFileList = GetParameterList(args, "-pubfile"); + var ugcIdList = GetParameterList(args, "-ugc"); + + if (pubFileList.Count > 0) { - #region Pubfile Downloading + if (pubFileList.Count != appIdList.Count) + { + Console.WriteLine("Error: Number of -pubfile arguments does not match number of -app arguments."); + return 1; + } if (InitializeSteam(username, password)) { try { - await ContentDownloader.DownloadPubfileAsync(appId, pubFile).ConfigureAwait(false); + for (var i = 0; i < appIdList.Count; i++) + { + var appId = appIdList[i]; + var pubFile = pubFileList[i]; + + await ContentDownloader.DownloadPubfileAsync(appId, pubFile).ConfigureAwait(false); + } } catch (Exception ex) when ( ex is ContentDownloaderException @@ -183,7 +195,7 @@ namespace DepotDownloader } catch (Exception e) { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); + Console.WriteLine("Download failed due to an unhandled exception: {0}", e.Message); throw; } finally @@ -196,18 +208,27 @@ namespace DepotDownloader Console.WriteLine("Error: InitializeSteam failed"); return 1; } - - #endregion } - else if (ugcId != ContentDownloader.INVALID_MANIFEST_ID) + + else if (ugcIdList.Count > 0) { - #region UGC Downloading + if (ugcIdList.Count != appIdList.Count) + { + Console.WriteLine("Error: Number of -ugc arguments does not match number of -app arguments."); + return 1; + } if (InitializeSteam(username, password)) { try { - await ContentDownloader.DownloadUGCAsync(appId, ugcId).ConfigureAwait(false); + for (var i = 0; i < appIdList.Count; i++) + { + var appId = appIdList[i]; + var ugcId = ugcIdList[i]; + + await ContentDownloader.DownloadUGCAsync(appId, ugcId).ConfigureAwait(false); + } } catch (Exception ex) when ( ex is ContentDownloaderException @@ -218,7 +239,7 @@ namespace DepotDownloader } catch (Exception e) { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); + Console.WriteLine("Download failed due to an unhandled exception: {0}", e.Message); throw; } finally @@ -231,9 +252,8 @@ namespace DepotDownloader Console.WriteLine("Error: InitializeSteam failed"); return 1; } - - #endregion } + else { #region App downloading @@ -272,45 +292,59 @@ namespace DepotDownloader var lv = HasParameter(args, "-lowviolence"); - var depotManifestIds = new List<(uint, ulong)>(); - var isUGC = false; var depotIdList = GetParameterList(args, "-depot"); var manifestIdList = GetParameterList(args, "-manifest"); - if (manifestIdList.Count > 0) - { - if (depotIdList.Count != manifestIdList.Count) - { - Console.WriteLine("Error: -manifest requires one id for every -depot specified"); - return 1; - } - var zippedDepotManifest = depotIdList.Zip(manifestIdList, (depotId, manifestId) => (depotId, manifestId)); - depotManifestIds.AddRange(zippedDepotManifest); - } - else - { - depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); - } if (InitializeSteam(username, password)) { try { - await ContentDownloader.DownloadAppAsync(appId, depotManifestIds, branch, os, arch, language, lv, isUGC).ConfigureAwait(false); + foreach (var appId in appIdList) + { + var depotManifestIds = new List<(uint, ulong)>(); + + var isUGC = false; + + + if (manifestIdList.Count > 0) + { + if (depotIdList.Count != manifestIdList.Count) + { + Console.WriteLine("Error: -manifest requires one id for every -depot specified"); + return 1; + } + + var zippedDepotManifest = depotIdList.Zip(manifestIdList, (depotId, manifestId) => (depotId, manifestId)); + depotManifestIds.AddRange(zippedDepotManifest); + } + else + { + depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); + } + + try + { + await ContentDownloader.DownloadAppAsync(appId, depotManifestIds, branch, os, arch, language, lv, isUGC).ConfigureAwait(false); + } + catch (ContentDownloaderException ex) + { + Console.WriteLine($"Warning: {ex.Message}"); + // Continue with the next appId + } + catch (OperationCanceledException ex) + { + Console.WriteLine($"Warning: Operation canceled for AppID {appId}: {ex.Message}"); + // Decide whether to continue or break. Here, we continue. + } + } } - catch (Exception ex) when ( - ex is ContentDownloaderException - || ex is OperationCanceledException) + catch (Exception ex) // Handle other unforeseen exceptions { - Console.WriteLine(ex.Message); + Console.WriteLine("An unexpected error occurred: " + ex.Message); return 1; } - catch (Exception e) - { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); - throw; - } finally { ContentDownloader.ShutdownSteam3(); @@ -411,69 +445,125 @@ namespace DepotDownloader static List GetParameterList(string[] args, string param) { var list = new List(); - var index = IndexOfParam(args, param); - if (index == -1 || index == (args.Length - 1)) + var converter = TypeDescriptor.GetConverter(typeof(T)); + if (converter == null) + { + Console.WriteLine($"Warning: No type converter available for type {typeof(T)}"); return list; + } - index++; - + int index = 0; while (index < args.Length) { - var strParam = args[index]; + // Find the next occurrence of the parameter + if (args[index].Equals(param, StringComparison.OrdinalIgnoreCase)) + { + index++; // Move to the value(s) after the parameter - if (strParam[0] == '-') break; + // Process values following the parameter + while (index < args.Length && !args[index].StartsWith("-")) + { + var strParam = args[index]; + + // Handle space-separated values within a single argument + if (strParam.Contains(" ")) + { + var values = strParam.Split(' ', StringSplitOptions.RemoveEmptyEntries); + foreach (var val in values) + { + try + { + var convertedValue = converter.ConvertFromString(val); + if (convertedValue != null) + { + list.Add((T)convertedValue); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Unable to convert value '{val}' to type {typeof(T)}. Exception: {ex.Message}"); + } + } + } + else + { + try + { + var convertedValue = converter.ConvertFromString(strParam); + if (convertedValue != null) + { + list.Add((T)convertedValue); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Unable to convert value '{strParam}' to type {typeof(T)}. Exception: {ex.Message}"); + } + } - var converter = TypeDescriptor.GetConverter(typeof(T)); - if (converter != null) + index++; + } + } + else { - list.Add((T)converter.ConvertFromString(strParam)); + index++; } - - index++; } return list; } + static void PrintUsage() { // Do not use tabs to align parameters here because tab size may differ Console.WriteLine(); - Console.WriteLine("Usage: downloading one or all depots for an app:"); - Console.WriteLine(" depotdownloader -app [-depot [-manifest ]]"); + Console.WriteLine("Usage: downloading depots for one or more apps:"); + Console.WriteLine(" depotdownloader -app [-depot [-manifest ]]"); Console.WriteLine(" [-username [-password ]] [other options]"); Console.WriteLine(); - Console.WriteLine("Usage: downloading a workshop item using pubfile id"); - Console.WriteLine(" depotdownloader -app -pubfile [-username [-password ]]"); - Console.WriteLine("Usage: downloading a workshop item using ugc id"); - Console.WriteLine(" depotdownloader -app -ugc [-username [-password ]]"); + Console.WriteLine("Examples:"); + Console.WriteLine(" Single list format:"); + Console.WriteLine(" depotdownloader -app \"10 20 30\" -depot \"11 22 33\" -manifest \"1111 2222 3333\""); + Console.WriteLine(); + Console.WriteLine(" Group list format:"); + Console.WriteLine(" depotdownloader -app 10 -depot 11 -manifest 1111 -app 20 -depot 22 -manifest 2222"); + Console.WriteLine(" -app 30 -depot 33 -manifest 3333"); + Console.WriteLine(); + Console.WriteLine("Usage: downloading a workshop item using pubfile id:"); + Console.WriteLine(" depotdownloader -app -pubfile [-username [-password ]]"); + Console.WriteLine("Usage: downloading a workshop item using ugc id:"); + Console.WriteLine(" depotdownloader -app -ugc [-username [-password ]]"); Console.WriteLine(); Console.WriteLine("Parameters:"); - Console.WriteLine(" -app <#> - the AppID to download."); - Console.WriteLine(" -depot <#> - the DepotID to download."); - Console.WriteLine(" -manifest - manifest id of content to download (requires -depot, default: current for branch)."); + Console.WriteLine(" -app <# or \"# # ...\"> - the AppID(s) to download. Provide multiple IDs separated by spaces."); + Console.WriteLine(" -depot <# or \"# # ...\"> - the DepotID(s) to download. Must correspond to the provided AppIDs."); + Console.WriteLine(" -manifest <# or \"# # ...\"> - manifest ID(s) of content to download (requires -depot). Must correspond to the provided DepotIDs."); Console.WriteLine($" -branch - download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH})."); Console.WriteLine(" -branchpassword - 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 - the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)"); - Console.WriteLine(" -osarch - 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 - the language for which to download the game (default: english)"); - Console.WriteLine(" -lowviolence - download low violence depots when -app is used."); + Console.WriteLine(" -all-platforms - downloads all platform-specific depots when -app is used."); + Console.WriteLine(" -all-archs - download all architecture-specific depots when -app is used."); + Console.WriteLine(" -os - the operating system for which to download the game (windows, macos, or linux)."); + Console.WriteLine(" (default: OS the program is currently running on)"); + Console.WriteLine(" -osarch - the architecture for which to download the game (32 or 64)."); + Console.WriteLine(" (default: the host's architecture)"); + Console.WriteLine(" -all-languages - download all language-specific depots when -app is used."); + Console.WriteLine(" -language - the language for which to download the game (default: english)"); + Console.WriteLine(" -lowviolence - download low violence depots when -app is used."); Console.WriteLine(); - Console.WriteLine(" -ugc <#> - the UGC ID to download."); - Console.WriteLine(" -pubfile <#> - the PublishedFileId to download. (Will automatically resolve to UGC id)"); + Console.WriteLine(" -ugc <# or \"# # ...\"> - the UGC ID(s) to download. Must correspond to the provided AppIDs."); + Console.WriteLine(" -pubfile <# or \"# # ...\"> - the PublishedFileId(s) to download. Will automatically resolve to UGC IDs."); Console.WriteLine(); - Console.WriteLine(" -username - the username of the account to login to for restricted content."); - Console.WriteLine(" -password - 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 -remember-password as login credentials."); + Console.WriteLine(" -username - the username of the account to login to for restricted content."); + Console.WriteLine(" -password - 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 -remember-password as login credentials."); Console.WriteLine(); - Console.WriteLine(" -dir - the directory in which to place downloaded files."); - Console.WriteLine(" -filelist - 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(" -dir - the directory in which to place downloaded files."); + Console.WriteLine(" -filelist - 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."); + Console.WriteLine(" Each file path should be on its own line."); Console.WriteLine(); Console.WriteLine(" -validate - include checksum verification of files already downloaded"); Console.WriteLine(" -manifest-only - downloads a human readable manifest for any depots that would be downloaded.");