mirror of
https://github.com/SteamRE/DepotDownloader.git
synced 2026-02-04 05:31:18 +01:00
Add roslyn-like .editorconfig (#236)
This commit is contained in:
229
.editorconfig
229
.editorconfig
@@ -1,7 +1,230 @@
|
||||
; EditorConfig: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Code files
|
||||
[*.{cs, csx, vb, vbx}]
|
||||
indent_size = 4
|
||||
|
||||
# XML project files
|
||||
[*.{csproj, vbproj, vcxproj, vcxproj.filters, proj, projitems, shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# XML config files
|
||||
[*.{props, targets, ruleset, config, nuspec, resx, vsixmanifest, vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs, vb}]
|
||||
|
||||
# IDE0055: Fix formatting
|
||||
dotnet_diagnostic.IDE0055.severity = warning
|
||||
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_separate_import_directive_groups = false
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Non-private static fields are PascalCase
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
|
||||
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
|
||||
|
||||
# Non-private readonly fields are PascalCase
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
|
||||
|
||||
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
|
||||
|
||||
dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
|
||||
|
||||
# Constants are PascalCase
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
|
||||
|
||||
dotnet_naming_symbols.constants.applicable_kinds = field, local
|
||||
dotnet_naming_symbols.constants.required_modifiers = const
|
||||
|
||||
dotnet_naming_style.constant_style.capitalization = pascal_case
|
||||
|
||||
# Static readonly fields are PascalCase
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
|
||||
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static, readonly
|
||||
|
||||
dotnet_naming_style.static_field_style.capitalization = pascal_case
|
||||
|
||||
# Instance fields are camelCase and start with _
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
|
||||
|
||||
dotnet_naming_symbols.instance_fields.applicable_kinds = field
|
||||
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
# Locals and parameters are camelCase
|
||||
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
|
||||
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
|
||||
|
||||
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
|
||||
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
# Local functions are PascalCase
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
|
||||
|
||||
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
||||
|
||||
dotnet_naming_style.local_function_style.capitalization = pascal_case
|
||||
|
||||
# By default, name items with PascalCase
|
||||
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
|
||||
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
|
||||
|
||||
dotnet_naming_symbols.all_members.applicable_kinds = *
|
||||
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
# 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
|
||||
|
||||
# IDE0035: Remove unreachable code
|
||||
dotnet_diagnostic.IDE0035.severity = warning
|
||||
|
||||
# IDE0036: Order modifiers
|
||||
dotnet_diagnostic.IDE0036.severity = warning
|
||||
|
||||
# IDE0043: Format string contains invalid placeholder
|
||||
dotnet_diagnostic.IDE0043.severity = warning
|
||||
|
||||
# IDE0044: Make field readonly
|
||||
dotnet_diagnostic.IDE0044.severity = warning
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
# csharp_new_line_before_members_in_object_initializers = true TODO seems like Rider/ReSharper has the value inverted, uncomment when its fixed
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = false
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a block body
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Blocks are allowed
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using ProtoBuf;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.Linq;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Discovery;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
[ProtoContract]
|
||||
class AccountSettingsStore
|
||||
{
|
||||
[ProtoMember(1, IsRequired=false)]
|
||||
[ProtoMember(1, IsRequired = false)]
|
||||
public Dictionary<string, byte[]> SentryData { get; private set; }
|
||||
|
||||
[ProtoMember(2, IsRequired = false)]
|
||||
public System.Collections.Concurrent.ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; }
|
||||
public ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; }
|
||||
|
||||
[ProtoMember(3, IsRequired = false)]
|
||||
public Dictionary<string, string> LoginKeys { get; private set; }
|
||||
|
||||
string FileName = null;
|
||||
string FileName;
|
||||
|
||||
AccountSettingsStore()
|
||||
{
|
||||
SentryData = new Dictionary<string, byte[]>();
|
||||
ContentServerPenalty = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
|
||||
ContentServerPenalty = new ConcurrentDictionary<string, int>();
|
||||
LoginKeys = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
@@ -36,7 +34,7 @@ namespace DepotDownloader
|
||||
get { return Instance != null; }
|
||||
}
|
||||
|
||||
public static AccountSettingsStore Instance = null;
|
||||
public static AccountSettingsStore Instance;
|
||||
static readonly IsolatedStorageFile IsolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly();
|
||||
|
||||
public static void LoadFromFile(string filename)
|
||||
@@ -49,9 +47,9 @@ namespace DepotDownloader
|
||||
try
|
||||
{
|
||||
using (var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
|
||||
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
|
||||
{
|
||||
Instance = ProtoBuf.Serializer.Deserialize<AccountSettingsStore>(ds);
|
||||
Instance = Serializer.Deserialize<AccountSettingsStore>(ds);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
@@ -76,9 +74,9 @@ namespace DepotDownloader
|
||||
try
|
||||
{
|
||||
using (var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
{
|
||||
ProtoBuf.Serializer.Serialize<AccountSettingsStore>(ds, Instance);
|
||||
Serializer.Serialize(ds, Instance);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using SteamKit2;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SteamKit2;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
@@ -82,7 +82,7 @@ namespace DepotDownloader
|
||||
|
||||
private async Task ConnectionPoolMonitorAsync()
|
||||
{
|
||||
bool didPopulate = false;
|
||||
var didPopulate = false;
|
||||
|
||||
while (!shutdownToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -165,10 +165,8 @@ namespace DepotDownloader
|
||||
var result = await authTokenCallbackPromise.Task;
|
||||
return result.Token;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Failed to retrieve CDN token for server {server.Host} depot {depotId}");
|
||||
}
|
||||
|
||||
throw new Exception($"Failed to retrieve CDN token for server {server.Host} depot {depotId}");
|
||||
}
|
||||
|
||||
public void ReturnConnection(CDNClient.Server server)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ProtoBuf;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
@@ -12,7 +12,7 @@ namespace DepotDownloader
|
||||
[ProtoMember(1)]
|
||||
public Dictionary<uint, ulong> InstalledManifestIDs { get; private set; }
|
||||
|
||||
string FileName = null;
|
||||
string FileName;
|
||||
|
||||
DepotConfigStore()
|
||||
{
|
||||
@@ -24,7 +24,7 @@ namespace DepotDownloader
|
||||
get { return Instance != null; }
|
||||
}
|
||||
|
||||
public static DepotConfigStore Instance = null;
|
||||
public static DepotConfigStore Instance;
|
||||
|
||||
public static void LoadFromFile(string filename)
|
||||
{
|
||||
@@ -33,9 +33,9 @@ namespace DepotDownloader
|
||||
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
using (FileStream fs = File.Open(filename, FileMode.Open))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
|
||||
Instance = ProtoBuf.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 +50,9 @@ namespace DepotDownloader
|
||||
if (!Loaded)
|
||||
throw new Exception("Saved config before loading");
|
||||
|
||||
using (FileStream fs = File.Open(Instance.FileName, FileMode.Create))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
ProtoBuf.Serializer.Serialize<DepotConfigStore>(ds, Instance);
|
||||
using (var fs = File.Open(Instance.FileName, FileMode.Create))
|
||||
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
Serializer.Serialize(ds, Instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
|
||||
<Version>2.4.3</Version>
|
||||
<Description>Steam Downloading Utility</Description>
|
||||
@@ -13,4 +14,4 @@
|
||||
<PackageReference Include="protobuf-net" Version="3.0.101" />
|
||||
<PackageReference Include="SteamKit2" Version="2.4.0-Alpha.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -26,6 +26,6 @@ namespace DepotDownloader
|
||||
public bool RememberPassword { get; set; }
|
||||
|
||||
// A Steam LoginID to allow multiple concurrent connections
|
||||
public uint? LoginID {get; set; }
|
||||
public uint? LoginID { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -33,7 +29,7 @@ namespace DepotDownloader
|
||||
// By default, we create dual-mode sockets:
|
||||
// Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.NoDelay = true;
|
||||
|
||||
try
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace DepotDownloader
|
||||
protected override void OnEventWritten(EventWrittenEventArgs eventData)
|
||||
{
|
||||
var sb = new StringBuilder().Append($"{eventData.TimeStamp:HH:mm:ss.fffffff} {eventData.EventSource.Name}.{eventData.EventName}(");
|
||||
for (int i = 0; i < eventData.Payload?.Count; i++)
|
||||
for (var i = 0; i < eventData.Payload?.Count; i++)
|
||||
{
|
||||
sb.Append(eventData.PayloadNames?[i]).Append(": ").Append(eventData.Payload[i]);
|
||||
if (i < eventData.Payload?.Count - 1)
|
||||
|
||||
@@ -1,397 +1,400 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using SteamKit2;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static int Main( string[] args )
|
||||
=> MainAsync( args ).GetAwaiter().GetResult();
|
||||
|
||||
static async Task<int> MainAsync( string[] args )
|
||||
{
|
||||
if ( args.Length == 0 )
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
DebugLog.Enabled = false;
|
||||
|
||||
AccountSettingsStore.LoadFromFile( "account.config" );
|
||||
|
||||
#region Common Options
|
||||
|
||||
if ( HasParameter( args, "-debug" ) )
|
||||
{
|
||||
DebugLog.Enabled = true;
|
||||
DebugLog.AddListener( ( category, message ) =>
|
||||
{
|
||||
Console.WriteLine( "[{0}] {1}", category, message );
|
||||
});
|
||||
|
||||
var httpEventListener = new HttpDiagnosticEventListener();
|
||||
}
|
||||
|
||||
string username = GetParameter<string>( args, "-username" ) ?? GetParameter<string>( args, "-user" );
|
||||
string password = GetParameter<string>( args, "-password" ) ?? GetParameter<string>( args, "-pass" );
|
||||
ContentDownloader.Config.RememberPassword = HasParameter( args, "-remember-password" );
|
||||
|
||||
ContentDownloader.Config.DownloadManifestOnly = HasParameter( args, "-manifest-only" );
|
||||
|
||||
int cellId = GetParameter<int>( args, "-cellid", -1 );
|
||||
if ( cellId == -1 )
|
||||
{
|
||||
cellId = 0;
|
||||
}
|
||||
|
||||
ContentDownloader.Config.CellID = cellId;
|
||||
|
||||
string fileList = GetParameter<string>( args, "-filelist" );
|
||||
|
||||
if ( fileList != null )
|
||||
{
|
||||
try
|
||||
{
|
||||
string fileListData = await File.ReadAllTextAsync( fileList );
|
||||
var files = fileListData.Split( new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries );
|
||||
|
||||
ContentDownloader.Config.UsingFileList = true;
|
||||
ContentDownloader.Config.FilesToDownload = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
|
||||
ContentDownloader.Config.FilesToDownloadRegex = new List<Regex>();
|
||||
|
||||
foreach ( var fileEntry in files )
|
||||
{
|
||||
if ( fileEntry.StartsWith( "regex:" ) )
|
||||
{
|
||||
Regex rgx = new Regex( fileEntry.Substring( 6 ), RegexOptions.Compiled | RegexOptions.IgnoreCase );
|
||||
ContentDownloader.Config.FilesToDownloadRegex.Add( rgx );
|
||||
}
|
||||
else
|
||||
{
|
||||
ContentDownloader.Config.FilesToDownload.Add( fileEntry.Replace( '\\', '/' ) );
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine( "Using filelist: '{0}'.", fileList );
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine( "Warning: Unable to load filelist: {0}", ex.ToString() );
|
||||
}
|
||||
}
|
||||
|
||||
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<int>( args, "-max-servers", 20 );
|
||||
ContentDownloader.Config.MaxDownloads = GetParameter<int>( args, "-max-downloads", 8 );
|
||||
ContentDownloader.Config.MaxServers = Math.Max( ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads );
|
||||
ContentDownloader.Config.LoginID = HasParameter( args, "-loginid" ) ? (uint?)GetParameter<uint>( args, "-loginid" ) : null;
|
||||
|
||||
#endregion
|
||||
|
||||
uint appId = GetParameter<uint>( args, "-app", ContentDownloader.INVALID_APP_ID );
|
||||
if ( appId == ContentDownloader.INVALID_APP_ID )
|
||||
{
|
||||
Console.WriteLine( "Error: -app not specified!" );
|
||||
return 1;
|
||||
}
|
||||
|
||||
ulong pubFile = GetParameter<ulong>( args, "-pubfile", ContentDownloader.INVALID_MANIFEST_ID );
|
||||
ulong ugcId = GetParameter<ulong>( args, "-ugc", ContentDownloader.INVALID_MANIFEST_ID );
|
||||
if ( pubFile != ContentDownloader.INVALID_MANIFEST_ID )
|
||||
{
|
||||
#region Pubfile Downloading
|
||||
|
||||
if ( InitializeSteam( username, password ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
await ContentDownloader.DownloadPubfileAsync( appId, pubFile ).ConfigureAwait( false );
|
||||
}
|
||||
catch ( Exception ex ) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException )
|
||||
{
|
||||
Console.WriteLine( ex.Message );
|
||||
return 1;
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine( "Error: InitializeSteam failed" );
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
else if ( ugcId != ContentDownloader.INVALID_MANIFEST_ID )
|
||||
{
|
||||
#region UGC Downloading
|
||||
|
||||
if ( InitializeSteam( username, password ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
await ContentDownloader.DownloadUGCAsync( appId, ugcId ).ConfigureAwait( false );
|
||||
}
|
||||
catch ( Exception ex ) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException )
|
||||
{
|
||||
Console.WriteLine( ex.Message );
|
||||
return 1;
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine( "Error: InitializeSteam failed" );
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
else
|
||||
{
|
||||
#region App downloading
|
||||
|
||||
string branch = GetParameter<string>( args, "-branch" ) ?? GetParameter<string>( args, "-beta" ) ?? ContentDownloader.DEFAULT_BRANCH;
|
||||
ContentDownloader.Config.BetaPassword = GetParameter<string>( args, "-betapassword" );
|
||||
|
||||
ContentDownloader.Config.DownloadAllPlatforms = HasParameter( args, "-all-platforms" );
|
||||
string os = GetParameter<string>( args, "-os", null );
|
||||
|
||||
if ( ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty( os ) )
|
||||
{
|
||||
Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string arch = GetParameter<string>( args, "-osarch", null );
|
||||
|
||||
ContentDownloader.Config.DownloadAllLanguages = HasParameter( args, "-all-languages" );
|
||||
string language = GetParameter<string>( args, "-language", null );
|
||||
|
||||
if ( ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty( language ) )
|
||||
{
|
||||
Console.WriteLine( "Error: Cannot specify -language when -all-languages is specified." );
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool lv = HasParameter( args, "-lowviolence" );
|
||||
|
||||
List<(uint, ulong)> depotManifestIds = new List<(uint, ulong)>();
|
||||
bool isUGC = false;
|
||||
|
||||
List<uint> depotIdList = GetParameterList<uint>( args, "-depot" );
|
||||
List<ulong> manifestIdList = GetParameterList<ulong>( 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 );
|
||||
}
|
||||
catch ( Exception ex ) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException )
|
||||
{
|
||||
Console.WriteLine( ex.Message );
|
||||
return 1;
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
Console.WriteLine( "Download failed to due to an unhandled exception: {0}", e.Message );
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine( "Error: InitializeSteam failed" );
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool InitializeSteam( string username, string password )
|
||||
{
|
||||
if ( username != null && password == null && ( !ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey( username ) ) )
|
||||
{
|
||||
do
|
||||
{
|
||||
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 );
|
||||
}
|
||||
else if ( username == null )
|
||||
{
|
||||
Console.WriteLine( "No username given. Using anonymous account with dedicated server subscription." );
|
||||
}
|
||||
|
||||
// capture the supplied password in case we need to re-use it after checking the login key
|
||||
ContentDownloader.Config.SuppliedPassword = password;
|
||||
|
||||
return ContentDownloader.InitializeSteam3( username, password );
|
||||
}
|
||||
|
||||
static int IndexOfParam( string[] args, string param )
|
||||
{
|
||||
for ( int x = 0; x < args.Length; ++x )
|
||||
{
|
||||
if ( args[ x ].Equals( param, StringComparison.OrdinalIgnoreCase ) )
|
||||
return x;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
static bool HasParameter( string[] args, string param )
|
||||
{
|
||||
return IndexOfParam( args, param ) > -1;
|
||||
}
|
||||
|
||||
static T GetParameter<T>( string[] args, string param, T defaultValue = default( T ) )
|
||||
{
|
||||
int index = IndexOfParam( args, param );
|
||||
|
||||
if ( index == -1 || index == ( args.Length - 1 ) )
|
||||
return defaultValue;
|
||||
|
||||
string strParam = args[ index + 1 ];
|
||||
|
||||
var converter = TypeDescriptor.GetConverter( typeof( T ) );
|
||||
if ( converter != null )
|
||||
{
|
||||
return ( T )converter.ConvertFromString( strParam );
|
||||
}
|
||||
|
||||
return default( T );
|
||||
}
|
||||
|
||||
static List<T> GetParameterList<T>(string[] args, string param)
|
||||
{
|
||||
List<T> list = new List<T>();
|
||||
int index = IndexOfParam(args, param);
|
||||
|
||||
if (index == -1 || index == (args.Length - 1))
|
||||
return list;
|
||||
|
||||
index++;
|
||||
|
||||
while (index < args.Length)
|
||||
{
|
||||
string strParam = args[index];
|
||||
|
||||
if (strParam[0] == '-') break;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (converter != null)
|
||||
{
|
||||
list.Add((T)converter.ConvertFromString(strParam));
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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." );
|
||||
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();
|
||||
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." );
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using SteamKit2;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static int Main(string[] args)
|
||||
=> MainAsync(args).GetAwaiter().GetResult();
|
||||
|
||||
static async Task<int> MainAsync(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
DebugLog.Enabled = false;
|
||||
|
||||
AccountSettingsStore.LoadFromFile("account.config");
|
||||
|
||||
#region Common Options
|
||||
|
||||
if (HasParameter(args, "-debug"))
|
||||
{
|
||||
DebugLog.Enabled = true;
|
||||
DebugLog.AddListener((category, message) =>
|
||||
{
|
||||
Console.WriteLine("[{0}] {1}", category, message);
|
||||
});
|
||||
|
||||
var httpEventListener = new HttpDiagnosticEventListener();
|
||||
}
|
||||
|
||||
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.DownloadManifestOnly = HasParameter(args, "-manifest-only");
|
||||
|
||||
var cellId = GetParameter(args, "-cellid", -1);
|
||||
if (cellId == -1)
|
||||
{
|
||||
cellId = 0;
|
||||
}
|
||||
|
||||
ContentDownloader.Config.CellID = cellId;
|
||||
|
||||
var fileList = GetParameter<string>(args, "-filelist");
|
||||
|
||||
if (fileList != null)
|
||||
{
|
||||
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>();
|
||||
|
||||
foreach (var fileEntry in files)
|
||||
{
|
||||
if (fileEntry.StartsWith("regex:"))
|
||||
{
|
||||
var rgx = new Regex(fileEntry.Substring(6), RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
ContentDownloader.Config.FilesToDownloadRegex.Add(rgx);
|
||||
}
|
||||
else
|
||||
{
|
||||
ContentDownloader.Config.FilesToDownload.Add(fileEntry.Replace('\\', '/'));
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Using filelist: '{0}'.", fileList);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Warning: Unable to load filelist: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
|
||||
var appId = GetParameter(args, "-app", ContentDownloader.INVALID_APP_ID);
|
||||
if (appId == ContentDownloader.INVALID_APP_ID)
|
||||
{
|
||||
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)
|
||||
{
|
||||
#region Pubfile Downloading
|
||||
|
||||
if (InitializeSteam(username, password))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ContentDownloader.DownloadPubfileAsync(appId, pubFile).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Error: InitializeSteam failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
else if (ugcId != ContentDownloader.INVALID_MANIFEST_ID)
|
||||
{
|
||||
#region UGC Downloading
|
||||
|
||||
if (InitializeSteam(username, password))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ContentDownloader.DownloadUGCAsync(appId, ugcId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Error: InitializeSteam failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
else
|
||||
{
|
||||
#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.DownloadAllPlatforms = HasParameter(args, "-all-platforms");
|
||||
var os = GetParameter<string>(args, "-os");
|
||||
|
||||
if (ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty(os))
|
||||
{
|
||||
Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var arch = GetParameter<string>(args, "-osarch");
|
||||
|
||||
ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages");
|
||||
var language = GetParameter<string>(args, "-language");
|
||||
|
||||
if (ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty(language))
|
||||
{
|
||||
Console.WriteLine("Error: Cannot specify -language when -all-languages is specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var lv = HasParameter(args, "-lowviolence");
|
||||
|
||||
var depotManifestIds = new List<(uint, ulong)>();
|
||||
var isUGC = false;
|
||||
|
||||
var depotIdList = GetParameterList<uint>(args, "-depot");
|
||||
var manifestIdList = GetParameterList<ulong>(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);
|
||||
}
|
||||
catch (Exception ex) when (
|
||||
ex is ContentDownloaderException
|
||||
|| ex is OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentDownloader.ShutdownSteam3();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Error: InitializeSteam failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool InitializeSteam(string username, string password)
|
||||
{
|
||||
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey(username)))
|
||||
{
|
||||
do
|
||||
{
|
||||
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);
|
||||
}
|
||||
else if (username == null)
|
||||
{
|
||||
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription.");
|
||||
}
|
||||
|
||||
// capture the supplied password in case we need to re-use it after checking the login key
|
||||
ContentDownloader.Config.SuppliedPassword = password;
|
||||
|
||||
return ContentDownloader.InitializeSteam3(username, password);
|
||||
}
|
||||
|
||||
static int IndexOfParam(string[] args, string param)
|
||||
{
|
||||
for (var x = 0; x < args.Length; ++x)
|
||||
{
|
||||
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
|
||||
return x;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool HasParameter(string[] args, string param)
|
||||
{
|
||||
return IndexOfParam(args, param) > -1;
|
||||
}
|
||||
|
||||
static T GetParameter<T>(string[] args, string param, T defaultValue = default(T))
|
||||
{
|
||||
var index = IndexOfParam(args, param);
|
||||
|
||||
if (index == -1 || index == (args.Length - 1))
|
||||
return defaultValue;
|
||||
|
||||
var strParam = args[index + 1];
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (converter != null)
|
||||
{
|
||||
return (T)converter.ConvertFromString(strParam);
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
static List<T> GetParameterList<T>(string[] args, string param)
|
||||
{
|
||||
var list = new List<T>();
|
||||
var index = IndexOfParam(args, param);
|
||||
|
||||
if (index == -1 || index == (args.Length - 1))
|
||||
return list;
|
||||
|
||||
index++;
|
||||
|
||||
while (index < args.Length)
|
||||
{
|
||||
var strParam = args[index];
|
||||
|
||||
if (strParam[0] == '-') break;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (converter != null)
|
||||
{
|
||||
list.Add((T)converter.ConvertFromString(strParam));
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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.");
|
||||
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();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
using ProtoBuf;
|
||||
using SteamKit2;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
[ProtoContract()]
|
||||
[ProtoContract]
|
||||
class ProtoManifest
|
||||
{
|
||||
// Proto ctor
|
||||
@@ -24,7 +23,7 @@ namespace DepotDownloader
|
||||
CreationTime = sourceManifest.CreationTime;
|
||||
}
|
||||
|
||||
[ProtoContract()]
|
||||
[ProtoContract]
|
||||
public class FileData
|
||||
{
|
||||
// Proto ctor
|
||||
@@ -130,32 +129,31 @@ namespace DepotDownloader
|
||||
return null;
|
||||
}
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (FileStream fs = File.Open(filename, FileMode.Open))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
|
||||
using (var fs = File.Open(filename, FileMode.Open))
|
||||
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
|
||||
ds.CopyTo(ms);
|
||||
|
||||
checksum = Util.SHAHash(ms.ToArray());
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return ProtoBuf.Serializer.Deserialize<ProtoManifest>(ms);
|
||||
return Serializer.Deserialize<ProtoManifest>(ms);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveToFile(string filename, out byte[] checksum)
|
||||
{
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
ProtoBuf.Serializer.Serialize<ProtoManifest>(ms, this);
|
||||
Serializer.Serialize(ms, this);
|
||||
|
||||
checksum = Util.SHAHash(ms.ToArray());
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (FileStream fs = File.Open(filename, FileMode.Create))
|
||||
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
using (var fs = File.Open(filename, FileMode.Create))
|
||||
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
|
||||
ms.CopyTo(ds);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,176 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
static class Util
|
||||
{
|
||||
public static string GetSteamOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return "windows";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "macos";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "linux";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public static string GetSteamArch()
|
||||
{
|
||||
return Environment.Is64BitOperatingSystem ? "64" : "32";
|
||||
}
|
||||
|
||||
public static string ReadPassword()
|
||||
{
|
||||
ConsoleKeyInfo keyInfo;
|
||||
StringBuilder password = new StringBuilder();
|
||||
|
||||
do
|
||||
{
|
||||
keyInfo = Console.ReadKey( true );
|
||||
|
||||
if ( keyInfo.Key == ConsoleKey.Backspace )
|
||||
{
|
||||
if ( password.Length > 0 )
|
||||
{
|
||||
password.Remove( password.Length - 1, 1 );
|
||||
Console.Write( "\x1B[1D\x1B[1P" );
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Printable ASCII characters only */
|
||||
char c = keyInfo.KeyChar;
|
||||
if ( c >= ' ' && c <= '~' )
|
||||
{
|
||||
password.Append( c );
|
||||
Console.Write( '*' );
|
||||
}
|
||||
} while ( keyInfo.Key != ConsoleKey.Enter );
|
||||
|
||||
return password.ToString();
|
||||
}
|
||||
|
||||
// Validate a file against Steam3 Chunk data
|
||||
public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata)
|
||||
{
|
||||
var neededChunks = new List<ProtoManifest.ChunkData>();
|
||||
int read;
|
||||
|
||||
foreach (var data in chunkdata)
|
||||
{
|
||||
byte[] 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;
|
||||
}
|
||||
|
||||
byte[] adler = AdlerHash(tempchunk);
|
||||
if (!adler.SequenceEqual(data.Checksum))
|
||||
{
|
||||
neededChunks.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
return neededChunks;
|
||||
}
|
||||
|
||||
public static byte[] AdlerHash(byte[] input)
|
||||
{
|
||||
uint a = 0, b = 0;
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
a = (a + input[i]) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
return BitConverter.GetBytes(a | (b << 16));
|
||||
}
|
||||
|
||||
public static byte[] SHAHash( byte[] input )
|
||||
{
|
||||
using (var sha = SHA1.Create())
|
||||
{
|
||||
var output = sha.ComputeHash( input );
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] DecodeHexString( string hex )
|
||||
{
|
||||
if ( hex == null )
|
||||
return null;
|
||||
|
||||
int chars = hex.Length;
|
||||
byte[] bytes = new byte[ chars / 2 ];
|
||||
|
||||
for ( int i = 0 ; i < chars ; i += 2 )
|
||||
bytes[ i / 2 ] = Convert.ToByte( hex.Substring( i, 2 ), 16 );
|
||||
|
||||
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)
|
||||
{
|
||||
if (taskFactories == null) throw new ArgumentNullException(nameof(taskFactories));
|
||||
if (maxDegreeOfParallelism <= 0) throw new ArgumentException(nameof(maxDegreeOfParallelism));
|
||||
|
||||
Func<Task>[] queue = taskFactories.ToArray();
|
||||
|
||||
if (queue.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Task> tasksInFlight = new List<Task>(maxDegreeOfParallelism);
|
||||
int index = 0;
|
||||
|
||||
do
|
||||
{
|
||||
while (tasksInFlight.Count < maxDegreeOfParallelism && index < queue.Length)
|
||||
{
|
||||
Func<Task> taskFactory = queue[index++];
|
||||
|
||||
tasksInFlight.Add(taskFactory());
|
||||
}
|
||||
|
||||
Task completedTask = await Task.WhenAny(tasksInFlight).ConfigureAwait(false);
|
||||
|
||||
await completedTask.ConfigureAwait(false);
|
||||
|
||||
tasksInFlight.Remove(completedTask);
|
||||
}
|
||||
while (index < queue.Length || tasksInFlight.Count != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DepotDownloader
|
||||
{
|
||||
static class Util
|
||||
{
|
||||
public static string GetSteamOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return "windows";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "macos";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "linux";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public static string GetSteamArch()
|
||||
{
|
||||
return Environment.Is64BitOperatingSystem ? "64" : "32";
|
||||
}
|
||||
|
||||
public static string ReadPassword()
|
||||
{
|
||||
ConsoleKeyInfo keyInfo;
|
||||
var password = new StringBuilder();
|
||||
|
||||
do
|
||||
{
|
||||
keyInfo = Console.ReadKey(true);
|
||||
|
||||
if (keyInfo.Key == ConsoleKey.Backspace)
|
||||
{
|
||||
if (password.Length > 0)
|
||||
{
|
||||
password.Remove(password.Length - 1, 1);
|
||||
Console.Write("\x1B[1D\x1B[1P");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Printable ASCII characters only */
|
||||
var c = keyInfo.KeyChar;
|
||||
if (c >= ' ' && c <= '~')
|
||||
{
|
||||
password.Append(c);
|
||||
Console.Write('*');
|
||||
}
|
||||
} while (keyInfo.Key != ConsoleKey.Enter);
|
||||
|
||||
return password.ToString();
|
||||
}
|
||||
|
||||
// Validate a file against Steam3 Chunk data
|
||||
public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata)
|
||||
{
|
||||
var neededChunks = new List<ProtoManifest.ChunkData>();
|
||||
int read;
|
||||
|
||||
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))
|
||||
{
|
||||
neededChunks.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
return neededChunks;
|
||||
}
|
||||
|
||||
public static byte[] AdlerHash(byte[] input)
|
||||
{
|
||||
uint a = 0, b = 0;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
a = (a + input[i]) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
|
||||
return BitConverter.GetBytes(a | (b << 16));
|
||||
}
|
||||
|
||||
public static byte[] SHAHash(byte[] input)
|
||||
{
|
||||
using (var sha = SHA1.Create())
|
||||
{
|
||||
var output = sha.ComputeHash(input);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] DecodeHexString(string hex)
|
||||
{
|
||||
if (hex == null)
|
||||
return null;
|
||||
|
||||
var chars = hex.Length;
|
||||
var bytes = new byte[chars / 2];
|
||||
|
||||
for (var i = 0; i < chars; i += 2)
|
||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
await completedTask.ConfigureAwait(false);
|
||||
|
||||
tasksInFlight.Remove(completedTask);
|
||||
} while (index < queue.Length || tasksInFlight.Count != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user