diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e6f3453 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,217 @@ +# 如果要从更高级别的目录继承 .editorconfig 设置,请删除以下行 +root = true + +# c# 文件 +[*.cs] + +#### Core EditorConfig 选项 #### + +# 缩进和间距 +indent_size = 4 +indent_style = space +tab_width = 4 + +# 新行首选项 +end_of_line = crlf +insert_final_newline = false + +#### .NET 编码约定 #### + +# 组织 Using +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. 和 Me. 首选项 +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# 语言关键字与 bcl 类型首选项 +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# 括号首选项 +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion + +# 修饰符首选项 +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# 表达式级首选项 +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# 字段首选项 +dotnet_style_readonly_field = true + +# 参数首选项 +dotnet_code_quality_unused_parameters = all + +# 禁止显示首选项 +dotnet_remove_unnecessary_suppression_exclusions = none + +# 新行首选项 +dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### c# 编码约定 #### + +# var 首选项 +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true:suggestion + +# Expression-bodied 成员 +csharp_style_expression_bodied_accessors = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = when_on_single_line:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:suggestion + +# 模式匹配首选项 +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true + +# Null 检查首选项 +csharp_style_conditional_delegate_call = true + +# 修饰符首选项 +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# 代码块首选项 +csharp_prefer_braces = when_multiline:suggestion +csharp_prefer_simple_using_statement = true + +# 表达式级首选项 +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# "using" 指令首选项 +csharp_using_directive_placement = outside_namespace:suggestion + +# 新行首选项 +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# 格式规则 #### + +# 新行首选项 +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = false +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# 缩进首选项 +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# 空格键首选项 +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 = false +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 + +# 包装首选项 +csharp_preserve_single_line_blocks = false +csharp_preserve_single_line_statements = false + +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 命名样式 + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/L4d2AddonsMgr.sln b/L4d2AddonsMgr.sln index 6075bda..58a68cf 100644 --- a/L4d2AddonsMgr.sln +++ b/L4d2AddonsMgr.sln @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "L4d2AddonsMgrTest", "L4d2Ad EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{95174B3B-1FB5-4AE4-BFD6-A826054249B6}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig ReadMe.md = ReadMe.md EndProjectSection EndProject diff --git a/L4d2AddonsMgr/AcfFile/AcfFile.cs b/L4d2AddonsMgr/AcfFile/AcfFile.cs index e223b01..2d6fabd 100644 --- a/L4d2AddonsMgr/AcfFile/AcfFile.cs +++ b/L4d2AddonsMgr/AcfFile/AcfFile.cs @@ -50,7 +50,7 @@ public static Node GetNode(Node node, string key) { public static Node GetNodeIgnoreCase(Node node, string key) { if (!(node is CompoundNode cnode)) return null; foreach (var child in cnode.Value) { - if (key.ToLowerInvariant() == child.Key.ToLowerInvariant()) return child; + if (key.ToLowerInvariant() == child.Key?.ToLowerInvariant()) return child; } return null; } @@ -184,7 +184,7 @@ public Node GetNodeByPath(params string[] path) { Node node = Root; foreach (var seg in path) { if (node == null) return null; - node = GetNode(node, seg); + node = GetNodeIgnoreCase(node, seg); } return node; } diff --git a/L4d2AddonsMgr/AddonsCollection.cs b/L4d2AddonsMgr/AddonsCollection.cs index c420a04..2499dbf 100644 --- a/L4d2AddonsMgr/AddonsCollection.cs +++ b/L4d2AddonsMgr/AddonsCollection.cs @@ -73,8 +73,12 @@ public AddonsCollection(AddonsLibrary addonsLibrary) { public void ReloadFromLibrary() { Application.Current.Dispatcher.Invoke(() => Clear()); - foreach (var item in Library) - Application.Current.Dispatcher.Invoke(new Action(() => Add(item))); + try { + foreach (var item in Library) + Application.Current.Dispatcher.Invoke(new Action(() => Add(item))); + } catch (Exception) { + // + } } private void Add(VpkHolder vpkHolder) { diff --git a/L4d2AddonsMgr/ExplorerInterop.cs b/L4d2AddonsMgr/ExplorerInterop.cs new file mode 100644 index 0000000..e49d337 --- /dev/null +++ b/L4d2AddonsMgr/ExplorerInterop.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace L4d2AddonsMgr { + class ExplorerInterop { + [DllImport("shell32.dll", SetLastError = true)] + public static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, uint dwFlags); + + [DllImport("shell32.dll", SetLastError = true)] + public static extern void SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr bindingContext, [Out] out IntPtr pidl, uint sfgaoIn, [Out] out uint psfgaoOut); + + public static void OpenFolderAndSelectItem(string folderPath, string file) { + IntPtr nativeFolder; + uint psfgaoOut; + SHParseDisplayName(folderPath, IntPtr.Zero, out nativeFolder, 0, out psfgaoOut); + + if (nativeFolder == IntPtr.Zero) { + // Log error, can't find folder + return; + } + + IntPtr nativeFile; + SHParseDisplayName(Path.Combine(folderPath, file), IntPtr.Zero, out nativeFile, 0, out psfgaoOut); + + IntPtr[] fileArray; + if (nativeFile == IntPtr.Zero) { + // Open the folder without the file selected if we can't find the file + fileArray = new IntPtr[0]; + } else { + fileArray = new IntPtr[] { nativeFile }; + } + + SHOpenFolderAndSelectItems(nativeFolder, (uint)fileArray.Length, fileArray, 0); + + Marshal.FreeCoTaskMem(nativeFolder); + if (nativeFile != IntPtr.Zero) { + Marshal.FreeCoTaskMem(nativeFile); + } + } + } +} diff --git a/L4d2AddonsMgr/GameDirLocator.cs b/L4d2AddonsMgr/GameDirLocator.cs index 3ee9a47..65fba6e 100644 --- a/L4d2AddonsMgr/GameDirLocator.cs +++ b/L4d2AddonsMgr/GameDirLocator.cs @@ -23,11 +23,18 @@ public static string FindGameInLibraries(string baseDir) { var conf = AcfFile.ParseString(confTxt, true); var infoNode = conf.GetNodeByPath(CommonConsts.SteamLibraryConfigVdfFileRootNode); foreach (var node in (infoNode as AcfFile.CompoundNode).Value) { - if (!(node is AcfFile.LeafNode)) continue; + //if (!(node is AcfFile.LeafNode)) continue; try { int.Parse(node.Key); + var libPath = (node as AcfFile.LeafNode)?.Value; + if (libPath == null) { + var comNode = node as AcfFile.CompoundNode; + if ((comNode.GetChild("apps") as AcfFile.CompoundNode)?.GetChild("550") == null) + continue; + libPath = (comNode.GetChild("path") as AcfFile.LeafNode).Value; + } gamePath = FindGameInLibrary( - Path.Combine((node as AcfFile.LeafNode).Value, CommonConsts.SteamAppsDirectoryName) + Path.Combine(libPath, CommonConsts.SteamAppsDirectoryName) ); if (gamePath != null) { break; diff --git a/L4d2AddonsMgr/L4d2AddonsMgr.csproj b/L4d2AddonsMgr/L4d2AddonsMgr.csproj index 91481ff..2ff3e57 100644 --- a/L4d2AddonsMgr/L4d2AddonsMgr.csproj +++ b/L4d2AddonsMgr/L4d2AddonsMgr.csproj @@ -116,6 +116,7 @@ + CancelConfirmationDialog.xaml diff --git a/L4d2AddonsMgr/MainWindow.xaml.cs b/L4d2AddonsMgr/MainWindow.xaml.cs index 887d371..ee5f85a 100644 --- a/L4d2AddonsMgr/MainWindow.xaml.cs +++ b/L4d2AddonsMgr/MainWindow.xaml.cs @@ -83,7 +83,9 @@ public MainWindow(string libraryPath) { public bool SupportsEnabledState => libraryPath == null; - public AddonsCollection Addons { get; private set; } + public AddonsCollection Addons { + get; private set; + } // Stay identical with Windows accent color. // https://stackoverflow.com/questions/13660976/get-the-active-color-of-windows-8-automatic-color-theme @@ -97,11 +99,11 @@ protected override void OnSourceInitialized(EventArgs e) { private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { - case WmDwmColorizationColorChanged: - UpdateAccentColor(); - return IntPtr.Zero; - default: - return IntPtr.Zero; + case WmDwmColorizationColorChanged: + UpdateAccentColor(); + return IntPtr.Zero; + default: + return IntPtr.Zero; } } @@ -119,59 +121,60 @@ private async void DoReloadAddonsFromLibrary() { } private async void AddonsGrid_Loaded(object sender, RoutedEventArgs e) { - await Task.Run(() => { - string path; - // 0: Find Steam install path from registry. - try { - path = GameDirLocator.LocateSteamDirFromRegistry(); - } catch (Exception ex) { - // Just use "+"! It would ALWAYS be optimized away. - // https://stackoverflow.com/questions/288794/does-c-sharp-optimize-the-concatenation-of-string-literals - - // LMAO Forget about answers by - // https://stackoverflow.com/questions/1100260/multiline-string-literal-in-c-sharp - // https://stackoverflow.com/questions/31764898/long-string-interpolation-lines-in-c6/31766560#31766560 - // Performance art they are doing. - Debug.WriteLine( - "ERROR: Failed locating installed Steam directory using registry.\n" + - "Did anything restricted me from accesing the registry or what?\n" + - "We were looking into 32-bit's view of the registry.\nDetails:"); - Debug.WriteLine(ex); - ErrBox("尝试从您的计算机查找Steam安装信息时出错。"); - return; - } - if (path == null) { - ErrBox("未能从您的计算机查找到Steam安装信息。"); - return; - } - - // 1: Find game dir in all Steam libraries. - try { - path = GameDirLocator.FindGameInLibraries(path); - if (path == null) { - ErrBox("未能从您的Steam库中查找到左4死2。"); + if (libraryPath == null) + await Task.Run(() => { + string path; + // 0: Find Steam install path from registry. + try { + path = GameDirLocator.LocateSteamDirFromRegistry(); + } catch (Exception ex) { + // Just use "+"! It would ALWAYS be optimized away. + // https://stackoverflow.com/questions/288794/does-c-sharp-optimize-the-concatenation-of-string-literals + + // LMAO Forget about answers by + // https://stackoverflow.com/questions/1100260/multiline-string-literal-in-c-sharp + // https://stackoverflow.com/questions/31764898/long-string-interpolation-lines-in-c6/31766560#31766560 + // Performance art they are doing. + Debug.WriteLine( + "ERROR: Failed locating installed Steam directory using registry.\n" + + "Did anything restricted me from accesing the registry or what?\n" + + "We were looking into 32-bit's view of the registry.\nDetails:"); + Debug.WriteLine(ex); + ErrBox("尝试从您的计算机查找Steam安装信息时出错。"); return; } - if (!GameDirLocator.ValidateGameDir(path)) { - ErrBox("从您的Steam库中查找到的左4死2文件夹结构不完整。"); + if (path == null) { + ErrBox("未能从您的计算机查找到Steam安装信息。"); return; } - } catch (Exception ex) { - Debug.WriteLine(ex); - ErrBox("尝试从您的Steam库中查找左4死2时出错。"); - return; - } - if (libraryPath == null) { + + // 1: Find game dir in all Steam libraries. try { - addonsList = new AddonsListTxt(path); + path = GameDirLocator.FindGameInLibraries(path); + if (path == null) { + ErrBox("未能从您的Steam库中查找到左4死2。"); + return; + } + if (!GameDirLocator.ValidateGameDir(path)) { + ErrBox("从您的Steam库中查找到的左4死2文件夹结构不完整。"); + return; + } } catch (Exception ex) { Debug.WriteLine(ex); - MsgBox("读取附加组件配置文件失败,为避免数据丢失,部分功能将不可用。" + - "建议您检查并更正addonlist.txt中的格式错误,或在线寻求帮助。"); + ErrBox("尝试从您的Steam库中查找左4死2时出错。"); + return; } - } - gameDir = path; - }); + if (libraryPath == null) { + try { + addonsList = new AddonsListTxt(path); + } catch (Exception ex) { + Debug.WriteLine(ex); + MsgBox("读取附加组件配置文件失败,为避免数据丢失,部分功能将不可用。" + + "建议您检查并更正addonlist.txt中的格式错误,或在线寻求帮助。"); + } + } + gameDir = path; + }); var lib = libraryPath == null ? new GameDirAddonsLibrary(gameDir, addonsList) : (AddonsLibrary)new ExternalDirectoryAddonsLibrary(libraryPath); Addons = new AddonsCollection(lib); Addons.PropertyChanged += @@ -225,7 +228,7 @@ private void ShowItemInExplorerCommand_Invoke(object sender, ExecutedRoutedEvent // Show in explorer: // https://stackoverflow.com/questions/334630/opening-a-folder-in-explorer-and-selecting-a-file if (e.Parameter is FileInfo fileInfo) - Process.Start("explorer.exe", "/select, \"" + fileInfo.FullName + "\""); + ExplorerInterop.OpenFolderAndSelectItem(fileInfo.DirectoryName, fileInfo.Name); } private async void RefreshListCommand_Invoke(object sender, ExecutedRoutedEventArgs e) { @@ -259,20 +262,20 @@ private void OpAutoRenameCommand_Invoke(object sender, ExecutedRoutedEventArgs e if (res ?? false) { List list; switch (dialog.MyAppliedScope.MyScope) { - case AppliedScope.Scope.All: - list = new List(Addons.Files); - break; - case AppliedScope.Scope.SelectedItems: - // synchronized and lock: - // https://stackoverflow.com/questions/541194/c-sharp-version-of-javas-synchronized-keyword - lock (AddonsListBox.SelectedItems) { - list = new List(AddonsListBox.SelectedItems.Count); - foreach (object o in AddonsListBox.SelectedItems) - list.Add((VpkHolder)o); - } - break; - default: - return; + case AppliedScope.Scope.All: + list = new List(Addons.Files); + break; + case AppliedScope.Scope.SelectedItems: + // synchronized and lock: + // https://stackoverflow.com/questions/541194/c-sharp-version-of-javas-synchronized-keyword + lock (AddonsListBox.SelectedItems) { + list = new List(AddonsListBox.SelectedItems.Count); + foreach (object o in AddonsListBox.SelectedItems) + list.Add((VpkHolder)o); + } + break; + default: + return; } Addons.IsLoading = true; var renameTask = new AutoRenameTask(list, dialog.Model, gameDir, addonsList);