diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0ebd8e34 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,246 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +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:warning +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_return = true:warning +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +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:warning +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_local_over_anonymous_function = true:warning +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:warning + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +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# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +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 = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# 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 = 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 + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +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 + +# Symbol specifications + +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 = + +# Naming styles + +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 + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_return = true:warning +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5fe15c0..205d7910 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,8 @@ defaults: run: shell: pwsh +permissions: read-all + jobs: build: runs-on: windows-latest @@ -33,20 +35,20 @@ jobs: uses: actions/setup-dotnet@v1 with: # As usual, obtained from: https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/5.0/releases.json - dotnet-version: "3.1.404" # since we now use this + dotnet-version: "6.0.100" # since we now use this + - name: Log in to package source + shell: pwsh + run: | + dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} ` + --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Atlas-Rhythm/index.json" - name: Clear Nuget Cache run: dotnet nuget locals all --clear - name: Restore run: msbuild -t:Restore -m - name: Build run: msbuild -t:Build -m - - name: Upload net461 - uses: actions/upload-artifact@v2 - with: - name: BSIPA-net461-${{ env.Platform }} - path: BSIPA-Meta/bin/${{ env.Platform }}/${{ env.Configuration }}/net461/ - - name: Upload net35 + - name: Upload net472 uses: actions/upload-artifact@v2 with: - name: BSIPA-net35-${{ env.Platform }} - path: BSIPA-Meta/bin/${{ env.Platform }}/${{ env.Configuration }}/net35/ + name: BSIPA-net472-${{ env.Platform }} + path: BSIPA-Meta/bin/${{ env.Platform }}/${{ env.Configuration }}/net472/ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 868aaeab..0d60315f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,12 @@ jobs: uses: actions/setup-dotnet@v1 with: # As usual, obtained from: https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/5.0/releases.json - dotnet-version: "3.1.404" # since we now use this + dotnet-version: "6.0.100" # since we now use this + - name: Log in to package source + shell: pwsh + run: | + dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} ` + --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Atlas-Rhythm/index.json" - name: Clear Nuget Cache run: dotnet nuget locals all --clear - name: Nuget Restore diff --git a/.github/workflows/tag_docs.yml b/.github/workflows/tag_docs.yml index c6031945..743a0e4c 100644 --- a/.github/workflows/tag_docs.yml +++ b/.github/workflows/tag_docs.yml @@ -29,7 +29,12 @@ jobs: uses: actions/setup-dotnet@v1 with: # As usual, obtained from: https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/5.0/releases.json - dotnet-version: "3.1.404" # since we now use this + dotnet-version: "6.0.100" # since we now use this + - name: Log in to package source + shell: pwsh + run: | + dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} ` + --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Atlas-Rhythm/index.json" - name: Clear Nuget Cache run: dotnet nuget locals all --clear - name: Nuget Restore @@ -37,7 +42,7 @@ jobs: - name: Install DocFX uses: crazy-max/ghaction-chocolatey@v1 with: - args: install docfx --version 2.48 -y + args: install docfx -y - name: Checkout current pages uses: actions/checkout@v2 with: diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..5408330e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to BSIPA", + "type": "mono", + "request": "attach", + "address": "localhost", + "port": 10000 + } + ] +} \ No newline at end of file diff --git a/BSIPA-Meta/BSIPA-Meta.csproj b/BSIPA-Meta/BSIPA-Meta.csproj index cbdb384e..794a1291 100644 --- a/BSIPA-Meta/BSIPA-Meta.csproj +++ b/BSIPA-Meta/BSIPA-Meta.csproj @@ -1,7 +1,7 @@  - net35;net461 + net472 x86;x64 Debug;Release;Verbose;Verbose_Release @@ -59,9 +59,11 @@ + + @@ -69,7 +71,7 @@ - + @@ -87,3 +89,4 @@ + diff --git a/BSIPA.sln b/BSIPA.sln index f2a38632..3e223942 100644 --- a/BSIPA.sln +++ b/BSIPA.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31911.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IPA", "IPA\IPA.csproj", "{14092533-98BB-40A4-9AFC-27BB75672A70}" ProjectSection(ProjectDependencies) = postProject @@ -18,13 +18,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{C79C2C3A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4D6639A2-BD39-4F9B-AF7F-8E5F3B88243D}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .github\workflows\build.yml = .github\workflows\build.yml Common.props = Common.props Common.targets = Common.targets .github\workflows\docs.yml = .github\workflows\docs.yml README.md = README.md .github\release_draft.yml = .github\release_draft.yml - System.Diagnostics.CodeAnalysis.cs = System.Diagnostics.CodeAnalysis.cs .github\workflows\tag_docs.yml = .github\workflows\tag_docs.yml EndProjectSection EndProject @@ -50,180 +50,144 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IPA.Loader", "IPA.Loader\IP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net3-Proxy", "Net3-Proxy\Net3-Proxy.csproj", "{0DEDB099-9A26-4069-A4C1-A76CEB16283B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemVer", "SemVer\SemVer.csproj", "{B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 - Verbose_Release|Any CPU = Verbose_Release|Any CPU Verbose_Release|x64 = Verbose_Release|x64 Verbose_Release|x86 = Verbose_Release|x86 - Verbose|Any CPU = Verbose|Any CPU Verbose|x64 = Verbose|x64 Verbose|x86 = Verbose|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|Any CPU.Build.0 = Debug|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|x64.ActiveCfg = Debug|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|x64.Build.0 = Debug|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|x86.ActiveCfg = Debug|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Debug|x86.Build.0 = Debug|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|Any CPU.Build.0 = Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|x64.ActiveCfg = Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|x64.Build.0 = Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|x86.ActiveCfg = Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Release|x86.Build.0 = Release|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|Any CPU.ActiveCfg = Verbose_Release|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|Any CPU.Build.0 = Verbose_Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|x64.ActiveCfg = Verbose_Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|x64.Build.0 = Verbose_Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|x86.ActiveCfg = Verbose_Release|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose_Release|x86.Build.0 = Verbose_Release|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|Any CPU.ActiveCfg = Verbose|Any CPU - {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|Any CPU.Build.0 = Verbose|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x64.ActiveCfg = Verbose|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x64.Build.0 = Verbose|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x86.ActiveCfg = Verbose|Any CPU {14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x86.Build.0 = Verbose|Any CPU - {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|Any CPU.ActiveCfg = Verbose_Release|Win32 - {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|Any CPU.Build.0 = Verbose_Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|x64.ActiveCfg = Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|x64.Build.0 = Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|x86.ActiveCfg = Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Debug|x86.Build.0 = Release|Win32 - {88609E16-731F-46C9-8139-6B1A7A83240D}.Release|Any CPU.ActiveCfg = Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Release|x64.ActiveCfg = Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Release|x64.Build.0 = Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Release|x86.ActiveCfg = Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Release|x86.Build.0 = Release|Win32 - {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose_Release|Any CPU.ActiveCfg = Verbose_Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose_Release|x64.ActiveCfg = Verbose_Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose_Release|x64.Build.0 = Verbose_Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose_Release|x86.ActiveCfg = Verbose_Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose_Release|x86.Build.0 = Verbose_Release|Win32 - {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|Any CPU.ActiveCfg = Verbose_Release|Win32 - {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|Any CPU.Build.0 = Verbose_Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|x64.ActiveCfg = Verbose_Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|x64.Build.0 = Verbose_Release|x64 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|x86.ActiveCfg = Verbose_Release|Win32 {88609E16-731F-46C9-8139-6B1A7A83240D}.Verbose|x86.Build.0 = Verbose_Release|Win32 - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|Any CPU.Build.0 = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|x64.ActiveCfg = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|x64.Build.0 = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|x86.ActiveCfg = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Debug|x86.Build.0 = Debug|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Release|Any CPU.Build.0 = Release|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Release|x64.ActiveCfg = Release|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Release|x86.ActiveCfg = Release|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x64.ActiveCfg = Release|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x86.ActiveCfg = Release|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|Any CPU.ActiveCfg = Release|Any CPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|Any CPU.Build.0 = Release|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x64.ActiveCfg = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x64.Build.0 = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x86.ActiveCfg = Debug|Any CPU {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x86.Build.0 = Debug|Any CPU - {880A3560-82CD-4836-996B-11BEFE6B44DB}.Debug|Any CPU.ActiveCfg = Debug|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Debug|x64.ActiveCfg = Debug|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Debug|x64.Build.0 = Debug|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Debug|x86.ActiveCfg = Debug|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Debug|x86.Build.0 = Debug|x86 - {880A3560-82CD-4836-996B-11BEFE6B44DB}.Release|Any CPU.ActiveCfg = Release|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Release|x64.ActiveCfg = Release|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Release|x64.Build.0 = Release|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Release|x86.ActiveCfg = Release|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Release|x86.Build.0 = Release|x86 - {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose_Release|Any CPU.ActiveCfg = Verbose_Release|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose_Release|x64.ActiveCfg = Verbose_Release|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose_Release|x64.Build.0 = Verbose_Release|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose_Release|x86.ActiveCfg = Verbose_Release|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose_Release|x86.Build.0 = Verbose_Release|x86 - {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose|Any CPU.ActiveCfg = Verbose|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose|x64.ActiveCfg = Verbose|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose|x64.Build.0 = Verbose|x64 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose|x86.ActiveCfg = Verbose|x86 {880A3560-82CD-4836-996B-11BEFE6B44DB}.Verbose|x86.Build.0 = Verbose|x86 - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|Any CPU.Build.0 = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|x64.ActiveCfg = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|x64.Build.0 = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|x86.ActiveCfg = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Debug|x86.Build.0 = Debug|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|Any CPU.Build.0 = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|x64.ActiveCfg = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|x64.Build.0 = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|x86.ActiveCfg = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Release|x86.Build.0 = Release|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|x64.ActiveCfg = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|x64.Build.0 = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|x86.ActiveCfg = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose_Release|x86.Build.0 = Release|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|Any CPU.ActiveCfg = Release|Any CPU - {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|Any CPU.Build.0 = Release|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|x64.ActiveCfg = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|x64.Build.0 = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|x86.ActiveCfg = Debug|Any CPU {10F0057C-6C1E-41AA-A4DE-2F9D2EABE55C}.Verbose|x86.Build.0 = Debug|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|x64.ActiveCfg = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|x64.Build.0 = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|x86.ActiveCfg = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Debug|x86.Build.0 = Debug|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|Any CPU.Build.0 = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|x64.ActiveCfg = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|x64.Build.0 = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|x86.ActiveCfg = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Release|x86.Build.0 = Release|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|x64.ActiveCfg = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|x64.Build.0 = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|x86.ActiveCfg = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose_Release|x86.Build.0 = Release|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|Any CPU.ActiveCfg = Release|Any CPU - {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|Any CPU.Build.0 = Release|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|x64.ActiveCfg = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|x64.Build.0 = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|x86.ActiveCfg = Debug|Any CPU {BBBA5CAD-B40E-4565-AE96-E8EC468DB54B}.Verbose|x86.Build.0 = Debug|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|Any CPU.Build.0 = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|x64.ActiveCfg = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|x64.Build.0 = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|x86.ActiveCfg = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Debug|x86.Build.0 = Debug|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|Any CPU.Build.0 = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|x64.ActiveCfg = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|x64.Build.0 = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|x86.ActiveCfg = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Release|x86.Build.0 = Release|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|x64.ActiveCfg = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|x64.Build.0 = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|x86.ActiveCfg = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose_Release|x86.Build.0 = Release|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|Any CPU.ActiveCfg = Release|Any CPU - {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|Any CPU.Build.0 = Release|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|x64.ActiveCfg = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|x64.Build.0 = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|x86.ActiveCfg = Debug|Any CPU {0DEDB099-9A26-4069-A4C1-A76CEB16283B}.Verbose|x86.Build.0 = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Debug|x64.Build.0 = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Debug|x86.Build.0 = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Release|x64.ActiveCfg = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Release|x64.Build.0 = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Release|x86.ActiveCfg = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Release|x86.Build.0 = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose_Release|x64.ActiveCfg = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose_Release|x64.Build.0 = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose_Release|x86.ActiveCfg = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose_Release|x86.Build.0 = Release|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose|x64.ActiveCfg = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose|x64.Build.0 = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose|x86.ActiveCfg = Debug|Any CPU + {B25EEC48-A5D0-4A63-BA73-5DD43F7F592A}.Verbose|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BuildTools b/BuildTools index 41c3a12d..b04769a3 160000 --- a/BuildTools +++ b/BuildTools @@ -1 +1 @@ -Subproject commit 41c3a12d56de96a3495893d1fea4a485a98c67af +Subproject commit b04769a3aebdd111b81f5a59d438907310e83207 diff --git a/Common.props b/Common.props index c3f703e4..f33175a4 100644 --- a/Common.props +++ b/Common.props @@ -11,5 +11,17 @@ AllEnabledByDefault latest + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers + + \ No newline at end of file diff --git a/Common.targets b/Common.targets index 56750be5..f6c26ae1 100644 --- a/Common.targets +++ b/Common.targets @@ -1,8 +1,4 @@  - - - - \ No newline at end of file diff --git a/Doorstop/Proxy/main.c b/Doorstop/Proxy/main.c index 5d3d51f9..c925a1a3 100644 --- a/Doorstop/Proxy/main.c +++ b/Doorstop/Proxy/main.c @@ -111,6 +111,13 @@ void unhandledException(void* exc, void* data) // We use this since it will always be called once to initialize Mono's JIT void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_version) { + const BOOL debugger_already_initialized = mono_debug_enabled(); + + if(debugger_already_initialized) + { + LOG("Debugger was already initialized\n"); + } + // Call the original mono_jit_init_version to initialize the Unity Root Domain if (debug) { char* opts[1]; @@ -118,14 +125,14 @@ void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_ve ownMonoJitParseOptions(0, opts); } #ifdef WIN32 - if (debug_info) { + if (debug_info && !debugger_already_initialized) { mono_debug_init(MONO_DEBUG_FORMAT_MONO); } #endif void *domain = mono_jit_init_version(root_domain_name, runtime_version); - if (debug_info) { + if (debug_info && !debugger_already_initialized) { #ifdef WIN64 mono_debug_init(MONO_DEBUG_FORMAT_MONO); #endif @@ -194,8 +201,10 @@ void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_ve wchar_t* dll_path_w; // self path size_t dll_path_len = get_module_path((HINSTANCE)&__ImageBase, &dll_path_w, NULL, 0); - char* self_dll_path = memalloc(dll_path_len + 1); - WideCharToMultiByte(CP_UTF8, 0, dll_path_w, -1, self_dll_path, dll_path_len + 1, NULL, NULL); + size_t multibyte_path_len = WideCharToMultiByte(CP_UTF8, 0, dll_path_w, dll_path_len, NULL, 0, NULL, NULL); + char* self_dll_path = memalloc(multibyte_path_len + 1); + WideCharToMultiByte(CP_UTF8, 0, dll_path_w, dll_path_len, self_dll_path, multibyte_path_len + 1, NULL, NULL); + self_dll_path[multibyte_path_len] = 0; mono_dllmap_insert(NULL, "i:bsipa-doorstop", NULL, self_dll_path, NULL); // remap `bsipa-doorstop` to this assembly diff --git a/Doorstop/Proxy/mono.h b/Doorstop/Proxy/mono.h index 6eb41f41..f3ed2c4a 100644 --- a/Doorstop/Proxy/mono.h +++ b/Doorstop/Proxy/mono.h @@ -40,6 +40,7 @@ typedef enum { void (*mono_jit_parse_options)(int argc, char * argv[]); void (*mono_debug_init)(MonoDebugFormat format); +BOOL (*mono_debug_enabled)(void); void (*mono_debug_domain_create)(void*); void *(*mono_jit_init_version)(const char *root_domain_name, const char *runtime_version); @@ -90,6 +91,7 @@ inline void loadMonoFunctions(HMODULE monoLib) GET_MONO_PROC(mono_assembly_get_image); GET_MONO_PROC(mono_runtime_invoke); GET_MONO_PROC(mono_debug_init); + GET_MONO_PROC(mono_debug_enabled); GET_MONO_PROC(mono_jit_init_version); GET_MONO_PROC(mono_jit_parse_options); GET_MONO_PROC(mono_method_desc_new); diff --git a/IPA.Injector/GameVersionEarly.cs b/IPA.Injector/GameVersionEarly.cs index e6e0280c..5ed1ace7 100644 --- a/IPA.Injector/GameVersionEarly.cs +++ b/IPA.Injector/GameVersionEarly.cs @@ -1,4 +1,5 @@ -using IPA.Utilities; +#nullable enable +using IPA.Utilities; using System; using System.Collections.Generic; using System.IO; @@ -48,7 +49,7 @@ namespace IPA.Injector } var rewind = -sizeof(int) - sizeof(byte); - stream.Seek(rewind, SeekOrigin.Current); // rewind to the string length + _ = stream.Seek(rewind, SeekOrigin.Current); // rewind to the string length var strlen = reader.ReadInt32(); var strbytes = reader.ReadBytes(strlen); @@ -57,7 +58,7 @@ namespace IPA.Injector } } - internal static AlmostVersion SafeParseVersion() => new AlmostVersion(GetGameVersion()); + internal static AlmostVersion SafeParseVersion() => new(GetGameVersion()); private static void _Load() { diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj index a100bf44..1fbe99af 100644 --- a/IPA.Injector/IPA.Injector.csproj +++ b/IPA.Injector/IPA.Injector.csproj @@ -1,18 +1,18 @@  - - + + - net461;net35 + net472 IPA.Injector - + true false false - true + true - + $(DefineConstants);NET4 @@ -21,14 +21,14 @@ $(DefineConstants);BeatSaber - + - + - + ..\Refs\UnityEngine.CoreModule.Net4.dll False @@ -37,8 +37,8 @@ False - - + + Libraries\Mono\I18N.dll Always @@ -55,7 +55,19 @@ Libraries\Mono\System.Runtime.Serialization.dll Always + + Libraries\Mono\netstandard.dll + Always + + + + + Libraries\Thirdparty\%(Filename)%(Extension) + Always + + + Libraries\Mono\I18N.dll @@ -70,13 +82,22 @@ Always - + - - + + + - + + + + + + + + + @@ -91,7 +112,7 @@ - + @@ -102,6 +123,6 @@ - + diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 1ea7e058..7d338af7 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -1,4 +1,6 @@ -using IPA.Config; +#nullable enable +using IPA.AntiMalware; +using IPA.Config; using IPA.Injector.Backups; using IPA.Loader; using IPA.Logging; @@ -29,8 +31,8 @@ namespace IPA.Injector // ReSharper disable once UnusedMember.Global internal static class Injector { - private static Task pluginAsyncLoadTask; - private static Task permissionFixTask; + private static Task? pluginAsyncLoadTask; + private static Task? permissionFixTask; //private static string otherNewtonsoftJson = null; // ReSharper disable once UnusedParameter.Global @@ -40,7 +42,7 @@ namespace IPA.Injector // and since this class doesn't have any static fields that // aren't defined in mscorlib, we can control exactly what // gets loaded. - + _ = args; try { if (Environment.GetCommandLineArgs().Contains("--verbose")) @@ -48,16 +50,6 @@ namespace IPA.Injector SetupLibraryLoading(); - /*var otherNewtonsoft = Path.Combine( - Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(), - "Managed", - "Newtonsoft.Json.dll"); - if (File.Exists(otherNewtonsoft)) - { // this game ships its own Newtonsoft; force load ours and flag loading theirs - LibLoader.LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed")); - otherNewtonsoftJson = otherNewtonsoft; - }*/ - EnsureDirectories(); // this is weird, but it prevents Mono from having issues loading the type. @@ -71,7 +63,7 @@ namespace IPA.Injector */ #endregion - log.Debug("Initializing logger"); + Default.Debug("Initializing logger"); SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs()); SelfConfig.Load(); @@ -79,23 +71,27 @@ namespace IPA.Injector if (AntiPiracy.IsInvalid(Environment.CurrentDirectory)) { - log.Error("Invalid installation; please buy the game to run BSIPA."); + Default.Error("Invalid installation; please buy the game to run BSIPA."); return; } CriticalSection.Configure(); - injector.Debug("Prepping bootstrapper"); + Logging.Logger.Injector.Debug("Prepping bootstrapper"); + + // make sure to load the game version and check boundaries before installing the bootstrap, because that uses the game assemblies property + GameVersionEarly.Load(); + SelfConfig.Instance.CheckVersionBoundary(); // updates backup InstallBootstrapPatch(); - GameVersionEarly.Load(); + AntiMalwareEngine.Initialize(); Updates.InstallPendingUpdates(); - LibLoader.SetupAssemblyFilenames(true); + Loader.LibLoader.SetupAssemblyFilenames(true); pluginAsyncLoadTask = PluginLoader.LoadTask(); permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory)); @@ -110,16 +106,16 @@ namespace IPA.Injector { string path; if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData"))) - Directory.CreateDirectory(path); + _ = Directory.CreateDirectory(path); if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins"))) - Directory.CreateDirectory(path); + _ = Directory.CreateDirectory(path); } private static void SetupLibraryLoading() { if (loadingDone) return; loadingDone = true; - LibLoader.Configure(); + Loader.LibLoader.Configure(); } private static void InstallHarmonyProtections() @@ -137,13 +133,13 @@ namespace IPA.Injector var dataDir = new DirectoryInfo(managedPath).Parent.Name; var gameName = dataDir.Substring(0, dataDir.Length - 5); - injector.Debug("Finding backup"); + Logging.Logger.Injector.Debug("Finding backup"); var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName); var bkp = BackupManager.FindLatestBackup(backupPath); if (bkp == null) - injector.Warn("No backup found! Was BSIPA installed using the installer?"); + Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?"); - injector.Debug("Ensuring patch on UnityEngine.CoreModule exists"); + Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists"); #region Insert patch into UnityEngine.CoreModule.dll @@ -179,12 +175,12 @@ namespace IPA.Injector if (application == null) { - injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" + Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" + "Nothing to patch to get ourselves into the Unity run cycle!"); goto endPatchCoreModule; } - MethodDefinition cctor = null; + MethodDefinition? cctor = null; foreach (var m in application.Methods) if (m.IsRuntimeSpecialName && m.Name == ".cctor") cctor = m; @@ -244,7 +240,7 @@ namespace IPA.Injector endPatchCoreModule: #endregion Insert patch into UnityEngine.CoreModule.dll - injector.Debug("Ensuring game assemblies are virtualized"); + Logging.Logger.Injector.Debug("Ensuring game assemblies are virtualized"); #region Virtualize game assemblies bool isFirst = true; @@ -256,15 +252,15 @@ namespace IPA.Injector try { - injector.Debug($"Virtualizing {name}"); + Logging.Logger.Injector.Debug($"Virtualizing {name}"); using var ascModule = VirtualizedModule.Load(ascPath); ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath)); } catch (Exception e) { - injector.Error($"Could not virtualize {ascPath}"); + Logging.Logger.Injector.Error($"Could not virtualize {ascPath}"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - injector.Error(e); + Logging.Logger.Injector.Error(e); } #if BeatSaber @@ -272,7 +268,7 @@ namespace IPA.Injector { try { - injector.Debug("Applying anti-yeet patch"); + Logging.Logger.Injector.Debug("Applying anti-yeet patch"); using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters { @@ -291,9 +287,9 @@ namespace IPA.Injector } catch (Exception e) { - injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); + Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - injector.Warn(e); + Logging.Logger.Injector.Warn(e); } } #endif @@ -301,7 +297,7 @@ namespace IPA.Injector #endregion sw.Stop(); - injector.Info($"Installing bootstrapper took {sw.Elapsed}"); + Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}"); } private static bool bootstrapped; @@ -311,10 +307,6 @@ namespace IPA.Injector if (bootstrapped) return; bootstrapped = true; - /*if (otherNewtonsoftJson != null) - Assembly.LoadFrom(otherNewtonsoftJson);*/ - - Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type) { var level = UnityLogRedirector.LogTypeToLevel(type); @@ -322,6 +314,8 @@ namespace IPA.Injector UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}"); }; + StdoutInterceptor.EnsureHarmonyLogging(); + // need to reinit streams singe Unity seems to redirect stdout StdoutInterceptor.RedirectConsole(); @@ -336,12 +330,12 @@ namespace IPA.Injector private static void Bootstrapper_Destroyed() { // wait for plugins to finish loading - pluginAsyncLoadTask.Wait(); - permissionFixTask.Wait(); + pluginAsyncLoadTask?.Wait(); + permissionFixTask?.Wait(); - log.Debug("Plugins loaded"); - log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); - PluginComponent.Create(); + Default.Debug("Plugins loaded"); + Default.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); + _ = PluginComponent.Create(); } } } \ No newline at end of file diff --git a/IPA.Injector/PermissionFix.cs b/IPA.Injector/PermissionFix.cs index 7b531e52..152c6435 100644 --- a/IPA.Injector/PermissionFix.cs +++ b/IPA.Injector/PermissionFix.cs @@ -1,73 +1,73 @@ using IPA.Logging; -using System; +using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading.Tasks; -#if NET3 -using Net3_Proxy; -#endif - -namespace IPA.Injector -{ - internal static class PermissionFix +using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading.Tasks; +#if NET3 +using Net3_Proxy; +#endif + +namespace IPA.Injector +{ + internal static class PermissionFix { [SuppressMessage("Reliability", "CA2008:Do not create tasks without passing a TaskScheduler", Justification = "I very explicitly want the default scheduler")] - public static Task FixPermissions(DirectoryInfo root) - { - if (!root.Exists) return new Task(() => { }); - - return Task.Factory.StartNew(() => - { - var sw = Stopwatch.StartNew(); - try - { - var acl = root.GetAccessControl(); - - var rules = acl.GetAccessRules(true, true, typeof(SecurityIdentifier)); - - var requestedRights = FileSystemRights.Modify; - var requestedInheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - var requestedPropagation = PropagationFlags.InheritOnly; - - bool hasRule = false; - for (var i = 0; i < rules.Count; i++) - { - var rule = rules[i]; - - if (rule is FileSystemAccessRule fsrule - && fsrule.AccessControlType == AccessControlType.Allow - && fsrule.InheritanceFlags.HasFlag(requestedInheritance) - && fsrule.PropagationFlags == requestedPropagation - && fsrule.FileSystemRights.HasFlag(requestedRights)) - { hasRule = true; break; } - } - - if (!hasRule) - { // this is *sooo* fucking slow on first run - acl.AddAccessRule( - new FileSystemAccessRule( - new SecurityIdentifier(WellKnownSidType.WorldSid, null), - requestedRights, - requestedInheritance, - requestedPropagation, - AccessControlType.Allow - ) - ); - root.SetAccessControl(acl); - } - } - catch (Exception e) - { - Logger.log.Warn("Error configuring permissions in the game install dir"); - Logger.log.Warn(e); - } - sw.Stop(); - Logger.log.Info($"Configuring permissions took {sw.Elapsed}"); - }); - } - } -} + public static Task FixPermissions(DirectoryInfo root) + { + if (!root.Exists) return new Task(() => { }); + + return Task.Factory.StartNew(() => + { + var sw = Stopwatch.StartNew(); + try + { + var acl = root.GetAccessControl(); + + var rules = acl.GetAccessRules(true, true, typeof(SecurityIdentifier)); + + var requestedRights = FileSystemRights.Modify; + var requestedInheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; + var requestedPropagation = PropagationFlags.InheritOnly; + + bool hasRule = false; + for (var i = 0; i < rules.Count; i++) + { + var rule = rules[i]; + + if (rule is FileSystemAccessRule fsrule + && fsrule.AccessControlType == AccessControlType.Allow + && fsrule.InheritanceFlags.HasFlag(requestedInheritance) + && fsrule.PropagationFlags == requestedPropagation + && fsrule.FileSystemRights.HasFlag(requestedRights)) + { hasRule = true; break; } + } + + if (!hasRule) + { // this is *sooo* fucking slow on first run + acl.AddAccessRule( + new FileSystemAccessRule( + new SecurityIdentifier(WellKnownSidType.WorldSid, null), + requestedRights, + requestedInheritance, + requestedPropagation, + AccessControlType.Allow + ) + ); + root.SetAccessControl(acl); + } + } + catch (Exception e) + { + Logger.Default.Warn("Error configuring permissions in the game install dir"); + Logger.Default.Warn(e); + } + sw.Stop(); + Logger.Default.Info($"Configuring permissions took {sw.Elapsed}"); + }); + } + } +} diff --git a/IPA.Injector/Properties/AssemblyInfo.cs b/IPA.Injector/Properties/AssemblyInfo.cs index 5f18531f..bdafdad9 100644 --- a/IPA.Injector/Properties/AssemblyInfo.cs +++ b/IPA.Injector/Properties/AssemblyInfo.cs @@ -1,37 +1,39 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("IPA.Injector")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("IPA.Injector")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] -[assembly: InternalsVisibleTo("IPA.Loader")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion(IPA.Config.SelfConfig.IPAVersion)] +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("IPA.Injector")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("IPA.Injector")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] +[assembly: InternalsVisibleTo("IPA.Loader")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion(IPA.Config.SelfConfig.IPAVersion)] [assembly: AssemblyFileVersion(IPA.Config.SelfConfig.IPAVersion)] \ No newline at end of file diff --git a/IPA.Injector/Updates.cs b/IPA.Injector/Updates.cs index 1862f889..7822e209 100644 --- a/IPA.Injector/Updates.cs +++ b/IPA.Injector/Updates.cs @@ -1,4 +1,7 @@ -using IPA.Utilities; +#nullable enable +using IPA.AntiMalware; +using IPA.Config; +using IPA.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; @@ -36,14 +39,27 @@ namespace IPA.Injector if (ipaVersion > selfVersion) { + var scanResult = AntiMalwareEngine.Engine.ScanFile(new FileInfo(path)); + if (scanResult == ScanResult.Detected) + { + Updater.Error("Scan of BSIPA installer found malware; not updating"); + return; + } + if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && scanResult is not ScanResult.KnownSafe and not ScanResult.NotDetected) + { + Updater.Error("Scan of BSIPA installer returned partial threat; not updating. To allow this, enable AntiMalware.RunPartialThreatCode in the config."); + return; + } + _ = Process.Start(new ProcessStartInfo { FileName = path, - Arguments = $"\"-nw={Process.GetCurrentProcess().Id},s={string.Join(" ", Environment.GetCommandLineArgs().Skip(1).StrJP()).Replace("\\", "\\\\").Replace(",", "\\,")}\"", + Arguments = $"\"-nw={Process.GetCurrentProcess().Id}," + + $"s={string.Join(" ", Environment.GetCommandLineArgs().Skip(1).StrJP()).Replace("\\", "\\\\").Replace(",", "\\,")}\"", UseShellExecute = false }); - updater.Info("Updating BSIPA..."); + Updater.Info("Updating BSIPA..."); Environment.Exit(0); } } @@ -54,7 +70,7 @@ namespace IPA.Injector if (!Directory.Exists(pendingDir)) return; // there are pending updates, install - updater.Info("Installing pending updates"); + Updater.Info("Installing pending updates"); var toDelete = Array.Empty(); var delFn = Path.Combine(pendingDir, DeleteFileName); @@ -72,8 +88,8 @@ namespace IPA.Injector } catch (Exception e) { - updater.Error("While trying to install pending updates: Error deleting file marked for deletion"); - updater.Error(e); + Updater.Error("While trying to install pending updates: Error deleting file marked for deletion"); + Updater.Error(e); } } @@ -98,12 +114,12 @@ namespace IPA.Injector } catch (UnauthorizedAccessException e) { - updater.Error(e); + Updater.Error(e); continue; } catch (DirectoryNotFoundException e) { - updater.Error(e); + Updater.Error(e); continue; } @@ -116,7 +132,7 @@ namespace IPA.Injector } catch (FileNotFoundException e) { - updater.Error(e); + Updater.Error(e); } } @@ -137,15 +153,15 @@ namespace IPA.Injector { Utils.CopyAll(new DirectoryInfo(pendingDir), new DirectoryInfo(UnityGame.InstallPath), onCopyException: (e, f) => { - updater.Error($"Error copying file {Utils.GetRelativePath(f.FullName, pendingDir)} from Pending:"); - updater.Error(e); + Updater.Error($"Error copying file {Utils.GetRelativePath(f.FullName, pendingDir)} from Pending:"); + Updater.Error(e); return true; }); } catch (Exception e) { - updater.Error("While trying to install pending updates: Error copying files in"); - updater.Error(e); + Updater.Error("While trying to install pending updates: Error copying files in"); + Updater.Error(e); } try @@ -154,8 +170,8 @@ namespace IPA.Injector } catch (Exception e) { - updater.Error("Something went wrong performing an operation that should never fail!"); - updater.Error(e); + Updater.Error("Something went wrong performing an operation that should never fail!"); + Updater.Error(e); } } } diff --git a/IPA.Loader/AntiMalware/AmsiConstants.cs b/IPA.Loader/AntiMalware/AmsiConstants.cs new file mode 100644 index 00000000..d77aa55c --- /dev/null +++ b/IPA.Loader/AntiMalware/AmsiConstants.cs @@ -0,0 +1,14 @@ +#nullable enable +using System; + +namespace IPA.AntiMalware +{ + internal static class AmsiConstants + { + public const string AppName = "BSIPA/" + Config.SelfConfig.IPAVersion; + public const string IAntimalwareGuidStr = "82d29c2e-f062-44e6-b5c9-3d9a2f24a2df"; + public static readonly Guid IAntimalwareGuid = new(IAntimalwareGuidStr); + public static readonly Guid CAntimalwareGuid = new("fdb00e52-a214-4aa1-8fba-4357bb0072ec"); + public static readonly Guid IUnknownGuid = new("00000000-0000-0000-C000-000000000046"); + } +} diff --git a/IPA.Loader/AntiMalware/AmsiResult.cs b/IPA.Loader/AntiMalware/AmsiResult.cs new file mode 100644 index 00000000..ff755180 --- /dev/null +++ b/IPA.Loader/AntiMalware/AmsiResult.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace IPA.AntiMalware +{ + internal enum AmsiResult + { + Clean = 0, + NotDetected = 1, + BlockedByAdminStart = 0x4000, + BlockedByAdminEnd = 0x4fff, + Detected = 32768 + } +} diff --git a/IPA.Loader/AntiMalware/AntiMalwareEngine.cs b/IPA.Loader/AntiMalware/AntiMalwareEngine.cs new file mode 100644 index 00000000..dc9aef79 --- /dev/null +++ b/IPA.Loader/AntiMalware/AntiMalwareEngine.cs @@ -0,0 +1,46 @@ +#nullable enable +using IPA.Config; +using IPA.Logging; +using System; + +namespace IPA.AntiMalware +{ + /// + /// Provides a way to access BSIPA's Anti-Malware engine. + /// + /// + /// + public static class AntiMalwareEngine + { + private static IAntiMalware? engine; + + /// + /// Gets the current Anti-Malware engine. + /// + public static IAntiMalware Engine => engine ?? throw new InvalidOperationException(); + + internal static bool IsInitialized => engine != null; + + internal static void Initialize() + { + engine = CreateEngine(); + } + + private static IAntiMalware CreateEngine() + { + IAntiMalware? engine = null; + if (SelfConfig.AntiMalware_.UseIfAvailable_) + { +#if !NET35 + engine = WindowsCOMAntiMalware.TryInitialize(); +#endif + engine ??= WindowsWin32AntiMalware.TryInitialize(); + } + engine ??= new NoopAntiMalware(); + + Logger.AntiMalware.Debug($"Antimalware engine initialized with {engine.GetType()}"); + + return engine; + } + } +} diff --git a/IPA.Loader/AntiMalware/IAntiMalware.cs b/IPA.Loader/AntiMalware/IAntiMalware.cs new file mode 100644 index 00000000..5c8d37a6 --- /dev/null +++ b/IPA.Loader/AntiMalware/IAntiMalware.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.IO; + +namespace IPA.AntiMalware +{ + /// + /// An Anti-Malware engine that can be used to scan and detect potentially harmful files. + /// + public interface IAntiMalware + { + /// + /// Scans a particular file for malware. + /// + /// The file to scan. + /// A indicating whether the file is safe or not. + ScanResult ScanFile(FileInfo file); + /// + /// Scans a particular in-memory blob for malware. + /// + /// The binary blob to scan. + /// The name of the content. If this is left , one will be automatically generated. + /// A indicating whether the file is safe or not. + ScanResult ScanData(byte[] data, string? contentName = null); + } +} diff --git a/IPA.Loader/AntiMalware/NoopAntiMalware.cs b/IPA.Loader/AntiMalware/NoopAntiMalware.cs new file mode 100644 index 00000000..60609898 --- /dev/null +++ b/IPA.Loader/AntiMalware/NoopAntiMalware.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware +{ + internal class NoopAntiMalware : IAntiMalware + { + public ScanResult ScanData(byte[] data, string contentName = null) => ScanResult.NotDetected; + public ScanResult ScanFile(FileInfo file) => ScanResult.NotDetected; + } +} diff --git a/IPA.Loader/AntiMalware/ScanResult.cs b/IPA.Loader/AntiMalware/ScanResult.cs new file mode 100644 index 00000000..43d501eb --- /dev/null +++ b/IPA.Loader/AntiMalware/ScanResult.cs @@ -0,0 +1,27 @@ + +namespace IPA.AntiMalware +{ + /// + /// The result of an Anti-Malware scan. + /// + public enum ScanResult + { + /// + /// The object is known to be safe. + /// + KnownSafe, + /// + /// No malware was detected, but it is not known to be safe. + /// + NotDetected, + /// + /// Malware was detected, and the content should not be executed. + /// + Detected, + /// + /// The malware engine returned a threat level less than the max, so this object may be dangerous. + /// Proceed with caution. + /// + MaybeMalware + } +} diff --git a/IPA.Loader/AntiMalware/WindowsWin32AntiMalware.cs b/IPA.Loader/AntiMalware/WindowsWin32AntiMalware.cs new file mode 100644 index 00000000..43bfba69 --- /dev/null +++ b/IPA.Loader/AntiMalware/WindowsWin32AntiMalware.cs @@ -0,0 +1,111 @@ +#nullable enable +using IPA.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware +{ + internal class WindowsWin32AntiMalware : IAntiMalware, IDisposable + { + internal static WindowsWin32AntiMalware? TryInitialize() + { + try + { + return new(); + } + catch (Exception e) + { + Logger.AntiMalware.Warn("Could not initialize Win32-based antimalware engine:"); + Logger.AntiMalware.Warn(e); + return null; + } + } + + private readonly IntPtr handle; + private bool disposedValue; + + private WindowsWin32AntiMalware() + { + AmsiInitialize(AmsiConstants.AppName, out handle); + } + + private static ScanResult ScanResultFromAmsiResult(AmsiResult result) + => result switch + { + AmsiResult.Clean => ScanResult.KnownSafe, + AmsiResult.NotDetected => ScanResult.NotDetected, + AmsiResult.Detected => ScanResult.Detected, + _ => ScanResult.MaybeMalware + }; + + public ScanResult ScanFile(FileInfo file) + { + var data = File.ReadAllBytes(file.FullName); + return ScanData(data, file.FullName); + } + + public ScanResult ScanData(byte[] data, string? contentName = null) + { + contentName ??= $"unknown_data_{Guid.NewGuid()}"; + + AmsiScanBuffer(handle, data, (uint)data.Length, contentName, IntPtr.Zero, out var result); + + Logger.AntiMalware.Trace($"Scanned data named '{contentName}' and got '{result}'"); + return ScanResultFromAmsiResult(result); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // we have no disposable managed state + } + + AmsiUninitialize(handle); + disposedValue = true; + } + } + + ~WindowsWin32AntiMalware() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + [DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] +#if !NET35 + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] +#endif + private static extern void AmsiInitialize([MarshalAs(UnmanagedType.LPWStr)] string appName, [Out] out IntPtr handle); + + [DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] +#if !NET35 + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] +#endif + private static extern void AmsiUninitialize(IntPtr handle); + + [DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] +#if !NET35 + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] +#endif + private static extern void AmsiScanBuffer(IntPtr context, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] buffer, uint length, + [MarshalAs(UnmanagedType.LPWStr)] string contentName, + IntPtr session, + [Out] out AmsiResult result); + } +} diff --git a/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiFileStream.cs b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiFileStream.cs new file mode 100644 index 00000000..be0d383f --- /dev/null +++ b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiFileStream.cs @@ -0,0 +1,113 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware.ComAPI +{ + internal class AmsiFileStream : IAmsiStream, IDisposable + { + private readonly FileInfo file; + private readonly IntPtr session; + + public AmsiFileStream(FileInfo file, IntPtr session) + { + this.file = file; + this.session = session; + } + + public unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, out uint writtenData) + { + switch (attribute) + { + case AmsiAttribute.AppName: + writtenData = WriteWString(AmsiConstants.AppName, dataSize, buffer); + return; + case AmsiAttribute.Session: + *(IntPtr*)buffer = session; + writtenData = (uint)sizeof(IntPtr); + return; + + case AmsiAttribute.ContentName: + writtenData = WriteWString(file.FullName, dataSize, buffer); + return; + + case AmsiAttribute.ContentSize: + *(ulong*)buffer = (ulong)file.Length; + writtenData = sizeof(ulong); + return; + + default: + throw new NotImplementedException(); // return e_notimpl + } + + static unsafe uint WriteWString(string str, uint dataSize, byte* buffer) + { + fixed (char* name = str) + { + return (uint)Encoding.Unicode.GetBytes(name, str.Length, buffer, (int)dataSize); + } + } + } + + private FileStream? stream; + private bool disposedValue; + private readonly byte[] readBuffer = new byte[1024]; + + public unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, out uint readSize) + { + stream ??= file.OpenRead(); + + stream.Position = (long)position; + + var bytesToRead = dataSize; + readSize = 0; + + while (bytesToRead > 0) + { + var bytesRead = stream.Read(readBuffer, 0, (int)Math.Min(readBuffer.Length, bytesToRead)); + if (bytesRead == 0) + { + break; + } + fixed (byte* readBufferPtr = readBuffer) + { + Buffer.MemoryCopy(readBufferPtr, buffer + readSize, dataSize - readSize, bytesRead); + } + bytesToRead -= (uint)bytesRead; + readSize += (uint)bytesRead; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + stream?.Dispose(); + } + + disposedValue = true; + } + } + + // This does not have unmanagd resources, so it doesn't need to exist + // ~AmsiFileStream() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiMemoryStream.cs b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiMemoryStream.cs new file mode 100644 index 00000000..a611fa7a --- /dev/null +++ b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiMemoryStream.cs @@ -0,0 +1,114 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware.ComAPI +{ + internal class AmsiMemoryStream : IAmsiStream, IDisposable + { + private readonly string contentName; + private readonly byte[] data; + private readonly GCHandle dataHandle; + + private readonly IntPtr session; + private bool disposedValue; + + public AmsiMemoryStream(string contentName, byte[] data, IntPtr session) + { + this.data = data; + dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + this.session = session; + this.contentName = contentName; + } + + public unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, out uint writtenData) + { + switch (attribute) + { + case AmsiAttribute.AppName: + writtenData = WriteWString(AmsiConstants.AppName, dataSize, buffer); + return; + case AmsiAttribute.Session: + *(IntPtr*)buffer = session; + writtenData = (uint)sizeof(IntPtr); + return; + + case AmsiAttribute.ContentName: + writtenData = WriteWString(contentName, dataSize, buffer); + return; + + case AmsiAttribute.ContentSize: + *(ulong*)buffer = (ulong)data.Length; + writtenData = sizeof(ulong); + return; + + case AmsiAttribute.ContentAddress: + // because our data is pinned, it can't move while this object exists so we can pass out the fixed address + fixed (byte* dataAddr = data) + { + *(byte**)buffer = dataAddr; + } + writtenData = (uint)sizeof(IntPtr); + return; + + default: + throw new NotImplementedException(); // return e_notimpl + } + + static unsafe uint WriteWString(string str, uint dataSize, byte* buffer) + { + fixed (char* name = str) + { + return (uint)Encoding.Unicode.GetBytes(name, str.Length, buffer, (int)dataSize); + } + } + } + + public unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, out uint readSize) + { + if (position >= (ulong)data.Length) + { + throw new EndOfStreamException(); + } + + fixed (byte* dataPtr = data) + { + var toRead = Math.Min((ulong)data.Length - position, dataSize); + Buffer.MemoryCopy(dataPtr + position, buffer, dataSize, toRead); + readSize = (uint)toRead; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // no managed stae to dispose + } + + dataHandle.Free(); + disposedValue = true; + } + } + + ~AmsiMemoryStream() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/IAntimalware.cs b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/IAntimalware.cs new file mode 100644 index 00000000..109fce79 --- /dev/null +++ b/IPA.Loader/AntiMalware/_HideInNet3/ComAPI/IAntimalware.cs @@ -0,0 +1,52 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware.ComAPI +{ + [ComImport] + [Guid(AmsiConstants.IAntimalwareGuidStr)] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAntimalware + { + void Scan([In] IAmsiStream stream, [Out] out AmsiResult result, [Out] out IAntimalwareProvider provider); + void CloseSession([In] ulong session); + } + + [ComImport] + [Guid("3e47f2e5-81d4-4d3b-897f-545096770373")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAmsiStream + { + unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, [Out] out uint writtenData); + unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, [Out] out uint readSize); + } + + [ComImport] + [Guid("b2cabfe3-fe04-42b1-a5df-08d483d4d125")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAntimalwareProvider + { + [return: MarshalAs(UnmanagedType.LPWStr)] string DisplayName(); + + AmsiResult Scan([In] IAmsiStream stream); + void CloseSession([In] ulong session); + } + + internal enum AmsiAttribute + { + AppName = 0, + ContentName = 1, + ContentSize = 2, + ContentAddress = 3, + Session = 4, + RedirectChainSize = 5, + RedirectChainAddress = 6, + AllSize = 7, + AllAddress = 8, + } +} diff --git a/IPA.Loader/AntiMalware/_HideInNet3/WindowsCOMAntiMalware.cs b/IPA.Loader/AntiMalware/_HideInNet3/WindowsCOMAntiMalware.cs new file mode 100644 index 00000000..70868bd2 --- /dev/null +++ b/IPA.Loader/AntiMalware/_HideInNet3/WindowsCOMAntiMalware.cs @@ -0,0 +1,88 @@ +#nullable enable +using IPA.AntiMalware.ComAPI; +using IPA.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.AntiMalware +{ + internal class WindowsCOMAntiMalware : IAntiMalware + { + internal static WindowsCOMAntiMalware? TryInitialize() + { + // Mono's COM interop *fundamentally doesn't work.* + // End of story. +#if false + try + { + return new(); + } + catch (Exception e) + { + Logger.AntiMalware.Warn("Could not initialize COM-based antimalware engine:"); + Logger.AntiMalware.Warn(e); + } +#endif + return null; + } + + private readonly IAntimalware amInterface; + + private WindowsCOMAntiMalware() + { + var hr = CoCreateInstanceAM(AmsiConstants.CAntimalwareGuid, + null, + 0x1 | 0x4 /* inproc server, local server */, + AmsiConstants.IAntimalwareGuid, + out var antimalware); + Marshal.ThrowExceptionForHR(hr); + + amInterface = antimalware; + } + + [DllImport("ole32", + CallingConvention = CallingConvention.Winapi, + ExactSpelling = true, + PreserveSig = false, + EntryPoint = "CoCreateInstance")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern int CoCreateInstanceAM( + [In] in Guid clsid, + [In, MarshalAs(UnmanagedType.Interface)] object? unkOuter, + [In] int dwClsContext, + [In] in Guid iid, + [Out, MarshalAs(UnmanagedType.Interface)] out IAntimalware @interface); + + + private static ScanResult ScanResultFromAmsiResult(AmsiResult result) + => result switch + { + AmsiResult.Clean => ScanResult.KnownSafe, + AmsiResult.NotDetected => ScanResult.NotDetected, + AmsiResult.Detected => ScanResult.Detected, + _ => ScanResult.MaybeMalware + }; + + public ScanResult ScanFile(FileInfo file) + { + using var stream = new AmsiFileStream(file, IntPtr.Zero); + amInterface.Scan(stream, out var result, out var provider); + Logger.AntiMalware.Trace($"Scanned file '{file}' with {provider.DisplayName()}, and got '{result}'"); + return ScanResultFromAmsiResult(result); + } + + public ScanResult ScanData(byte[] data, string? contentName = null) + { + contentName ??= $"unknown_data_{Guid.NewGuid()}"; + using var stream = new AmsiMemoryStream(contentName, data, IntPtr.Zero); + amInterface.Scan(stream, out var result, out var provider); + Logger.AntiMalware.Trace($"Scanned data named '{contentName}' with {provider.DisplayName()}, and got '{result}'"); + return ScanResultFromAmsiResult(result); + } + } +} diff --git a/IPA.Loader/Config/Config.cs b/IPA.Loader/Config/Config.cs index 0e4c19f1..d818e5b8 100644 --- a/IPA.Loader/Config/Config.cs +++ b/IPA.Loader/Config/Config.cs @@ -81,7 +81,7 @@ namespace IPA.Config /// Registers a to use for configs. /// /// the type to register - public static void Register() where T : IConfigProvider => Register(typeof(T)); + public static void Register() where T : IConfigProvider, new() => Register(typeof(T)); /// /// Registers a to use for configs. diff --git a/IPA.Loader/Config/ConfigRuntime.cs b/IPA.Loader/Config/ConfigRuntime.cs index d9c4a203..baf5138f 100644 --- a/IPA.Loader/Config/ConfigRuntime.cs +++ b/IPA.Loader/Config/ConfigRuntime.cs @@ -185,8 +185,8 @@ namespace IPA.Config } catch (Exception e) { - Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk"); - Logger.config.Error(e); + Logger.Config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk"); + Logger.Config.Error(e); } } @@ -209,8 +209,8 @@ namespace IPA.Config } catch (Exception e) { - Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}"); - Logger.config.Error(e); + Logger.Config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}"); + Logger.Config.Error(e); } } @@ -235,8 +235,8 @@ namespace IPA.Config } catch (Exception e) { - Logger.config.Error($"Error waiting for in-memory updates"); - Logger.config.Error(e); + Logger.Config.Error($"Error waiting for in-memory updates"); + Logger.Config.Error(e); Thread.Sleep(TimeSpan.FromSeconds(1)); } diff --git a/IPA.Loader/Config/Data/List.cs b/IPA.Loader/Config/Data/List.cs index d4f48397..764c619b 100644 --- a/IPA.Loader/Config/Data/List.cs +++ b/IPA.Loader/Config/Data/List.cs @@ -1,4 +1,5 @@ -using IPA.Utilities; +#nullable enable +using IPA.Utilities; using System; using System.Collections; using System.Collections.Generic; @@ -10,9 +11,9 @@ namespace IPA.Config.Data /// A list of s for serialization by an . /// Use or to create. /// - public sealed class List : Value, IList + public sealed class List : Value, IList { - private readonly List values = new List(); + private readonly List values = new(); internal List() { } @@ -22,7 +23,7 @@ namespace IPA.Config.Data /// the index to retrieve the at /// the at /// - public Value this[int index] { get => values[index]; set => values[index] = value; } + public Value? this[int index] { get => values[index]; set => values[index] = value; } /// /// Gets the number of elements in the . @@ -30,21 +31,22 @@ namespace IPA.Config.Data /// public int Count => values.Count; - bool ICollection.IsReadOnly => ((IList)values).IsReadOnly; + bool ICollection.IsReadOnly => ((IList)values).IsReadOnly; /// /// Adds a to the end of this . /// /// the to add /// - public void Add(Value item) => values.Add(item); + public void Add(Value? item) => values.Add(item); /// /// Adds a range of s to the end of this . /// /// the range of s to add - public void AddRange(IEnumerable vals) + public void AddRange(IEnumerable vals) { + if (vals is null) throw new ArgumentNullException(nameof(vals)); foreach (var val in vals) Add(val); } @@ -60,7 +62,7 @@ namespace IPA.Config.Data /// the to check for /// if the item was founc, otherwise /// - public bool Contains(Value item) => values.Contains(item); + public bool Contains(Value? item) => values.Contains(item); /// /// Copies the s in the to the in . @@ -68,14 +70,14 @@ namespace IPA.Config.Data /// the to copy to /// the starting index to copy to /// - public void CopyTo(Value[] array, int arrayIndex) => values.CopyTo(array, arrayIndex); + public void CopyTo(Value?[] array, int arrayIndex) => values.CopyTo(array, arrayIndex); /// /// Gets an enumerator to enumerate the . /// /// an for this /// - public IEnumerator GetEnumerator() => ((IList)values).GetEnumerator(); + public IEnumerator GetEnumerator() => ((IList)values).GetEnumerator(); /// /// Gets the index that a given is in the . @@ -83,7 +85,7 @@ namespace IPA.Config.Data /// the to search for /// the index that the was at, or -1. /// - public int IndexOf(Value item) => values.IndexOf(item); + public int IndexOf(Value? item) => values.IndexOf(item); /// /// Inserts a at an index. @@ -91,7 +93,7 @@ namespace IPA.Config.Data /// the index to insert at /// the to insert /// - public void Insert(int index, Value item) => values.Insert(index, item); + public void Insert(int index, Value? item) => values.Insert(index, item); /// /// Removes a from the . @@ -99,7 +101,7 @@ namespace IPA.Config.Data /// the to remove /// if the item was removed, otherwise /// - public bool Remove(Value item) => values.Remove(item); + public bool Remove(Value? item) => values.Remove(item); /// /// Removes a at an index. @@ -115,7 +117,7 @@ namespace IPA.Config.Data public override string ToString() => $"[{string.Join(",",this.Select(v => v?.ToString() ?? "null").StrJP())}]"; - IEnumerator IEnumerable.GetEnumerator() => ((IList)values).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/IPA.Loader/Config/Data/Map.cs b/IPA.Loader/Config/Data/Map.cs index 21be20b4..8ed8f17c 100644 --- a/IPA.Loader/Config/Data/Map.cs +++ b/IPA.Loader/Config/Data/Map.cs @@ -1,4 +1,5 @@ -using IPA.Utilities; +#nullable enable +using IPA.Utilities; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -10,10 +11,10 @@ namespace IPA.Config.Data /// A ordered map of to for serialization by an . /// Use or to create. /// - public sealed class Map : Value, IDictionary + public sealed class Map : Value, IDictionary { - private readonly Dictionary values = new Dictionary(); - private readonly List keyOrder = new List(); + private readonly Dictionary values = new(); + private readonly List keyOrder = new(); internal Map() { } @@ -23,7 +24,7 @@ namespace IPA.Config.Data /// the key to get the value associated with /// the value associated with the /// - public Value this[string key] { get => values[key]; set => values[key] = value; } + public Value? this[string key] { get => values[key]; set => values[key] = value; } /// /// Gets a collection of the keys for the . @@ -39,7 +40,7 @@ namespace IPA.Config.Data /// guarantee that order is maintained. /// /// - public ICollection Values => values.Values; + public ICollection Values => values.Values; /// /// Gets the number of key-value pairs in this . @@ -47,7 +48,7 @@ namespace IPA.Config.Data /// public int Count => values.Count; - bool ICollection>.IsReadOnly => ((IDictionary)values).IsReadOnly; + bool ICollection>.IsReadOnly => ((IDictionary)values).IsReadOnly; /// /// Adds a new with a given key. @@ -55,13 +56,13 @@ namespace IPA.Config.Data /// the key to put the value at /// the to add /// - public void Add(string key, Value value) + public void Add(string key, Value? value) { values.Add(key, value); keyOrder.Add(key); } - void ICollection>.Add(KeyValuePair item) + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); /// @@ -74,8 +75,8 @@ namespace IPA.Config.Data keyOrder.Clear(); } - bool ICollection>.Contains(KeyValuePair item) - => ((IDictionary)values).Contains(item); + bool ICollection>.Contains(KeyValuePair item) + => ((IDictionary)values).Contains(item); /// /// Checks if the contains a given . @@ -85,18 +86,18 @@ namespace IPA.Config.Data /// public bool ContainsKey(string key) => values.ContainsKey(key); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - => ((IDictionary)values).CopyTo(array, arrayIndex); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + => ((IDictionary)values).CopyTo(array, arrayIndex); /// /// Enumerates the 's key-value pairs. /// /// an of key-value pairs in this /// - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { foreach (var key in keyOrder) - yield return new KeyValuePair(key, this[key]); + yield return new KeyValuePair(key, this[key]); } /// @@ -107,8 +108,8 @@ namespace IPA.Config.Data /// public bool Remove(string key) => values.Remove(key) && keyOrder.Remove(key); - bool ICollection>.Remove(KeyValuePair item) - => ((IDictionary)values).Remove(item) && (keyOrder.Remove(item.Key) || true); + bool ICollection>.Remove(KeyValuePair item) + => ((IDictionary)values).Remove(item) && (keyOrder.Remove(item.Key) || true); /// /// Gets the value associated with the specified key. @@ -117,7 +118,7 @@ namespace IPA.Config.Data /// the target location of the retrieved object /// if the key was found and set, otherwise /// - public bool TryGetValue(string key, out Value value) => values.TryGetValue(key, out value); + public bool TryGetValue(string key, out Value? value) => values.TryGetValue(key, out value); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/IPA.Loader/Config/Data/Primitives.cs b/IPA.Loader/Config/Data/Primitives.cs index c03b3c90..5abc6d88 100644 --- a/IPA.Loader/Config/Data/Primitives.cs +++ b/IPA.Loader/Config/Data/Primitives.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -12,10 +13,28 @@ namespace IPA.Config.Data /// public sealed class Text : Value { + /// + /// Constructs an empty object. + /// + [Obsolete("Use the String constructor.")] + public Text() + { + Value = null!; + } + + /// + /// Constructs a object containing the provided value. + /// + /// The value to construct with. + public Text(string value) + { + Value = value; + } + /// /// The actual value of this object. /// - public string Value { get; set; } + public string Value { get; init; } /// /// Converts this into a human-readable format. @@ -30,6 +49,24 @@ namespace IPA.Config.Data /// public sealed class Integer : Value { + /// + /// Constructs an empty object. + /// + [Obsolete("Use the long constructor.")] + public Integer() + { + Value = 0; + } + + /// + /// Constructs a object containing the provided value. + /// + /// The value to construct with. + public Integer(long value) + { + Value = value; + } + /// /// The actual value of the object. /// @@ -50,10 +87,28 @@ namespace IPA.Config.Data /// /// A representing a floating point value. This may hold a - /// 's worth of data. + /// 's worth of data. /// public sealed class FloatingPoint : Value { + /// + /// Constructs an empty object. + /// + [Obsolete("Use the long constructor.")] + public FloatingPoint() + { + Value = 0; + } + + /// + /// Constructs a object containing the provided value. + /// + /// The value to construct with. + public FloatingPoint(decimal value) + { + Value = value; + } + /// /// The actual value fo this object. /// @@ -76,16 +131,37 @@ namespace IPA.Config.Data /// A representing a boolean value. /// public sealed class Boolean : Value - { + { + /// + /// Constructs an empty object. + /// + [Obsolete("Use the long constructor.")] + public Boolean() + { + Value = false; + } + + /// + /// Constructs a object containing the provided value. + /// + /// The value to construct with. + public Boolean(bool value) + { + Value = value; + } + /// /// The actual value fo this object. /// - public bool Value { get; set; } - + public bool Value { get; set; } + + /// /// Converts this into a human-readable format. /// /// the result of Value.ToString().ToLower() - public override string ToString() => Value.ToString().ToLower(); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", + Justification = "ToLower is the desired display value.")] + public override string ToString() => Value.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture); } } diff --git a/IPA.Loader/Config/Data/Value.cs b/IPA.Loader/Config/Data/Value.cs index 2e0e4eb7..966f844c 100644 --- a/IPA.Loader/Config/Data/Value.cs +++ b/IPA.Loader/Config/Data/Value.cs @@ -1,145 +1,146 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IPA.Config.Data -{ - /// - /// A base value type for config data abstract representations, to be serialized with an - /// . If a is , then - /// that represents just that: a null in whatever serialization is being used. - /// Also contains factory functions for all derived types. - /// - public abstract class Value - { - /// - /// Converts this into a human-readable format. - /// - /// a human-readable string containing the value provided - public abstract override string ToString(); - - /// - /// Creates a Null . - /// - /// - public static Value Null() => null; - - /// - /// Creates an empty . - /// - /// an empty - /// - public static List List() => new List(); - /// - /// Creates an empty . - /// - /// an empty - /// - /// - public static Map Map() => new Map(); - - /// - /// Creates a new representing a . - /// - /// the value to wrap - /// a wrapping - /// - public static Text From(string val) => Text(val); - /// - /// Creates a new object wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static Text Text(string val) => val == null ? null : new Text { Value = val }; - - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static Integer From(long val) => Integer(val); - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static Integer Integer(long val) => new Integer { Value = val }; - - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static FloatingPoint From(decimal val) => Float(val); - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static FloatingPoint Float(decimal val) => new FloatingPoint { Value = val }; - - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static Boolean From(bool val) => Bool(val); - /// - /// Creates a new wrapping a . - /// - /// the value to wrap - /// a wrapping - /// - public static Boolean Bool(bool val) => new Boolean { Value = val }; - - /// - /// Creates a new holding the content of an - /// of . - /// - /// the s to initialize the with - /// a containing the content of - /// - public static List From(IEnumerable vals) - { - if (vals == null) return null; - var l = List(); - l.AddRange(vals); - return l; - } - - /// - /// Creates a new holding the content of an - /// of to . - /// - /// the dictionary of s to initialize the wtih - /// a containing the content of - /// - /// - public static Map From(IDictionary vals) => From(vals as IEnumerable>); - - /// - /// Creates a new holding the content of an - /// of of to . - /// - /// the enumerable of of name to - /// a containing the content of - /// - /// - public static Map From(IEnumerable> vals) - { - if (vals == null) return null; - var m = Map(); - foreach (var v in vals) m.Add(v.Key, v.Value); - return m; - } - } -} +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace IPA.Config.Data +{ + /// + /// A base value type for config data abstract representations, to be serialized with an + /// . If a is , then + /// that represents just that: a null in whatever serialization is being used. + /// Also contains factory functions for all derived types. + /// + public abstract class Value + { + /// + /// Converts this into a human-readable format. + /// + /// a human-readable string containing the value provided + public abstract override string ToString(); + + /// + /// Creates a Null . + /// + /// + public static Value? Null() => null; + + /// + /// Creates an empty . + /// + /// an empty + /// + public static List List() => new(); + /// + /// Creates an empty . + /// + /// an empty + /// + /// + public static Map Map() => new(); + + /// + /// Creates a new representing a . + /// + /// the value to wrap + /// a wrapping + /// + [return: NotNullIfNotNull("val")] + public static Text? From(string? val) => Text(val); + /// + /// Creates a new object wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + [return: NotNullIfNotNull("val")] + public static Text? Text(string? val) => val == null ? null : new(val); + + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static Integer From(long val) => Integer(val); + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static Integer Integer(long val) => new(val); + + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static FloatingPoint From(decimal val) => Float(val); + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static FloatingPoint Float(decimal val) => new(val); + + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static Boolean From(bool val) => Bool(val); + /// + /// Creates a new wrapping a . + /// + /// the value to wrap + /// a wrapping + /// + public static Boolean Bool(bool val) => new(val); + + /// + /// Creates a new holding the content of an + /// of . + /// + /// the s to initialize the with + /// a containing the content of + /// + [return: NotNullIfNotNull("vals")] + public static List? From(IEnumerable? vals) + { + if (vals is null) return null; + var l = List(); + l.AddRange(vals); + return l; + } + + /// + /// Creates a new holding the content of an + /// of to . + /// + /// the dictionary of s to initialize the wtih + /// a containing the content of + /// + /// + public static Map From(IDictionary vals) => From(vals as IEnumerable>); + + /// + /// Creates a new holding the content of an + /// of of to . + /// + /// the enumerable of of name to + /// a containing the content of + /// + /// + [return: NotNullIfNotNull("vals")] + public static Map? From(IEnumerable>? vals) + { + if (vals is null) return null; + var m = Map(); + foreach (var v in vals) m.Add(v.Key, v.Value); + return m; + } + } +} diff --git a/IPA.Loader/Config/Providers/JsonConfigProvider.cs b/IPA.Loader/Config/Providers/JsonConfigProvider.cs index 250918cc..dd9c8dfb 100644 --- a/IPA.Loader/Config/Providers/JsonConfigProvider.cs +++ b/IPA.Loader/Config/Providers/JsonConfigProvider.cs @@ -39,8 +39,8 @@ namespace IPA.Config.Providers } catch (Exception e) { - Logger.config.Error($"Error reading JSON file {file.FullName}; ignoring"); - Logger.config.Error(e); + Logger.Config.Error($"Error reading JSON file {file.FullName}; ignoring"); + Logger.Config.Error(e); return Value.Null(); } } @@ -54,19 +54,19 @@ namespace IPA.Config.Providers case JTokenType.Raw: // idk if the parser will normally emit a Raw type, but just to be safe return VisitToValue(JToken.Parse((tok as JRaw).Value as string)); case JTokenType.Undefined: - Logger.config.Warn("Found JTokenType.Undefined"); + Logger.Config.Warn("Found JTokenType.Undefined"); goto case JTokenType.Null; case JTokenType.Bytes: // never used by Newtonsoft - Logger.config.Warn("Found JTokenType.Bytes"); + Logger.Config.Warn("Found JTokenType.Bytes"); goto case JTokenType.Null; case JTokenType.Comment: // never used by Newtonsoft - Logger.config.Warn("Found JTokenType.Comment"); + Logger.Config.Warn("Found JTokenType.Comment"); goto case JTokenType.Null; case JTokenType.Constructor: // never used by Newtonsoft - Logger.config.Warn("Found JTokenType.Constructor"); + Logger.Config.Warn("Found JTokenType.Constructor"); goto case JTokenType.Null; case JTokenType.Property: // never used by Newtonsoft - Logger.config.Warn("Found JTokenType.Property"); + Logger.Config.Warn("Found JTokenType.Property"); goto case JTokenType.Null; case JTokenType.Null: return Value.Null(); @@ -133,8 +133,8 @@ namespace IPA.Config.Providers } catch (Exception e) { - Logger.config.Error($"Error serializing value for {file.FullName}"); - Logger.config.Error(e); + Logger.Config.Error($"Error serializing value for {file.FullName}"); + Logger.Config.Error(e); } } diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index 4ad33bc6..abe30bea 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -36,7 +36,7 @@ namespace IPA.Config protected internal virtual void Changed() { - Logger.log.Debug("SelfConfig Changed called"); + Logger.Default.Debug("SelfConfig Changed called"); } public static void ReadCommandLine(string[] args) @@ -53,6 +53,12 @@ namespace IPA.Config case "--no-yeet": CommandLineValues.YeetMods = false; break; + case "--no-logs": + CommandLineValues.WriteLogs = false; + break; + case "--darken-message": + CommandLineValues.Debug.DarkenMessages = true; + break; case "--condense-logs": CommandLineValues.Debug.CondenseModLogs = true; break; @@ -72,13 +78,24 @@ namespace IPA.Config } } + public void CheckVersionBoundary() + { + if (ResetGameAssebliesOnVersionChange && Utilities.UnityGame.IsGameVersionBoundary) + { + GameAssemblies = GetDefaultGameAssemblies(); + } + } + internal const string IPAName = "Beat Saber IPA"; - internal const string IPAVersion = "4.1.7.0"; + internal const string IPAVersion = "4.2.2.0"; // uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug, // Debug.CondenseModLogs internal static SelfConfig CommandLineValues = new(); + // For readability's sake, I want the default values to be visible in source. +#pragma warning disable CA1805 // Do not initialize unnecessarily + // END: section ignore public virtual bool Regenerate { get; set; } = true; @@ -145,30 +162,64 @@ namespace IPA.Config public virtual bool SyncLogging { get; set; } = false; // LINE: ignore public static bool SyncLogging_ => Instance?.Debug?.SyncLogging ?? false; + + public virtual bool DarkenMessages { get; set; } = false; + // LINE: ignore 2 + public static bool DarkenMessages_ => (Instance?.Debug?.DarkenMessages ?? false) + || CommandLineValues.Debug.DarkenMessages; } // LINE: ignore [NonNullable] - public virtual Debug_ Debug { get; set; } = new Debug_(); + public virtual Debug_ Debug { get; set; } = new(); + + public class AntiMalware_ + { + public virtual bool UseIfAvailable { get; set; } = true; + // LINE: ignore + public static bool UseIfAvailable_ => Instance?.AntiMalware?.UseIfAvailable ?? true; + + public virtual bool RunPartialThreatCode { get; set; } = false; + // LINE: ignore + public static bool RunPartialThreatCode_ => Instance?.AntiMalware?.RunPartialThreatCode ?? true; + } + + // LINE: ignore + [NonNullable] + public virtual AntiMalware_ AntiMalware { get; set; } = new(); public virtual bool YeetMods { get; set; } = true; // LINE: ignore 2 public static bool YeetMods_ => (Instance?.YeetMods ?? true) && CommandLineValues.YeetMods; + [JsonIgnore] + public bool WriteLogs { get; set; } = true; + + public virtual bool ResetGameAssebliesOnVersionChange { get; set; } = true; + // LINE: ignore - [NonNullable, UseConverter(typeof(CollectionConverter>))] - public virtual HashSet GameAssemblies { get; set; } = new HashSet + [NonNullable, UseConverter(typeof(CollectionConverter>))] + public virtual HashSet GameAssemblies { get; set; } = GetDefaultGameAssemblies(); + + // BEGIN: section ignore + public static HashSet GetDefaultGameAssemblies() + => new() { - // LINE: ignore 5 #if BeatSaber // provide these defaults only for Beat Saber builds "Main.dll", "Core.dll", "HMLib.dll", "HMUI.dll", "HMRendering.dll", "VRUI.dll", "BeatmapCore.dll", "GameplayCore.dll", "HMLibAttributes.dll", "BeatmapEditor3D.dll" #else // otherwise specify Assembly-CSharp.dll "Assembly-CSharp.dll" - // LINE: ignore #endif }; + // END: section ignore + + // LINE: ignore +#if false // used to make schema gen happy + private static HashSet GetDefaultGameAssemblies() => null; + // LINE: ignore +#endif // LINE: ignore public static HashSet GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet { "Assembly-CSharp.dll" }; diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs index 574dfd80..66c63e16 100644 --- a/IPA.Loader/Config/Stores/Attributes.cs +++ b/IPA.Loader/Config/Stores/Attributes.cs @@ -1,6 +1,8 @@ -using IPA.Config.Stores.Converters; +#nullable enable +using IPA.Config.Stores.Converters; using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace IPA.Config.Stores.Attributes @@ -37,23 +39,25 @@ namespace IPA.Config.Stores.Attributes /// /// Gets whether or not to use the default converter for the member type instead of the specified type. /// + [MemberNotNullWhen(false, nameof(ConverterType))] public bool UseDefaultConverterForType { get; } /// /// Gets the type of the converter to use. /// - public Type ConverterType { get; } + public Type? ConverterType { get; } /// /// Gets the target type of the converter if it is avaliable at instantiation time, otherwise /// . /// - public Type ConverterTargetType { get; } + public Type? ConverterTargetType { get; } /// /// Gets whether or not this converter is a generic . /// - public bool IsGenericConverter => ConverterTargetType != null; + [MemberNotNullWhen(true, nameof(ConverterTargetType))] + public bool IsGenericConverter => ConverterTargetType is not null; /// /// Creates a new specifying to use the default converter type for the target member. @@ -67,11 +71,17 @@ namespace IPA.Config.Stores.Attributes /// the type to assign to public UseConverterAttribute(Type converterType) { + if (converterType is null) + throw new ArgumentNullException(nameof(converterType)); + UseDefaultConverterForType = false; ConverterType = converterType; + if (converterType.IsValueType) + throw new ArgumentException("Type is not a value converter!"); + var baseT = ConverterType.BaseType; - while (baseT != null && baseT != typeof(object) && + while (baseT != typeof(object) && (!baseT.IsGenericType || baseT.GetGenericTypeDefinition() != typeof(ValueConverter<>))) baseT = baseT.BaseType; if (baseT == typeof(object)) ConverterTargetType = null; diff --git a/IPA.Loader/Config/Stores/CollectionConverter.cs b/IPA.Loader/Config/Stores/CollectionConverter.cs index 635ba49c..16093540 100644 --- a/IPA.Loader/Config/Stores/CollectionConverter.cs +++ b/IPA.Loader/Config/Stores/CollectionConverter.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -14,7 +15,7 @@ namespace IPA.Config.Stores.Converters /// the type of the items in the collection /// the instantiated type of collection public class CollectionConverter : ValueConverter - where TCollection : ICollection + where TCollection : ICollection { /// /// Creates a using the default converter for the @@ -54,7 +55,7 @@ namespace IPA.Config.Stores.Converters /// protected void PopulateFromValue(TCollection col, List list, object parent) { - //Logger.log.Debug($"CollectionConverter<{typeof(T)}, {typeof(TCollection)}>({BaseConverter.GetType()}).PopulateFromValue([object], {list}, {parent.GetType()})"); + if (list is null) throw new ArgumentNullException(nameof(list)); foreach (var it in list) col.Add(BaseConverter.FromValue(it, parent)); } @@ -66,9 +67,9 @@ namespace IPA.Config.Stores.Converters /// the object that will own the resulting /// a new holding the deserialized content of /// - public override TCollection FromValue(Value value, object parent) + public override TCollection FromValue(Value? value, object parent) { - if (!(value is List list)) throw new ArgumentException("Argument not a List", nameof(value)); + if (value is not List list) throw new ArgumentException("Argument not a List", nameof(value)); var col = Create(list.Count, parent); PopulateFromValue(col, list, parent); @@ -81,7 +82,7 @@ namespace IPA.Config.Stores.Converters /// the object owning /// the that was serialized into /// - public override Value ToValue(TCollection obj, object parent) + public override Value? ToValue(TCollection? obj, object parent) => Value.From(obj.Select(t => BaseConverter.ToValue(t, parent))); } /// @@ -92,7 +93,7 @@ namespace IPA.Config.Stores.Converters /// the type of the converter to use for /// public sealed class CollectionConverter : CollectionConverter - where TCollection : ICollection + where TCollection : ICollection where TConverter : ValueConverter, new() { /// @@ -110,7 +111,7 @@ namespace IPA.Config.Stores.Converters /// /// the element type of the /// - public class ISetConverter : CollectionConverter> + public class ISetConverter : CollectionConverter> { /// /// Creates an using the default converter for . @@ -128,8 +129,8 @@ namespace IPA.Config.Stores.Converters /// the size to initialize it to /// the object that will own the new object /// the new - protected override ISet Create(int size, object parent) - => new HashSet(); + protected override ISet Create(int size, object parent) + => new HashSet(); } /// /// An which default constructs a converter for use as the value converter. @@ -155,7 +156,7 @@ namespace IPA.Config.Stores.Converters /// /// the element type of the /// - public class ListConverter : CollectionConverter> + public class ListConverter : CollectionConverter> { /// /// Creates an using the default converter for . @@ -173,8 +174,8 @@ namespace IPA.Config.Stores.Converters /// the size to initialize it to /// the object that will own the new object /// the new - protected override List Create(int size, object parent) - => new List(size); + protected override List Create(int size, object parent) + => new(size); } /// /// A which default constructs a converter for use as the value converter. @@ -199,7 +200,7 @@ namespace IPA.Config.Stores.Converters /// /// the element type of the /// - public class IListConverter : CollectionConverter> + public class IListConverter : CollectionConverter> { /// /// Creates an using the default converter for . @@ -217,8 +218,8 @@ namespace IPA.Config.Stores.Converters /// the size to initialize it to /// the object that will own the new object /// the new - protected override IList Create(int size, object parent) - => new List(size); + protected override IList Create(int size, object parent) + => new List(size); } /// /// An which default constructs a converter for use as the value converter. diff --git a/IPA.Loader/Config/Stores/Converters.cs b/IPA.Loader/Config/Stores/Converters.cs index 7821b90a..433640ad 100644 --- a/IPA.Loader/Config/Stores/Converters.cs +++ b/IPA.Loader/Config/Stores/Converters.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Config.Stores.Attributes; using IPA.Logging; using System; @@ -22,7 +23,7 @@ namespace IPA.Config.Stores.Converters /// /// the to get the integral value of /// the integral value of , or - public static long? IntValue(Value val) + public static long? IntValue(Value? val) => val is Integer inte ? inte.Value : val is FloatingPoint fp ? fp.AsInteger()?.Value : null; @@ -32,7 +33,7 @@ namespace IPA.Config.Stores.Converters /// /// the to get the floaing point value of /// the floaing point value of , or - public static decimal? FloatValue(Value val) + public static decimal? FloatValue(Value? val) => val is FloatingPoint fp ? fp.Value : val is Integer inte ? inte.AsFloat()?.Value : null; @@ -79,11 +80,11 @@ namespace IPA.Config.Stores.Converters if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { // this is a Nullable //Logger.log.Debug($"gives NullableConverter<{Nullable.GetUnderlyingType(t)}>"); - return (typeof(NullableConverter<>).MakeGenericType(Nullable.GetUnderlyingType(t))); + return typeof(NullableConverter<>).MakeGenericType(Nullable.GetUnderlyingType(t)); } //Logger.log.Debug($"gives converter for value type {t}"); - var valConv = Activator.CreateInstance(typeof(ValConv<>).MakeGenericType(t)) as IValConv; + var valConv = (IValConv)Activator.CreateInstance(typeof(ValConv<>).MakeGenericType(t)); return valConv.Get(); } @@ -117,7 +118,7 @@ namespace IPA.Config.Stores.Converters IValConv, IValConv, IValConv { - internal static readonly ValConvImpls Impl = new ValConvImpls(); + internal static readonly ValConvImpls Impl = new(); Type IValConv.Get() => typeof(CharConverter); Type IValConv.Get() => typeof(LongConverter); Type IValConv.Get() => typeof(ULongConverter); @@ -145,7 +146,7 @@ namespace IPA.Config.Stores.Converters /// the type of the that this works on public static class Converter { - private static ValueConverter defaultConverter = null; + private static ValueConverter? defaultConverter; /// /// Gets the default for the current type. /// @@ -158,7 +159,7 @@ namespace IPA.Config.Stores.Converters //Logger.log.Debug($"Converter<{t}>.MakeDefault()"); static ValueConverter MakeInstOf(Type ty) - => Activator.CreateInstance(ty) as ValueConverter; + => (ValueConverter)Activator.CreateInstance(ty); return MakeInstOf(Converter.GetDefaultConverterType(t)); } @@ -193,16 +194,16 @@ namespace IPA.Config.Stores.Converters /// the tree to convert /// the object which will own the created object /// the object represented by - public override T? FromValue(Value value, object parent) - => value == null ? null : new T?(baseConverter.FromValue(value, parent)); + public override T? FromValue(Value? value, object parent) + => value is null ? null : new T?(baseConverter.FromValue(value, parent)); /// /// Converts a nullable to a tree. /// /// the value to serialize /// the object which owns /// a tree representing . - public override Value ToValue(T? obj, object parent) - => obj == null ? null : baseConverter.ToValue(obj.Value, parent); + public override Value? ToValue(T? obj, object parent) + => obj is null ? null : baseConverter.ToValue(obj.Value, parent); } /// @@ -237,7 +238,7 @@ namespace IPA.Config.Stores.Converters /// the object which will own the created object /// the deserialized enum value /// if is not a node - public override T FromValue(Value value, object parent) + public override T FromValue(Value? value, object parent) => value is Text t ? (T)Enum.Parse(typeof(T), t.Value) : throw new ArgumentException("Value not a string", nameof(value)); @@ -248,8 +249,8 @@ namespace IPA.Config.Stores.Converters /// the value to serialize /// the object which owns /// a node representing - public override Value ToValue(T obj, object parent) - => Value.Text(obj.ToString()); + public override Value? ToValue(T? obj, object parent) + => Value.Text(obj?.ToString()); } /// @@ -267,7 +268,7 @@ namespace IPA.Config.Stores.Converters /// the object which will own the created object /// the deserialized enum value /// if is not a node - public override T FromValue(Value value, object parent) + public override T FromValue(Value? value, object parent) => value is Text t ? (T)Enum.Parse(typeof(T), t.Value, true) : throw new ArgumentException("Value not a string", nameof(value)); @@ -278,8 +279,8 @@ namespace IPA.Config.Stores.Converters /// the value to serialize /// the object which owns /// a node representing - public override Value ToValue(T obj, object parent) - => Value.Text(obj.ToString()); + public override Value? ToValue(T? obj, object parent) + => Value.Text(obj?.ToString()); } /// @@ -296,7 +297,7 @@ namespace IPA.Config.Stores.Converters /// the object which will own the created object /// the deserialized enum value /// if is not a numeric node - public override T FromValue(Value value, object parent) + public override T FromValue(Value? value, object parent) => (T)Enum.ToObject(typeof(T), Converter.IntValue(value) ?? throw new ArgumentException("Value not a numeric node", nameof(value))); @@ -306,7 +307,7 @@ namespace IPA.Config.Stores.Converters /// the value to serialize /// the object which owns /// an node representing - public override Value ToValue(T obj, object parent) + public override Value ToValue(T? obj, object parent) => Value.Integer(Convert.ToInt64(obj)); } @@ -314,7 +315,7 @@ namespace IPA.Config.Stores.Converters /// A converter for instances of . /// /// the value type of the dictionary - public class IDictionaryConverter : ValueConverter> + public class IDictionaryConverter : ValueConverter> { /// /// Gets the converter for the dictionary's value type. @@ -338,9 +339,9 @@ namespace IPA.Config.Stores.Converters /// the to convert /// the parent that will own the resulting object /// the deserialized dictionary - public override IDictionary FromValue(Value value, object parent) - => (value as Map)?.Select(kvp => (kvp.Key, val: BaseConverter.FromValue(kvp.Value, parent))) - ?.ToDictionary(p => p.Key, p => p.val) + public override IDictionary FromValue(Value? value, object parent) + => ((value as Map)?.Select(kvp => (kvp.Key, val: BaseConverter.FromValue(kvp.Value, parent))) + ?.ToDictionary(p => p.Key, p => p.val)) ?? throw new ArgumentException("Value not a map", nameof(value)); /// @@ -349,8 +350,8 @@ namespace IPA.Config.Stores.Converters /// the dictionary to serialize /// the object that owns the dictionary /// the dictionary serialized as a - public override Value ToValue(IDictionary obj, object parent) - => Value.From(obj.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); + public override Value? ToValue(IDictionary? obj, object parent) + => Value.From(obj.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); } /// @@ -373,7 +374,7 @@ namespace IPA.Config.Stores.Converters /// A converter for instances of . /// /// the value type of the dictionary - public class DictionaryConverter : ValueConverter> + public class DictionaryConverter : ValueConverter> { /// /// Gets the converter for the dictionary's value type. @@ -397,7 +398,7 @@ namespace IPA.Config.Stores.Converters /// the to convert /// the parent that will own the resulting object /// the deserialized dictionary - public override Dictionary FromValue(Value value, object parent) + public override Dictionary FromValue(Value? value, object parent) => (value as Map)?.Select(kvp => (kvp.Key, val: BaseConverter.FromValue(kvp.Value, parent))) ?.ToDictionary(p => p.Key, p => p.val) ?? throw new ArgumentException("Value not a map", nameof(value)); @@ -408,8 +409,8 @@ namespace IPA.Config.Stores.Converters /// the dictionary to serialize /// the object that owns the dictionary /// the dictionary serialized as a - public override Value ToValue(Dictionary obj, object parent) - => Value.From(obj.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); + public override Value? ToValue(Dictionary? obj, object parent) + => Value.From(obj?.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); } /// @@ -433,7 +434,7 @@ namespace IPA.Config.Stores.Converters /// A converter for instances of . /// /// the value type of the dictionary - public class IReadOnlyDictionaryConverter : ValueConverter> + public class IReadOnlyDictionaryConverter : ValueConverter> { /// /// Gets the converter for the dictionary's value type. @@ -457,7 +458,7 @@ namespace IPA.Config.Stores.Converters /// the to convert /// the parent that will own the resulting object /// the deserialized dictionary - public override IReadOnlyDictionary FromValue(Value value, object parent) + public override IReadOnlyDictionary FromValue(Value? value, object parent) => (value as Map)?.Select(kvp => (kvp.Key, val: BaseConverter.FromValue(kvp.Value, parent))) ?.ToDictionary(p => p.Key, p => p.val) ?? throw new ArgumentException("Value not a map", nameof(value)); @@ -468,8 +469,8 @@ namespace IPA.Config.Stores.Converters /// the dictionary to serialize /// the object that owns the dictionary /// the dictionary serialized as a - public override Value ToValue(IReadOnlyDictionary obj, object parent) - => Value.From(obj.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); + public override Value? ToValue(IReadOnlyDictionary? obj, object parent) + => Value.From(obj?.Select(p => new KeyValuePair(p.Key, BaseConverter.ToValue(p.Value, parent)))); } /// @@ -500,7 +501,7 @@ namespace IPA.Config.Stores.Converters /// the object which will own the created object /// the deserialized Color object /// if is not a node or couldn't be parsed into a Color object - public override Color FromValue(Value value, object parent) + public override Color FromValue(Value? value, object parent) { if (value is Text t) { @@ -526,146 +527,146 @@ namespace IPA.Config.Stores.Converters internal class StringConverter : ValueConverter { - public override string FromValue(Value value, object parent) + public override string? FromValue(Value? value, object parent) => (value as Text)?.Value; - public override Value ToValue(string obj, object parent) + public override Value? ToValue(string? obj, object parent) => Value.From(obj); } internal class CharConverter : ValueConverter { - public override char FromValue(Value value, object parent) + public override char FromValue(Value? value, object parent) => (value as Text)?.Value[0] ?? throw new ArgumentException("Value not a text node", nameof(value)); // can throw nullptr - public override Value ToValue(char obj, object parent) + public override Value? ToValue(char obj, object parent) => Value.From(char.ToString(obj)); } internal class LongConverter : ValueConverter { - public override long FromValue(Value value, object parent) + public override long FromValue(Value? value, object parent) => Converter.IntValue(value) ?? throw new ArgumentException("Value not a numeric value", nameof(value)); - public override Value ToValue(long obj, object parent) + public override Value? ToValue(long obj, object parent) => Value.From(obj); } internal class ULongConverter : ValueConverter { - public override ulong FromValue(Value value, object parent) + public override ulong FromValue(Value? value, object parent) => (ulong)(Converter.FloatValue(value) ?? throw new ArgumentException("Value not a numeric value", nameof(value))); - public override Value ToValue(ulong obj, object parent) + public override Value? ToValue(ulong obj, object parent) => Value.From(obj); } internal class IntPtrConverter : ValueConverter { - public override IntPtr FromValue(Value value, object parent) + public override IntPtr FromValue(Value? value, object parent) => (IntPtr)Converter.Default.FromValue(value, parent); - public override Value ToValue(IntPtr obj, object parent) + public override Value? ToValue(IntPtr obj, object parent) => Value.From((long)obj); } internal class UIntPtrConverter : ValueConverter { - public override UIntPtr FromValue(Value value, object parent) + public override UIntPtr FromValue(Value? value, object parent) => (UIntPtr)Converter.Default.FromValue(value, parent); - public override Value ToValue(UIntPtr obj, object parent) + public override Value? ToValue(UIntPtr obj, object parent) => Value.From((decimal)obj); } internal class IntConverter : ValueConverter { - public override int FromValue(Value value, object parent) + public override int FromValue(Value? value, object parent) => (int)Converter.Default.FromValue(value, parent); - public override Value ToValue(int obj, object parent) + public override Value? ToValue(int obj, object parent) => Value.From(obj); } internal class UIntConverter : ValueConverter { - public override uint FromValue(Value value, object parent) + public override uint FromValue(Value? value, object parent) => (uint)Converter.Default.FromValue(value, parent); - public override Value ToValue(uint obj, object parent) + public override Value? ToValue(uint obj, object parent) => Value.From(obj); } internal class ShortConverter : ValueConverter { - public override short FromValue(Value value, object parent) + public override short FromValue(Value? value, object parent) => (short)Converter.Default.FromValue(value, parent); - public override Value ToValue(short obj, object parent) + public override Value? ToValue(short obj, object parent) => Value.From(obj); } internal class UShortConverter : ValueConverter { - public override ushort FromValue(Value value, object parent) + public override ushort FromValue(Value? value, object parent) => (ushort)Converter.Default.FromValue(value, parent); - public override Value ToValue(ushort obj, object parent) + public override Value? ToValue(ushort obj, object parent) => Value.From(obj); } internal class ByteConverter : ValueConverter { - public override byte FromValue(Value value, object parent) + public override byte FromValue(Value? value, object parent) => (byte)Converter.Default.FromValue(value, parent); - public override Value ToValue(byte obj, object parent) + public override Value? ToValue(byte obj, object parent) => Value.From(obj); } internal class SByteConverter : ValueConverter { - public override sbyte FromValue(Value value, object parent) + public override sbyte FromValue(Value? value, object parent) => (sbyte)Converter.Default.FromValue(value, parent); - public override Value ToValue(sbyte obj, object parent) + public override Value? ToValue(sbyte obj, object parent) => Value.From(obj); } internal class DecimalConverter : ValueConverter { - public override decimal FromValue(Value value, object parent) + public override decimal FromValue(Value? value, object parent) => Converter.FloatValue(value) ?? throw new ArgumentException("Value not a numeric value", nameof(value)); - public override Value ToValue(decimal obj, object parent) + public override Value? ToValue(decimal obj, object parent) => Value.From(obj); } internal class FloatConverter : ValueConverter { - public override float FromValue(Value value, object parent) + public override float FromValue(Value? value, object parent) => (float)Converter.Default.FromValue(value, parent); - public override Value ToValue(float obj, object parent) + public override Value? ToValue(float obj, object parent) => Value.From((decimal)obj); } internal class DoubleConverter : ValueConverter { - public override double FromValue(Value value, object parent) + public override double FromValue(Value? value, object parent) => (double)Converter.Default.FromValue(value, parent); - public override Value ToValue(double obj, object parent) + public override Value? ToValue(double obj, object parent) => Value.From((decimal)obj); } internal class BooleanConverter : ValueConverter { - public override bool FromValue(Value value, object parent) + public override bool FromValue(Value? value, object parent) => (value as Boolean)?.Value ?? throw new ArgumentException("Value not a Boolean", nameof(value)); - public override Value ToValue(bool obj, object parent) + public override Value? ToValue(bool obj, object parent) => Value.From(obj); } internal class DateTimeConverter : ValueConverter { - public override DateTime FromValue(Value value, object parent) + public override DateTime FromValue(Value? value, object parent) { - if (!(value is Text text)) + if (value is not Text text) { throw new ArgumentException("Value is not of type Text", nameof(value)); } @@ -678,14 +679,14 @@ namespace IPA.Config.Stores.Converters throw new ArgumentException($"Parsing failed, {text.Value}"); } - public override Value ToValue(DateTime obj, object parent) => Value.Text(obj.ToString("O")); + public override Value? ToValue(DateTime obj, object parent) => Value.Text(obj.ToString("O")); } internal class DateTimeOffsetConverter : ValueConverter { - public override DateTimeOffset FromValue(Value value, object parent) + public override DateTimeOffset FromValue(Value? value, object parent) { - if (!(value is Text text)) + if (value is not Text text) { throw new ArgumentException("Value is not of type Text", nameof(value)); } @@ -703,9 +704,9 @@ namespace IPA.Config.Stores.Converters internal class TimeSpanConverter : ValueConverter { - public override TimeSpan FromValue(Value value, object parent) + public override TimeSpan FromValue(Value? value, object parent) { - if (!(value is Text text)) + if (value is not Text text) { throw new ArgumentException("Value is not of type Text", nameof(value)); } @@ -718,6 +719,6 @@ namespace IPA.Config.Stores.Converters throw new ArgumentException($"Parsing failed, {text.Value}"); } - public override Value ToValue(TimeSpan obj, object parent) => Value.Text(obj.ToString()); + public override Value? ToValue(TimeSpan obj, object parent) => Value.Text(obj.ToString()); } } diff --git a/IPA.Loader/Config/Stores/CustomObjectConverter.cs b/IPA.Loader/Config/Stores/CustomObjectConverter.cs index be881cc7..10787398 100644 --- a/IPA.Loader/Config/Stores/CustomObjectConverter.cs +++ b/IPA.Loader/Config/Stores/CustomObjectConverter.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using System; namespace IPA.Config.Stores.Converters @@ -12,24 +13,26 @@ namespace IPA.Config.Stores.Converters { private interface IImpl { - T FromValue(Value value, object parent); - Value ToValue(T obj, object parent); + T? FromValue(Value? value, object parent); + Value? ToValue(T? obj, object parent); } private class Impl : IImpl where U : class, GeneratedStoreImpl.IGeneratedStore, T { private static readonly GeneratedStoreImpl.GeneratedStoreCreator creator = GeneratedStoreImpl.GetCreator(typeof(T)); - private static U Create(GeneratedStoreImpl.IGeneratedStore parent) - => creator(parent) as U; + private static U Create(GeneratedStoreImpl.IGeneratedStore? parent) + => (U)creator(parent); - public T FromValue(Value value, object parent) + public T? FromValue(Value? value, object parent) { // lots of casting here, but it works i promise (probably) (parent can be a non-IGeneratedStore, however it won't necessarily behave then) + if (value is null) return null; var obj = Create(parent as GeneratedStoreImpl.IGeneratedStore); obj.Deserialize(value); return obj; } - public Value ToValue(T obj, object parent) + public Value? ToValue(T? obj, object parent) { + if (obj is null) return null; if (obj is GeneratedStoreImpl.IGeneratedStore store) return store.Serialize(); else @@ -51,7 +54,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that will own the deserialized value /// the deserialized value /// - public static T Deserialize(Value value, object parent) + public static T? Deserialize(Value? value, object parent) => impl.FromValue(value, parent); /// @@ -61,7 +64,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that owns /// the tree that represents /// - public static Value Serialize(T obj, object parent) + public static Value? Serialize(T? obj, object parent) => impl.ToValue(obj, parent); /// @@ -71,7 +74,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that will own the deserialized value /// the deserialized value /// - public override T FromValue(Value value, object parent) + public override T? FromValue(Value? value, object parent) => Deserialize(value, parent); /// @@ -81,7 +84,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that owns /// the tree that represents /// - public override Value ToValue(T obj, object parent) + public override Value? ToValue(T? obj, object parent) => Serialize(obj, parent); } @@ -104,7 +107,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that will own the deserialized value /// the deserialized value /// - public static T Deserialize(Value value, object parent) + public static T Deserialize(Value? value, object parent) => deserialize(value, parent); /// @@ -123,7 +126,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that will own the deserialized value /// the deserialized value /// - public override T FromValue(Value value, object parent) + public override T FromValue(Value? value, object parent) => Deserialize(value, parent); /// @@ -133,7 +136,7 @@ namespace IPA.Config.Stores.Converters /// the parent object that owns /// the tree that represents /// - public override Value ToValue(T obj, object parent) + public override Value? ToValue(T obj, object parent) => Serialize(obj); } diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs index 1ad9fd12..dc7f5ea9 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using System; using System.Collections.Generic; using System.Linq; @@ -13,12 +14,12 @@ namespace IPA.Config.Stores { internal delegate Value SerializeObject(T obj); - internal delegate T DeserializeObject(Value val, object parent); + internal delegate T DeserializeObject(Value? val, object parent); private static class DelegateStore { - public static SerializeObject Serialize; - public static DeserializeObject Deserialize; + public static SerializeObject? Serialize; + public static DeserializeObject? Deserialize; } internal static SerializeObject GetSerializerDelegate() diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs index 15d54c97..b59bf7d0 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Config.Stores.Attributes; using IPA.Logging; using System; diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs index 7f0bdffe..7f8e44f6 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Logging; using System; using System.Collections; @@ -43,6 +44,7 @@ namespace IPA.Config.Stores private static void EmitDeserializeNullable(ILGenerator il, SerializedMemberInfo member, Type expected, LocalAllocator GetLocal, Action thisarg, Action parentobj) { + if (!member.IsNullable) throw new InvalidOperationException("EmitDeserializeNullable called for non-nullable!"); thisarg ??= il => il.Emit(OpCodes.Ldarg_0); parentobj ??= thisarg; EmitDeserializeValue(il, member, member.NullableWrappedType, expected, GetLocal, thisarg, parentobj); @@ -100,7 +102,7 @@ namespace IPA.Config.Stores var structure = ReadObjectMembers(targetType); if (!structure.Any()) { - Logger.config.Warn($"Custom value type {targetType.FullName} (when compiling serialization of" + + Logger.Config.Warn($"Custom value type {targetType.FullName} (when compiling serialization of" + $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members"); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldloca, resultLocal); @@ -121,7 +123,7 @@ namespace IPA.Config.Stores } else { - Logger.config.Warn($"Implicit conversions to {expected} are not currently implemented"); + Logger.Config.Warn($"Implicit conversions to {expected} are not currently implemented"); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); } @@ -161,6 +163,9 @@ namespace IPA.Config.Stores private static void EmitDeserializeConverter(ILGenerator il, SerializedMemberInfo member, Label nextLabel, LocalAllocator GetLocal, Action thisobj, Action parentobj) { + if (!member.HasConverter) + throw new InvalidOperationException("EmitDeserializeConverter called for member without converter"); + using var stlocal = GetLocal.Allocate(typeof(Value)); using var valLocal = GetLocal.Allocate(member.Type); diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs index c069905f..7efa987a 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Config.Stores.Attributes; using IPA.Logging; using System; @@ -36,13 +37,12 @@ namespace IPA.Config.Stores private static readonly MethodInfo CreateGParent = typeof(GeneratedStoreImpl).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.Any, new[] { typeof(IGeneratedStore) }, Array.Empty()); - internal static T Create(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent); + internal static T Create(IGeneratedStore? parent) where T : class => (T)Create(typeof(T), parent); - private static IConfigStore Create(Type type, IGeneratedStore parent) + private static IConfigStore Create(Type type, IGeneratedStore? parent) => GetCreator(type)(parent); - private static readonly SingleCreationValueCache generatedCreators - = new SingleCreationValueCache(); + private static readonly SingleCreationValueCache generatedCreators = new(); private static (GeneratedStoreCreator ctor, Type type) GetCreatorAndGeneratedType(Type t) => generatedCreators.GetOrAdd(t, MakeCreator); @@ -55,7 +55,7 @@ namespace IPA.Config.Stores internal const string GeneratedAssemblyName = "IPA.Config.Generated"; - private static AssemblyBuilder assembly = null; + private static AssemblyBuilder? assembly; private static AssemblyBuilder Assembly { get @@ -75,7 +75,7 @@ namespace IPA.Config.Stores Assembly.Save(file); } - private static ModuleBuilder module = null; + private static ModuleBuilder? module; private static ModuleBuilder Module { get @@ -88,7 +88,7 @@ namespace IPA.Config.Stores } // TODO: does this need to be a SingleCreationValueCache or similar? - private static readonly Dictionary> TypeRequiredConverters = new Dictionary>(); + private static readonly Dictionary> TypeRequiredConverters = new(); private static void CreateAndInitializeConvertersFor(Type type, IEnumerable structure) { if (!TypeRequiredConverters.TryGetValue(type, out var converters)) @@ -96,7 +96,8 @@ namespace IPA.Config.Stores var converterFieldType = Module.DefineType($"{type.FullName}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.AnsiClass); // a static class - var uniqueConverterTypes = structure.Where(m => m.HasConverter).Select(m => m.Converter).Distinct().ToArray(); + var uniqueConverterTypes = structure.Where(m => m.HasConverter) + .Select(m => m.Converter).NonNull().Distinct().ToArray(); converters = new Dictionary(uniqueConverterTypes.Length); foreach (var convType in uniqueConverterTypes) @@ -122,11 +123,14 @@ namespace IPA.Config.Stores TypeRequiredConverters.Add(type, converters); - converterFieldType.CreateType(); + _ = converterFieldType.CreateType(); } - foreach (var member in structure.Where(m => m.HasConverter)) + foreach (var member in structure) + { + if (!member.HasConverter) continue; member.ConverterField = converters[member.Converter]; + } } } } diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs index 350ef742..28620626 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs @@ -1,4 +1,5 @@ -using IPA.Logging; +#nullable enable +using IPA.Logging; using System; using System.Collections.Generic; using System.Reflection; @@ -42,41 +43,41 @@ namespace IPA.Config.Stores internal class Impl : IConfigStore { private readonly IGeneratedStore generated; - private long enteredTransactions = 0; + private long enteredTransactions; internal static ConstructorInfo Ctor = typeof(Impl).GetConstructor(new[] { typeof(IGeneratedStore) }); public Impl(IGeneratedStore store) => generated = store; - private readonly AutoResetEvent resetEvent = new AutoResetEvent(false); + private readonly AutoResetEvent resetEvent = new(false); public WaitHandle SyncObject => resetEvent; - public static WaitHandle ImplGetSyncObject(IGeneratedStore s) => FindImpl(s).SyncObject; + public static WaitHandle? ImplGetSyncObject(IGeneratedStore s) => FindImpl(s)?.SyncObject; internal static MethodInfo ImplGetSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetSyncObject)); - public ReaderWriterLockSlim WriteSyncObject { get; } = new ReaderWriterLockSlim(); - public static ReaderWriterLockSlim ImplGetWriteSyncObject(IGeneratedStore s) => FindImpl(s)?.WriteSyncObject; + public ReaderWriterLockSlim WriteSyncObject { get; } = new(); + public static ReaderWriterLockSlim? ImplGetWriteSyncObject(IGeneratedStore s) => FindImpl(s)?.WriteSyncObject; internal static MethodInfo ImplGetWriteSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetWriteSyncObject)); internal static MethodInfo ImplSignalChangedMethod = typeof(Impl).GetMethod(nameof(ImplSignalChanged)); - public static void ImplSignalChanged(IGeneratedStore s) => FindImpl(s).SignalChanged(); + public static void ImplSignalChanged(IGeneratedStore s) => FindImpl(s)?.SignalChanged(); public void SignalChanged() { try { - resetEvent.Set(); + _ = resetEvent.Set(); } catch (ObjectDisposedException e) { - Logger.config.Error($"ObjectDisposedException while signalling a change for generated store {generated?.GetType()}"); - Logger.config.Error(e); + Logger.Config.Error($"ObjectDisposedException while signalling a change for generated store {generated?.GetType()}"); + Logger.Config.Error(e); } } internal static MethodInfo ImplInvokeChangedMethod = typeof(Impl).GetMethod(nameof(ImplInvokeChanged)); - public static void ImplInvokeChanged(IGeneratedStore s) => FindImpl(s).InvokeChanged(); + public static void ImplInvokeChanged(IGeneratedStore s) => FindImpl(s)?.InvokeChanged(); public void InvokeChanged() => generated.Changed(); internal static MethodInfo ImplTakeReadMethod = typeof(Impl).GetMethod(nameof(ImplTakeRead)); - public static void ImplTakeRead(IGeneratedStore s) => FindImpl(s).TakeRead(); + public static void ImplTakeRead(IGeneratedStore s) => FindImpl(s)?.TakeRead(); public void TakeRead() { if (!WriteSyncObject.IsWriteLockHeld) @@ -84,7 +85,7 @@ namespace IPA.Config.Stores } internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead)); - public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead(); + public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s)?.ReleaseRead(); public void ReleaseRead() { if (!WriteSyncObject.IsWriteLockHeld) @@ -92,24 +93,24 @@ namespace IPA.Config.Stores } internal static MethodInfo ImplTakeWriteMethod = typeof(Impl).GetMethod(nameof(ImplTakeWrite)); - public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s).TakeWrite(); + public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s)?.TakeWrite(); public void TakeWrite() => WriteSyncObject.EnterWriteLock(); internal static MethodInfo ImplReleaseWriteMethod = typeof(Impl).GetMethod(nameof(ImplReleaseWrite)); - public static void ImplReleaseWrite(IGeneratedStore s) => FindImpl(s).ReleaseWrite(); + public static void ImplReleaseWrite(IGeneratedStore s) => FindImpl(s)?.ReleaseWrite(); public void ReleaseWrite() => WriteSyncObject.ExitWriteLock(); internal static MethodInfo ImplChangeTransactionMethod = typeof(Impl).GetMethod(nameof(ImplChangeTransaction)); - public static IDisposable ImplChangeTransaction(IGeneratedStore s, IDisposable nest) => FindImpl(s).ChangeTransaction(nest); + public static IDisposable? ImplChangeTransaction(IGeneratedStore s, IDisposable nest) => FindImpl(s)?.ChangeTransaction(nest); // TODO: improve trasactionals so they don't always save in every case public IDisposable ChangeTransaction(IDisposable nest, bool takeWrite = true) => GetFreeTransaction().InitWith(this, nest, takeWrite && !WriteSyncObject.IsWriteLockHeld); - private ChangeTransactionObj GetFreeTransaction() + private static ChangeTransactionObj GetFreeTransaction() => freeTransactionObjs.Count > 0 ? freeTransactionObjs.Pop() : new ChangeTransactionObj(); // TODO: maybe sometimes clean this? - private static readonly Stack freeTransactionObjs = new Stack(); + private static readonly Stack freeTransactionObjs = new(); private sealed class ChangeTransactionObj : IDisposable { @@ -148,7 +149,7 @@ namespace IPA.Config.Stores try { if (data.ownsWrite) - data.impl.ReleaseWrite(); + data.impl?.ReleaseWrite(); } catch { @@ -163,17 +164,17 @@ namespace IPA.Config.Stores ~ChangeTransactionObj() => Dispose(false); } - public static Impl FindImpl(IGeneratedStore store) + public static Impl? FindImpl(IGeneratedStore store) { while (store?.Parent != null) store = store.Parent; // walk to the top of the tree return store?.Impl; } internal static MethodInfo ImplReadFromMethod = typeof(Impl).GetMethod(nameof(ImplReadFrom)); - public static void ImplReadFrom(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).ReadFrom(provider); + public static void ImplReadFrom(IGeneratedStore s, ConfigProvider provider) => FindImpl(s)?.ReadFrom(provider); public void ReadFrom(ConfigProvider provider) { - Logger.config.Debug($"Generated impl ReadFrom {generated.GetType()}"); + Logger.Config.Debug($"Generated impl ReadFrom {generated.GetType()}"); var values = provider.Load(); //Logger.config.Debug($"Read {values}"); generated.Deserialize(values); @@ -183,10 +184,10 @@ namespace IPA.Config.Stores } internal static MethodInfo ImplWriteToMethod = typeof(Impl).GetMethod(nameof(ImplWriteTo)); - public static void ImplWriteTo(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).WriteTo(provider); + public static void ImplWriteTo(IGeneratedStore s, ConfigProvider provider) => FindImpl(s)?.WriteTo(provider); public void WriteTo(ConfigProvider provider) { - Logger.config.Debug($"Generated impl WriteTo {generated.GetType()}"); + Logger.Config.Debug($"Generated impl WriteTo {generated.GetType()}"); var values = generated.Serialize(); //Logger.config.Debug($"Serialized {values}"); provider.Store(values); diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs index d9248c21..001e0393 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Config.Stores.Attributes; using IPA.Logging; using System; @@ -21,7 +22,7 @@ namespace IPA.Config.Stores internal static partial class GeneratedStoreImpl { - internal delegate IConfigStore GeneratedStoreCreator(IGeneratedStore parent); + internal delegate IConfigStore GeneratedStoreCreator(IGeneratedStore? parent); private static void GetMethodThis(ILGenerator il) => il.Emit(OpCodes.Ldarg_0); private static (GeneratedStoreCreator ctor, Type type) MakeCreator(Type type) @@ -52,7 +53,7 @@ namespace IPA.Config.Stores var structure = ReadObjectMembers(type); if (!structure.Any()) - Logger.config.Warn($"Custom type {type.FullName} has no accessible members"); + Logger.Config.Warn($"Custom type {type.FullName} has no accessible members"); #endregion var typeBuilder = Module.DefineType($"{type.FullName}", @@ -109,7 +110,7 @@ namespace IPA.Config.Stores const MethodAttributes virtualMemberMethod = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.Final; #region INotifyPropertyChanged - MethodBuilder notifyChanged = null; + MethodBuilder? notifyChanged = null; if (isINotifyPropertyChanged || hasNotifyAttribute) { // we don't actually want to notify if the base class implements it @@ -134,7 +135,7 @@ namespace IPA.Config.Stores } else { - Logger.log.Critical($"Type '{type.FullName}' implements INotifyPropertyChanged but does not have an accessible " + + Logger.Default.Critical($"Type '{type.FullName}' implements INotifyPropertyChanged but does not have an accessible " + "'RaisePropertyChanged(string)' method, automatic raising of PropertyChanged event is disabled."); } } @@ -435,7 +436,7 @@ namespace IPA.Config.Stores var IConfigStore_ReadFrom = IConfigStore_t.GetMethod(nameof(IConfigStore.ReadFrom)); #region IConfigStore.SyncObject - var syncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.SyncObject), PropertyAttributes.None, typeof(WaitHandle), null); + var syncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.SyncObject), PropertyAttributes.None, IConfigStore_GetSyncObject.ReturnType, null); var syncObjPropGet = typeBuilder.DefineMethod($"{nameof(IConfigStore.SyncObject)}", virtualPropertyMethodAttr, syncObjProp.PropertyType, Type.EmptyTypes); syncObjProp.SetGetMethod(syncObjPropGet); typeBuilder.DefineMethodOverride(syncObjPropGet, IConfigStore_GetSyncObject); @@ -450,7 +451,7 @@ namespace IPA.Config.Stores } #endregion #region IConfigStore.WriteSyncObject - var writeSyncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.WriteSyncObject), PropertyAttributes.None, typeof(WaitHandle), null); + var writeSyncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.WriteSyncObject), PropertyAttributes.None, IConfigStore_GetWriteSyncObject.ReturnType, null); var writeSyncObjPropGet = typeBuilder.DefineMethod($"{nameof(IConfigStore.WriteSyncObject)}", virtualPropertyMethodAttr, writeSyncObjProp.PropertyType, Type.EmptyTypes); writeSyncObjProp.SetGetMethod(writeSyncObjPropGet); typeBuilder.DefineMethodOverride(writeSyncObjPropGet, IConfigStore_GetWriteSyncObject); @@ -639,7 +640,7 @@ namespace IPA.Config.Stores #region Members foreach (var member in structure.Where(m => m.IsVirtual)) { // IsVirtual implies !IsField - var prop = member.Member as PropertyInfo; + var prop = (PropertyInfo)member.Member; var get = prop.GetGetMethod(true); var set = prop.GetSetMethod(true); diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs index 86384804..54595d76 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs @@ -1,10 +1,12 @@ -using IPA.Config.Stores.Attributes; +#nullable enable +using IPA.Config.Stores.Attributes; using IPA.Config.Stores.Converters; using IPA.Logging; using IPA.Utilities; using IPA.Utilities.Async; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; @@ -20,23 +22,25 @@ namespace IPA.Config.Stores { private class SerializedMemberInfo { - public string Name; - public MemberInfo Member; - public Type Type; + public string Name = null!; + public MemberInfo Member = null!; + public Type Type = null!; public bool AllowNull; public bool IsVirtual; public bool IsField; - public bool IsNullable; // signifies whether this is a Nullable + [MemberNotNullWhen(true, nameof(NullableWrappedType))] + public bool IsNullable { get; set; } // signifies whether this is a Nullable - public bool HasConverter; - public bool IsGenericConverter; // used so we can call directly to the generic version if it is - public Type Converter; - public Type ConverterBase; - public Type ConverterTarget; - public FieldInfo ConverterField; + [MemberNotNullWhen(true, nameof(Converter), nameof(ConverterBase))] + public bool HasConverter { get; set; } + public bool IsGenericConverter { get; set; } // used so we can call directly to the generic version if it is + public Type? Converter; + public Type? ConverterBase; + public Type? ConverterTarget; + public FieldInfo? ConverterField; // invalid for objects with IsNullable false - public Type NullableWrappedType => Nullable.GetUnderlyingType(Type); + public Type? NullableWrappedType => Nullable.GetUnderlyingType(Type); // invalid for objects with IsNullable false public PropertyInfo Nullable_HasValue => Type.GetProperty(nameof(Nullable.HasValue)); // invalid for objects with IsNullable false @@ -73,25 +77,27 @@ namespace IPA.Config.Stores { if (converterAttr.UseDefaultConverterForType) converterAttr = new UseConverterAttribute(Converter.GetDefaultConverterType(member.Type)); + if (converterAttr.UseDefaultConverterForType) + throw new InvalidOperationException("How did we get here?"); member.Converter = converterAttr.ConverterType; member.IsGenericConverter = converterAttr.IsGenericConverter; if (member.Converter.GetConstructor(Type.EmptyTypes) == null) { - Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not default-constructible"); + Logger.Config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not default-constructible"); goto endConverterAttr; // is there a better control flow structure to do this? } if (member.Converter.ContainsGenericParameters) { - Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that has unfilled type parameters"); + Logger.Config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that has unfilled type parameters"); goto endConverterAttr; } if (member.Converter.IsInterface || member.Converter.IsAbstract) { - Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not constructible"); + Logger.Config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not constructible"); goto endConverterAttr; } @@ -100,18 +106,18 @@ namespace IPA.Config.Stores { try { - var conv = Activator.CreateInstance(converterAttr.ConverterType) as IValueConverter; + var conv = (IValueConverter)Activator.CreateInstance(converterAttr.ConverterType); targetType = conv.Type; } catch { - Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter who's target type could not be determined"); + Logger.Config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter who's target type could not be determined"); goto endConverterAttr; } } if (targetType != member.Type) { - Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not of the member's type"); + Logger.Config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not of the member's type"); goto endConverterAttr; } @@ -128,8 +134,7 @@ namespace IPA.Config.Stores return true; } - private static readonly SingleCreationValueCache objectStructureCache - = new SingleCreationValueCache(); + private static readonly SingleCreationValueCache objectStructureCache = new(); private static IEnumerable ReadObjectMembers(Type type) => objectStructureCache.GetOrAdd(type, t => ReadObjectMembersInternal(type).ToArray()); diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs index cea03460..358bef88 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Logging; using System; using System.Collections.Generic; @@ -22,6 +23,17 @@ namespace IPA.Config.Stores { EmitLoad(il, member, thisarg); + using var valueTypeLocal = + member.IsNullable + ? GetLocal.Allocate(member.Type) + : default; + + if (member.IsNullable) + { + il.Emit(OpCodes.Stloc, valueTypeLocal.Local); + il.Emit(OpCodes.Ldloca, valueTypeLocal.Local); + } + var endSerialize = il.DefineLabel(); if (member.AllowNull) @@ -47,7 +59,7 @@ namespace IPA.Config.Stores var targetType = GetExpectedValueTypeForType(memberConversionType); if (member.HasConverter) { - using var stlocal = GetLocal.Allocate(member.Type); + using var stlocal = GetLocal.Allocate(memberConversionType); using var valLocal = GetLocal.Allocate(typeof(Value)); il.Emit(OpCodes.Stloc, stlocal); @@ -113,7 +125,7 @@ namespace IPA.Config.Stores else if (targetType == typeof(List)) { // TODO: impl this (enumerables) - Logger.config.Warn($"Implicit conversions to {targetType} are not currently implemented"); + Logger.Config.Warn($"Implicit conversions to {targetType} are not currently implemented"); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); } @@ -156,7 +168,7 @@ namespace IPA.Config.Stores var structure = ReadObjectMembers(memberConversionType); if (!structure.Any()) { - Logger.config.Warn($"Custom value type {memberConversionType.FullName} (when compiling serialization of" + + Logger.Config.Warn($"Custom value type {memberConversionType.FullName} (when compiling serialization of" + $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members"); il.Emit(OpCodes.Pop); } diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs index 43b766ed..f1a9ddb6 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Logging; using System; using System.Collections; @@ -22,26 +23,26 @@ namespace IPA.Config.Stores { #region Logs private static readonly MethodInfo LogErrorMethod = typeof(GeneratedStoreImpl).GetMethod(nameof(LogError), BindingFlags.NonPublic | BindingFlags.Static); - internal static void LogError(Type expected, Type found, string message) + internal static void LogError(Type? expected, Type? found, string message) { - Logger.config.Notice($"{message}{(expected == null ? "" : $" (expected {expected}, found {found?.ToString() ?? "null"})")}"); + Logger.Config.Notice($"{message}{(expected == null ? "" : $" (expected {expected}, found {found?.ToString() ?? "null"})")}"); } private static readonly MethodInfo LogWarningMethod = typeof(GeneratedStoreImpl).GetMethod(nameof(LogWarning), BindingFlags.NonPublic | BindingFlags.Static); internal static void LogWarning(string message) { - Logger.config.Warn(message); + Logger.Config.Warn(message); } private static readonly MethodInfo LogWarningExceptionMethod = typeof(GeneratedStoreImpl).GetMethod(nameof(LogWarningException), BindingFlags.NonPublic | BindingFlags.Static); internal static void LogWarningException(Exception exception) { - Logger.config.Warn(exception); + Logger.Config.Warn(exception); } #endregion //private delegate LocalBuilder LocalAllocator(Type type, int idx = 0); private static LocalAllocator MakeLocalAllocator(ILGenerator il) - => new LocalAllocator(il); + => new(il); private struct AllocatedLocal : IDisposable { @@ -59,14 +60,14 @@ namespace IPA.Config.Stores #endif public static implicit operator LocalBuilder(AllocatedLocal loc) => loc.Local; - public void Dealloc() => allocator.Deallocate(this); + public void Dealloc() => allocator?.Deallocate(this); public void Dispose() => Dealloc(); } private sealed class LocalAllocator { private readonly ILGenerator ilSource; - private readonly Dictionary> unallocatedLocals = new Dictionary>(); + private readonly Dictionary> unallocatedLocals = new(); public LocalAllocator(ILGenerator il) => ilSource = il; @@ -81,7 +82,7 @@ namespace IPA.Config.Stores { var list = GetLocalListForType(type); if (list.Count < 1) list.Push(ilSource.DeclareLocal(type)); - return new AllocatedLocal(this, list.Pop()); + return new(this, list.Pop()); } public void Deallocate(AllocatedLocal loc) @@ -97,12 +98,13 @@ namespace IPA.Config.Stores thisarg(il); // load this if (member.IsField) - il.Emit(OpCodes.Ldfld, member.Member as FieldInfo); + il.Emit(OpCodes.Ldfld, (FieldInfo)member.Member); else { // member is a property - var prop = member.Member as PropertyInfo; - var getter = prop.GetGetMethod(); - if (getter == null) throw new InvalidOperationException($"Property {member.Name} does not have a getter and is not ignored"); + var prop = (PropertyInfo)member.Member; + var getter = prop.GetGetMethod(true); + if (getter is null || getter.IsPrivate) + throw new InvalidOperationException($"Property {member.Name} does not have a getter and is not ignored"); il.Emit(OpCodes.Call, getter); } @@ -114,12 +116,13 @@ namespace IPA.Config.Stores value(il); if (member.IsField) - il.Emit(OpCodes.Stfld, member.Member as FieldInfo); + il.Emit(OpCodes.Stfld, (FieldInfo)member.Member); else { // member is a property - var prop = member.Member as PropertyInfo; - var setter = prop.GetSetMethod(); - if (setter == null) throw new InvalidOperationException($"Property {member.Name} does not have a setter and is not ignored"); + var prop = (PropertyInfo)member.Member; + var setter = prop.GetSetMethod(true); + if (setter is null || setter.IsPrivate) + throw new InvalidOperationException($"Property {member.Name} does not have a setter and is not ignored"); il.Emit(OpCodes.Call, setter); } @@ -132,7 +135,7 @@ namespace IPA.Config.Stores il.Emit(OpCodes.Call, LogWarningExceptionMethod); } - private static void EmitLogError(ILGenerator il, string message, bool tailcall = false, Action expected = null, Action found = null) + private static void EmitLogError(ILGenerator il, string message, bool tailcall = false, Action? expected = null, Action? found = null) { if (expected == null) expected = il => il.Emit(OpCodes.Ldnull); if (found == null) found = il => il.Emit(OpCodes.Ldnull); diff --git a/IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs b/IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs index 7ece8174..c768c1be 100644 --- a/IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs +++ b/IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs @@ -1,4 +1,5 @@ -using IPA.Config.Stores.Attributes; +#nullable enable +using IPA.Config.Stores.Attributes; using System; using System.Collections.Generic; using System.ComponentModel; diff --git a/IPA.Loader/Config/Stores/ValueConverter.cs b/IPA.Loader/Config/Stores/ValueConverter.cs index 8be1f300..68cec79e 100644 --- a/IPA.Loader/Config/Stores/ValueConverter.cs +++ b/IPA.Loader/Config/Stores/ValueConverter.cs @@ -1,4 +1,5 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using System; namespace IPA.Config.Stores @@ -31,14 +32,14 @@ namespace IPA.Config.Stores /// the object to convert /// the owning object of /// a representation of as a structure - Value ToValue(object obj, object parent); + Value? ToValue(object? obj, object parent); /// /// Converts the given to the object type handled by this converter. /// /// the to deserialize /// the object that will own the result /// the deserialized object - object FromValue(Value value, object parent); + object? FromValue(Value? value, object parent); /// /// Gets the type that this handles. /// @@ -59,7 +60,7 @@ namespace IPA.Config.Stores /// the owning object of /// a representation of as a structure /// - public abstract Value ToValue(T obj, object parent); + public abstract Value? ToValue(T? obj, object parent); /// /// Converts the given to the object type handled by this converter. /// @@ -67,10 +68,10 @@ namespace IPA.Config.Stores /// the object that will own the result /// the deserialized object /// - public abstract T FromValue(Value value, object parent); + public abstract T? FromValue(Value? value, object parent); - Value IValueConverter.ToValue(object obj, object parent) => ToValue((T)obj, parent); - object IValueConverter.FromValue(Value value, object parent) => FromValue(value, parent); + Value? IValueConverter.ToValue(object? obj, object parent) => ToValue((T?)obj, parent); + object? IValueConverter.FromValue(Value? value, object parent) => FromValue(value, parent); Type IValueConverter.Type => typeof(T); } } diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 3cc88a4f..ff62c1c3 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -3,16 +3,16 @@ - net461;net35 + net472 IPA true true false - true + true - + $(DefineConstants);NET4 @@ -22,7 +22,7 @@ $(DefineConstants);BeatSaber - + ..\Refs\UnityEngine.CoreModule.Net4.dll False @@ -49,12 +49,19 @@ - - - - - - + + + + + + + + + + @@ -69,6 +76,11 @@ + + + + + diff --git a/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs b/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs index 97cccb18..d17c5781 100644 --- a/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs +++ b/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs @@ -3,27 +3,49 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace IPA.JsonConverters { - internal class FeaturesFieldConverter : JsonConverter> + internal class FeaturesFieldConverter : JsonConverter>> { - public override Dictionary ReadJson(JsonReader reader, Type objectType, Dictionary existingValue, bool hasExistingValue, JsonSerializer serializer) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Assert([DoesNotReturnIf(false)] bool condition) + { + if (!condition) + throw new InvalidOperationException(); + } + + public override Dictionary> ReadJson(JsonReader reader, Type objectType, Dictionary> existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.StartArray) { _ = serializer.Deserialize(reader); - Logger.features.Warn("Encountered old features used. They no longer do anything, please move to the new format."); + Logger.Features.Warn("Encountered old features used. They no longer do anything, please move to the new format."); return existingValue; } - return serializer.Deserialize>(reader); + var dict = new Dictionary>(); + Assert(reader.TokenType == JsonToken.StartObject && reader.Read()); + + while (reader.TokenType == JsonToken.PropertyName) + { + var name = (string)reader.Value; + Assert(reader.Read()); + + var list = reader.TokenType == JsonToken.StartObject + ? (new() { serializer.Deserialize(reader) }) + : serializer.Deserialize>(reader); + + dict.Add(name, list); + Assert(reader.Read()); + } + + return dict; } - public override void WriteJson(JsonWriter writer, Dictionary value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, Dictionary> value, JsonSerializer serializer) { serializer.Serialize(writer, value); } diff --git a/IPA.Loader/JsonConverters/SemverRangeConverter.cs b/IPA.Loader/JsonConverters/SemverRangeConverter.cs index f570b4af..71e84983 100644 --- a/IPA.Loader/JsonConverters/SemverRangeConverter.cs +++ b/IPA.Loader/JsonConverters/SemverRangeConverter.cs @@ -1,15 +1,20 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; -using SemVer; - -namespace IPA.JsonConverters -{ - [SuppressMessage("ReSharper", "UnusedMember.Global")] - internal class SemverRangeConverter : JsonConverter - { - public override Range ReadJson(JsonReader reader, Type objectType, Range existingValue, bool hasExistingValue, JsonSerializer serializer) => new Range(reader.Value as string); - - public override void WriteJson(JsonWriter writer, Range value, JsonSerializer serializer) => writer.WriteValue(value.ToString()); - } -} +#nullable enable +using System; +using System.Runtime.Remoting.Messaging; +using Hive.Versioning; +using Newtonsoft.Json; + +namespace IPA.JsonConverters +{ + internal class SemverRangeConverter : JsonConverter + { + public override VersionRange? ReadJson(JsonReader reader, Type objectType, VersionRange? existingValue, bool hasExistingValue, JsonSerializer serializer) + => reader.Value is not string s ? existingValue : new VersionRange(s); + + public override void WriteJson(JsonWriter writer, VersionRange? value, JsonSerializer serializer) + { + if (value is null) writer.WriteNull(); + else writer.WriteValue(value.ToString()); + } + } +} diff --git a/IPA.Loader/JsonConverters/SemverVersionConverter.cs b/IPA.Loader/JsonConverters/SemverVersionConverter.cs index c79d2959..40683f03 100644 --- a/IPA.Loader/JsonConverters/SemverVersionConverter.cs +++ b/IPA.Loader/JsonConverters/SemverVersionConverter.cs @@ -1,17 +1,19 @@ -using System; -using Newtonsoft.Json; -using Version = SemVer.Version; - -namespace IPA.JsonConverters -{ - internal class SemverVersionConverter : JsonConverter - { - public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => reader.Value == null ? null : new Version(reader.Value as string, true); - - public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) - { - if (value == null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); - } - } -} +#nullable enable +using System; +using Newtonsoft.Json; +using Version = Hive.Versioning.Version; + +namespace IPA.JsonConverters +{ + internal class SemverVersionConverter : JsonConverter + { + public override Version? ReadJson(JsonReader reader, Type objectType, Version? existingValue, bool hasExistingValue, JsonSerializer serializer) + => reader.Value is not string s ? existingValue : new Version(s); + + public override void WriteJson(JsonWriter writer, Version? value, JsonSerializer serializer) + { + if (value == null) writer.WriteNull(); + else writer.WriteValue(value.ToString()); + } + } +} diff --git a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs index 386443d9..883ad7c4 100644 --- a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs +++ b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs @@ -27,7 +27,7 @@ namespace IPA.Loader.Composite } catch (Exception ex) { - Logger.log.Error($"{plugin.Metadata.Name} {method}: {ex}"); + Logger.Default.Error($"{plugin.Metadata.Name} {method}: {ex}"); } } } diff --git a/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs b/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs index 37beeb6a..9a66a3b7 100644 --- a/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs +++ b/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs @@ -38,7 +38,7 @@ namespace IPA.Loader.Composite } catch (Exception ex) { - Logger.log.Error($"{plugin.Name} {member}: {ex}"); + Logger.Default.Error($"{plugin.Name} {member}: {ex}"); } } } diff --git a/IPA.Loader/Loader/DependencyResolutionLoopException.cs b/IPA.Loader/Loader/DependencyResolutionLoopException.cs new file mode 100644 index 00000000..3d7fd89b --- /dev/null +++ b/IPA.Loader/Loader/DependencyResolutionLoopException.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace IPA.Loader +{ + [SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "This is only thrown and caught in local code")] + internal sealed class DependencyResolutionLoopException : Exception + { + public DependencyResolutionLoopException(string message) : base(message) + { + } + + public DependencyResolutionLoopException(string message, Exception innerException) : base(message, innerException) + { + } + + public DependencyResolutionLoopException() + { + } + } +} diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs index a8adcfe1..3d4aad77 100644 --- a/IPA.Loader/Loader/DisabledConfig.cs +++ b/IPA.Loader/Loader/DisabledConfig.cs @@ -86,13 +86,13 @@ namespace IPA.Loader try { if (transaction.WillNeedRestart) - Logger.loader.Warn("Runtime disabled config reload will need game restart to apply"); + Logger.Loader.Warn("Runtime disabled config reload will need game restart to apply"); return transaction.Commit().ContinueWith(t => { if (t.IsFaulted) { - Logger.loader.Error("Error changing disabled plugins"); - Logger.loader.Error(t.Exception); + Logger.Loader.Error("Error changing disabled plugins"); + Logger.Loader.Error(t.Exception); } }); } diff --git a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs index 61ab95f9..cd8e9cce 100644 --- a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs +++ b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +#nullable enable +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.IO; @@ -18,7 +19,7 @@ namespace IPA.Loader.Features DataModel data; try { - data = featureData.ToObject(); + data = featureData.ToObject() ?? throw new InvalidOperationException("Feature data is null"); } catch (Exception e) { @@ -36,7 +37,7 @@ namespace IPA.Loader.Features InvalidMessage = $"Invalid type name {data.TypeName}"; return false; } - catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) + catch (Exception e) when (e is FileNotFoundException or FileLoadException or BadImageFormatException) { string filename; diff --git a/IPA.Loader/Loader/Features/DefineFeature.cs b/IPA.Loader/Loader/Features/DefineFeature.cs index 9cba0a8c..f4d628f6 100644 --- a/IPA.Loader/Loader/Features/DefineFeature.cs +++ b/IPA.Loader/Loader/Features/DefineFeature.cs @@ -1,4 +1,5 @@ -using IPA.Logging; +#nullable enable +using IPA.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -15,20 +16,20 @@ namespace IPA.Loader.Features [JsonProperty("type", Required = Required.Always)] public string TypeName = ""; [JsonProperty("name", Required = Required.DisallowNull)] - public string ActualName = null; + public string? ActualName = null; public string Name => ActualName ?? TypeName; } - private DataModel data; + private DataModel data = null!; protected override bool Initialize(PluginMetadata meta, JObject featureData) { - Logger.features.Debug("Executing DefineFeature Init"); + Logger.Features.Debug("Executing DefineFeature Init"); try { - data = featureData.ToObject(); + data = featureData.ToObject() ?? throw new InvalidOperationException("Feature data is null"); } catch (Exception e) { @@ -42,7 +43,7 @@ namespace IPA.Loader.Features public override void BeforeInit(PluginMetadata meta) { - Logger.features.Debug("Executing DefineFeature AfterInit"); + Logger.Features.Debug("Executing DefineFeature AfterInit"); Type type; try @@ -51,10 +52,10 @@ namespace IPA.Loader.Features } catch (ArgumentException) { - Logger.features.Error($"Invalid type name {data.TypeName}"); + Logger.Features.Error($"Invalid type name {data.TypeName}"); return; } - catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) + catch (Exception e) when (e is FileNotFoundException or FileLoadException or BadImageFormatException) { var filename = ""; @@ -71,13 +72,13 @@ namespace IPA.Loader.Features break; } - Logger.features.Error($"Could not find {filename} while loading type"); + Logger.Features.Error($"Could not find {filename} while loading type"); return; } if (type == null) { - Logger.features.Error($"Invalid type name {data.TypeName}"); + Logger.Features.Error($"Invalid type name {data.TypeName}"); return; } @@ -89,12 +90,12 @@ namespace IPA.Loader.Features return; } - Logger.features.Error($"Feature with name {data.Name} already exists"); + Logger.Features.Error($"Feature with name {data.Name} already exists"); return; } catch (ArgumentException) { - Logger.features.Error($"{type.FullName} not a subclass of {nameof(Feature)}"); + Logger.Features.Error($"{type.FullName} not a subclass of {nameof(Feature)}"); return; } } diff --git a/IPA.Loader/Loader/HarmonyProtector.cs b/IPA.Loader/Loader/HarmonyProtector.cs index 2d49fea5..30f845ba 100644 --- a/IPA.Loader/Loader/HarmonyProtector.cs +++ b/IPA.Loader/Loader/HarmonyProtector.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using System.Collections.Generic; using System.Reflection; namespace IPA.Loader diff --git a/IPA.Loader/Loader/LibLoader.cs b/IPA.Loader/Loader/LibLoader.cs index 1e1ecdd8..337eb5e6 100644 --- a/IPA.Loader/Loader/LibLoader.cs +++ b/IPA.Loader/Loader/LibLoader.cs @@ -1,248 +1,237 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Linq; -using IPA.Logging; -using IPA.Utilities; -using Mono.Cecil; -#if NET3 -using Net3_Proxy; -using Directory = Net3_Proxy.Directory; -using Path = Net3_Proxy.Path; -using File = Net3_Proxy.File; -#endif - -namespace IPA.Loader -{ - internal class CecilLibLoader : BaseAssemblyResolver - { - private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; - private static readonly string CurrentAssemblyPath = Assembly.GetExecutingAssembly().Location; - - public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) - { - LibLoader.SetupAssemblyFilenames(); - - if (name.Name == CurrentAssemblyName) - return AssemblyDefinition.ReadAssembly(CurrentAssemblyPath, parameters); - - if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.dll", out var path)) - { - if (File.Exists(path)) - return AssemblyDefinition.ReadAssembly(path, parameters); - } - else if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.{name.Version}.dll", out path)) - { - if (File.Exists(path)) - return AssemblyDefinition.ReadAssembly(path, parameters); - } - - - return base.Resolve(name, parameters); - } - } - - internal static class LibLoader - { - internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); - internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); - internal static Dictionary FilenameLocations; - - internal static void Configure() - { - SetupAssemblyFilenames(true); - AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLibLoader; - AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; - } - - internal static void SetupAssemblyFilenames(bool force = false) - { - if (FilenameLocations == null || force) - { - FilenameLocations = new Dictionary(); - +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Linq; +using IPA.Logging; +using IPA.Utilities; +using Mono.Cecil; +using IPA.AntiMalware; +using IPA.Config; +#if NET3 +using Net3_Proxy; +using Directory = Net3_Proxy.Directory; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +#endif + +namespace IPA.Loader +{ + internal class CecilLibLoader : BaseAssemblyResolver + { + private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + private static readonly string CurrentAssemblyPath = Assembly.GetExecutingAssembly().Location; + + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + LibLoader.SetupAssemblyFilenames(); + + if (name.Name == CurrentAssemblyName) + return AssemblyDefinition.ReadAssembly(CurrentAssemblyPath, parameters); + + if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.dll", out var path)) + { + if (File.Exists(path)) + return AssemblyDefinition.ReadAssembly(path, parameters); + } + else if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.{name.Version}.dll", out path)) + { + if (File.Exists(path)) + return AssemblyDefinition.ReadAssembly(path, parameters); + } + + + return base.Resolve(name, parameters); + } + } + + internal static class LibLoader + { + internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); + internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); + internal static Dictionary FilenameLocations = null!; + + internal static void Configure() + { + SetupAssemblyFilenames(true); + AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLibLoader; + AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; + } + + internal static void SetupAssemblyFilenames(bool force = false) + { + if (FilenameLocations == null || force) + { + FilenameLocations = new Dictionary(); + foreach (var fn in TraverseTree(LibraryPath, s => s != NativeLibraryPath)) { - if (FilenameLocations.ContainsKey(fn.Name)) - Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}"); + if (FilenameLocations.ContainsKey(fn.Name)) + Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}"); else FilenameLocations.Add(fn.Name, fn.FullName); - } - - if (!SetDefaultDllDirectories(LoadLibraryFlags.LOAD_LIBRARY_SEARCH_USER_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_SYSTEM32 - | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_APPLICATION_DIR)) - { - var err = new Win32Exception(); - Log(Logger.Level.Critical, $"Error configuring DLL search path"); - Log(Logger.Level.Critical, err); - return; - } - - static void AddDir(string path) - { - var retPtr = AddDllDirectory(path); - if (retPtr == IntPtr.Zero) - { - var err = new Win32Exception(); - Log(Logger.Level.Warning, $"Could not add DLL directory {path}"); - Log(Logger.Level.Warning, err); - } - } - - if (Directory.Exists(NativeLibraryPath)) - { - AddDir(NativeLibraryPath); - _ = TraverseTree(NativeLibraryPath, dir => - { // this is a terrible hack for iterating directories - AddDir(dir); return true; - }).All(f => true); // force it to iterate all - } - - //var unityData = Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(); - //AddDir(Path.Combine(unityData, "Plugins")); - - foreach (var dir in Environment.GetEnvironmentVariable("path") - .Split(Path.PathSeparator) - .Select(Environment.ExpandEnvironmentVariables)) + } + + static void AddDir(string path) { - AddDir(dir); - } - } - } - - public static Assembly AssemblyLibLoader(object source, ResolveEventArgs e) - { - var asmName = new AssemblyName(e.Name); - return LoadLibrary(asmName); - } - - internal static Assembly LoadLibrary(AssemblyName asmName) - { - Log(Logger.Level.Debug, $"Resolving library {asmName}"); - - SetupAssemblyFilenames(); - - var testFile = $"{asmName.Name}.dll"; - Log(Logger.Level.Debug, $"Looking for file {asmName.Name}.dll"); - - if (FilenameLocations.TryGetValue(testFile, out var path)) - { - Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); - if (File.Exists(path)) - return Assembly.LoadFrom(path); - - Log(Logger.Level.Critical, $"but {path} no longer exists!"); - } - else if (FilenameLocations.TryGetValue(testFile = $"{asmName.Name}.{asmName.Version}.dll", out path)) - { - Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); - Log(Logger.Level.Warning, $"File {testFile} should be renamed to just {asmName.Name}.dll"); - if (File.Exists(path)) - return Assembly.LoadFrom(path); - - Log(Logger.Level.Critical, $"but {path} no longer exists!"); - } - - Log(Logger.Level.Critical, $"No library {asmName} found"); - - return null; - } - - internal static void Log(Logger.Level lvl, string message) - { // multiple proxy methods to delay loading of assemblies until it's done - if (Logger.LogCreated) - AssemblyLibLoaderCallLogger(lvl, message); - else - if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) - Console.WriteLine($"[{lvl}] {message}"); - } - internal static void Log(Logger.Level lvl, Exception message) - { // multiple proxy methods to delay loading of assemblies until it's done - if (Logger.LogCreated) - AssemblyLibLoaderCallLogger(lvl, message); - else - if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) - Console.WriteLine($"[{lvl}] {message}"); - } - - private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message) => Logger.libLoader.Log(lvl, message); - private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, Exception message) => Logger.libLoader.Log(lvl, message); - - // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree - private static IEnumerable TraverseTree(string root, Func dirValidator = null) - { - if (dirValidator == null) dirValidator = s => true; - - var dirs = new Stack(32); - - if (!Directory.Exists(root)) - throw new ArgumentException("Directory does not exist", nameof(root)); - dirs.Push(root); - - while (dirs.Count > 0) - { - string currentDir = dirs.Pop(); - string[] subDirs; - try - { - subDirs = Directory.GetDirectories(currentDir); - } - catch (UnauthorizedAccessException) - { continue; } - catch (DirectoryNotFoundException) - { continue; } - - string[] files; - try - { - files = Directory.GetFiles(currentDir); - } - catch (UnauthorizedAccessException) - { continue; } - catch (DirectoryNotFoundException) - { continue; } - - foreach (string str in subDirs) - if (dirValidator(str)) dirs.Push(str); - - foreach (string file in files) - { - FileInfo nextValue; - try - { - nextValue = new FileInfo(file); - } - catch (FileNotFoundException) - { continue; } - - yield return nextValue; - } - } - } - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] -#if NET461 - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] -#endif - private static extern IntPtr AddDllDirectory(string lpPathName); - - [Flags] - private enum LoadLibraryFlags : uint - { - None = 0, - LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, - LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, - LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, - LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, - } - - [DllImport("kernel32.dll", SetLastError = true)] -#if NET461 - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] -#endif - private static extern bool SetDefaultDllDirectories(LoadLibraryFlags dwFlags); - } -} + var pathEnvironmentVariable = Environment.GetEnvironmentVariable("Path"); + Environment.SetEnvironmentVariable("Path", path + Path.PathSeparator + pathEnvironmentVariable); + } + + if (Directory.Exists(NativeLibraryPath)) + { + AddDir(NativeLibraryPath); + _ = TraverseTree(NativeLibraryPath, dir => + { // this is a terrible hack for iterating directories + AddDir(dir); return true; + }).All(f => true); // force it to iterate all + } + + //var unityData = Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(); + //AddDir(Path.Combine(unityData, "Plugins")); + + // TODO: find a way to either safely remove Newtonsoft, or switch to a different JSON lib + _ = LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral")); + } + } + + public static Assembly? AssemblyLibLoader(object source, ResolveEventArgs e) + { + var asmName = new AssemblyName(e.Name); + return LoadLibrary(asmName); + } + + internal static Assembly? LoadLibrary(AssemblyName asmName) + { + Log(Logger.Level.Debug, $"Resolving library {asmName}"); + + SetupAssemblyFilenames(); + + var testFile = $"{asmName.Name}.dll"; + Log(Logger.Level.Debug, $"Looking for file {asmName.Name}.dll"); + + if (FilenameLocations.TryGetValue(testFile, out var path)) + { + Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); + return LoadSafe(path); + } + else if (FilenameLocations.TryGetValue(testFile = $"{asmName.Name}.{asmName.Version}.dll", out path)) + { + Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); + Log(Logger.Level.Warning, $"File {testFile} should be renamed to just {asmName.Name}.dll"); + return LoadSafe(path); + } + + Log(Logger.Level.Critical, $"No library {asmName} found"); + + return null; + } + + private static Assembly? LoadSafe(string path) + { + if (!File.Exists(path)) + { + Log(Logger.Level.Critical, $"{path} no longer exists!"); + return null; + } + + if (AntiMalwareEngine.IsInitialized) + { + var result = AntiMalwareEngine.Engine.ScanFile(new FileInfo(path)); + if (result is ScanResult.Detected) + { + Log(Logger.Level.Error, $"Scan of '{path}' found malware; not loading"); + return null; + } + if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && result is not ScanResult.KnownSafe and not ScanResult.NotDetected) + { + Log(Logger.Level.Error, $"Scan of '{path}' found partial threat; not loading. To load this, enable AntiMalware.RunPartialThreatCode in the config."); + return null; + } + } + + return Assembly.LoadFrom(path); + } + + internal static void Log(Logger.Level lvl, string message) + { // multiple proxy methods to delay loading of assemblies until it's done + if (Logger.LogCreated) + { + AssemblyLibLoaderCallLogger(lvl, message); + } + else + { + if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) + Console.WriteLine($"[{lvl}] {message}"); + } + } + internal static void Log(Logger.Level lvl, Exception message) + { // multiple proxy methods to delay loading of assemblies until it's done + if (Logger.LogCreated) + { + AssemblyLibLoaderCallLogger(lvl, message); + } + else + { + if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) + Console.WriteLine($"[{lvl}] {message}"); + } + } + + private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message) => Logger.LibLoader.Log(lvl, message); + private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, Exception message) => Logger.LibLoader.Log(lvl, message); + + // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree + private static IEnumerable TraverseTree(string root, Func? dirValidator = null) + { + if (dirValidator == null) dirValidator = s => true; + + var dirs = new Stack(32); + + if (!Directory.Exists(root)) + throw new ArgumentException("Directory does not exist", nameof(root)); + dirs.Push(root); + + while (dirs.Count > 0) + { + string currentDir = dirs.Pop(); + string[] subDirs; + try + { + subDirs = Directory.GetDirectories(currentDir); + } + catch (UnauthorizedAccessException) + { continue; } + catch (DirectoryNotFoundException) + { continue; } + + string[] files; + try + { + files = Directory.GetFiles(currentDir); + } + catch (UnauthorizedAccessException) + { continue; } + catch (DirectoryNotFoundException) + { continue; } + + foreach (string str in subDirs) + if (dirValidator(str)) dirs.Push(str); + + foreach (string file in files) + { + FileInfo nextValue; + try + { + nextValue = new FileInfo(file); + } + catch (FileNotFoundException) + { continue; } + + yield return nextValue; + } + } + } + } +} diff --git a/IPA.Loader/Loader/PluginExecutor.cs b/IPA.Loader/Loader/PluginExecutor.cs index 90b54582..307d4557 100644 --- a/IPA.Loader/Loader/PluginExecutor.cs +++ b/IPA.Loader/Loader/PluginExecutor.cs @@ -80,7 +80,7 @@ namespace IPA.Loader .OrderByDescending(t => t.c.GetParameters().Length) .Select(t => t.c).ToArray(); if (ctors.Length > 1) - Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters."); + Logger.Loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters."); bool usingDefaultCtor = false; var ctor = ctors.FirstOrDefault(); @@ -122,6 +122,7 @@ namespace IPA.Loader // TODO: make enable and disable able to take a bool indicating which it is private static Action MakeLifecycleEnableFunc(Type type, string name) { + var noEnableDisable = type.GetCustomAttribute() is not null; var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) .Select(t => (t.m, attrs: t.attrs.Cast())) @@ -129,7 +130,8 @@ namespace IPA.Loader .Select(t => t.m).ToArray(); if (enableMethods.Length == 0) { - Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); + if (!noEnableDisable) + Logger.Loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); return o => { }; } @@ -138,7 +140,7 @@ namespace IPA.Loader if (m.GetParameters().Length > 0) throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters."); if (m.ReturnType != typeof(void)) - Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); + Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); } var objParam = Expression.Parameter(typeof(object), "obj"); @@ -153,6 +155,7 @@ namespace IPA.Loader } private static Func MakeLifecycleDisableFunc(Type type, string name) { + var noEnableDisable = type.GetCustomAttribute() is not null; var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) .Select(t => (t.m, attrs: t.attrs.Cast())) @@ -160,7 +163,8 @@ namespace IPA.Loader .Select(t => t.m).ToArray(); if (disableMethods.Length == 0) { - Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); + if (!noEnableDisable) + Logger.Loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); return o => TaskEx.WhenAll(); } @@ -178,7 +182,7 @@ namespace IPA.Loader continue; } else - Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored."); + Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored."); } nonTaskMethods.Add(m); diff --git a/IPA.Loader/Loader/PluginInitInjector.cs b/IPA.Loader/Loader/PluginInitInjector.cs index c4d085ab..0d5052e5 100644 --- a/IPA.Loader/Loader/PluginInitInjector.cs +++ b/IPA.Loader/Loader/PluginInitInjector.cs @@ -1,11 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using IPA.Config; using IPA.Logging; using IPA.Utilities; -using System.Linq.Expressions; +using IPA.AntiMalware; #if NET4 using Expression = System.Linq.Expressions.Expression; using ExpressionEx = System.Linq.Expressions.Expression; @@ -36,14 +36,17 @@ namespace IPA.Loader /// /// /// - /// - /// A object for the plugin being injected. + /// A object for the plugin being injected. /// /// These parameters may have and to control /// how it is constructed. /// /// /// + /// + /// + /// The instance which should be used for any potentially dangerous files. + /// /// /// For all of the default injectors, only one of each will be generated, and any later parameters will recieve the same value as the first one. /// @@ -57,7 +60,40 @@ namespace IPA.Loader /// the of the parameter being injected. /// the for the plugin being loaded. /// the value to inject into that parameter. - public delegate object InjectParameter(object previous, ParameterInfo param, PluginMetadata meta); + public delegate object? InjectParameter(object? previous, ParameterInfo param, PluginMetadata meta); + + /// + /// A provider for parameter injectors to request injected values themselves. + /// + /// + /// Some injectors may look at attributes on the parameter to gain additional information about what it should provide. + /// If an injector wants to allow end users to affect the things it requests, it may pass the parameter it is currently + /// injecting for to this delegate along with a type override to select some other type. + /// + /// the parameter that this is providing for. + /// an optional override for the parameter type. + /// the value that would otherwise be injected. + public delegate object? InjectedValueProvider(ParameterInfo forParam, Type? typeOverride = null); + + /// + /// A typed injector for a plugin's Init method. When registered, called for all associated types. If it returns null, the default for the type will be used. + /// + /// the previous return value of the function, or if never called for plugin. + /// the of the parameter being injected. + /// the for the plugin being loaded. + /// an to allow the injector to request injected values. + /// the value to inject into that parameter. + public delegate object? InjectParameterNested(object? previous, ParameterInfo param, PluginMetadata meta, InjectedValueProvider provider); + + /// + /// Invokes the provider with and and casts the result to . + /// + /// the type of object to be injected + /// the provider to invoke. + /// the parameter to provide for + /// the value requested, or . + public static T? Inject(this InjectedValueProvider provider, ParameterInfo param) + => (T?)provider?.Invoke(param, typeof(T)); /// /// Adds an injector to be used when calling future plugins' Init methods. @@ -65,6 +101,14 @@ namespace IPA.Loader /// the type of the parameter. /// the function to call for injection. public static void AddInjector(Type type, InjectParameter injector) + => AddInjector(type, (pre, par, met, pro) => injector(pre, par, met)); + + /// + /// Adds an injector to be used when calling future plugins' Init methods. + /// + /// the type of the parameter. + /// the function to call for injection. + public static void AddInjector(Type type, InjectParameterNested injector) { injectors.Add(new TypedInjector(type, injector)); } @@ -72,13 +116,13 @@ namespace IPA.Loader private struct TypedInjector : IEquatable { public Type Type; - public InjectParameter Injector; + public InjectParameterNested Injector; - public TypedInjector(Type t, InjectParameter i) + public TypedInjector(Type t, InjectParameterNested i) { Type = t; Injector = i; } - public object Inject(object prev, ParameterInfo info, PluginMetadata meta) - => Injector(prev, info, meta); + public object? Inject(object? prev, ParameterInfo info, PluginMetadata meta, InjectedValueProvider provider) + => Injector(prev, info, meta, provider); public bool Equals(TypedInjector other) => Type == other.Type && Injector == other.Injector; @@ -94,15 +138,12 @@ namespace IPA.Loader public static bool operator !=(TypedInjector a, TypedInjector b) => !a.Equals(b); } - private static readonly List injectors = new List + private static readonly List injectors = new() { - new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), - new TypedInjector(typeof(PluginMetadata), (prev, param, meta) => prev ?? meta), - new TypedInjector(typeof(Config.Config), (prev, param, meta) => - { - if (prev != null) return prev; - return Config.Config.GetConfigFor(meta.Name, param); - }) + new TypedInjector(typeof(Logger), (prev, param, meta, _) => prev ?? new StandardLogger(meta.Name)), + new TypedInjector(typeof(PluginMetadata), (prev, param, meta, _) => prev ?? meta), + new TypedInjector(typeof(Config.Config), (prev, param, meta, _) => prev ?? Config.Config.GetConfigFor(meta.Name, param)), + new TypedInjector(typeof(IAntiMalware), (prev, param, meta, _) => prev ?? AntiMalwareEngine.Engine) }; private static int? MatchPriority(Type target, Type source) @@ -111,10 +152,10 @@ namespace IPA.Loader if (!target.IsAssignableFrom(source)) return null; if (!target.IsInterface && !source.IsSubclassOf(target)) return int.MinValue; - int value = 0; + int value = int.MaxValue - 1; while (true) { - if (source == null) return value; + if (source is null) return value; if (target.IsInterface && source.GetInterfaces().Contains(target)) return value; else if (target == source) @@ -139,52 +180,80 @@ namespace IPA.Loader Expression.ArrayIndex(arr, Expression.Constant(i)), t)))); } - internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta, ref object persist) + private static object? InjectForParameter( + Dictionary previousValues, + PluginMetadata meta, + ParameterInfo param, + Type paramType, + InjectedValueProvider provider) { - var initArgs = new List(); + var value = paramType.GetDefault(); - var previousValues = persist as Dictionary; - if (previousValues == null) + var toUse = injectors + .Select(i => (inject: i, priority: MatchPriority(paramType, i.Type))) // check match priority, combine it + .NonNull(t => t.priority) // filter null priorities + .Select(t => (t.inject, priority: t.priority!.Value)) // remove nullable + .OrderByDescending(t => t.priority) // sort by value + .Select(t => t.inject); // remove priority value + + // this tries injectors in order of closest match by type provided + foreach (var pair in toUse) { - previousValues = new Dictionary(injectors.Count); - persist = previousValues; + object? prev = null; + if (previousValues.ContainsKey(pair)) + prev = previousValues[pair]; + + var val = pair.Inject(prev, param, meta, provider); + + previousValues[pair] = val; + + if (val == null) continue; + value = val; + break; } - foreach (var param in initParams) - { - var paramType = param.ParameterType; + return value; + } - var value = paramType.GetDefault(); + private class InjectedValueProviderWrapperImplementation + { + public Dictionary PreviousValues { get; } - var toUse = injectors.Select(i => (inject: i, priority: MatchPriority(paramType, i.Type))) // check match priority, combine it - .Where(t => t.priority != null) // filter null priorities - .Select(t => (t.inject, priority: t.priority.Value)) // remove nullable - .OrderByDescending(t => t.priority) // sort by value - .Select(t => t.inject); // remove priority value + public PluginMetadata Meta { get; } - // this tries injectors in order of closest match by type provided - foreach (var pair in toUse) - { - object prev = null; - if (previousValues.ContainsKey(pair)) - prev = previousValues[pair]; + public InjectedValueProvider Provider { get; } - var val = pair.Inject(prev, param, meta); + public InjectedValueProviderWrapperImplementation(PluginMetadata meta) + { + Meta = meta; + PreviousValues = new(); + Provider = Inject; + } - if (previousValues.ContainsKey(pair)) - previousValues[pair] = val; - else - previousValues.Add(pair, val); + private object? Inject(ParameterInfo param, Type? typeOverride = null) + => InjectForParameter(PreviousValues, Meta, param, typeOverride ?? param.ParameterType, Provider); + } - if (val == null) continue; - value = val; - break; - } + internal static object?[] Inject(ParameterInfo[] initParams, PluginMetadata meta, ref object? persist) + { + var initArgs = new List(); + + var impl = persist as InjectedValueProviderWrapperImplementation; + if (impl == null || impl.Meta != meta) + { + impl = new(meta); + persist = impl; + } + + foreach (var param in initParams) + { + var paramType = param.ParameterType; + + var value = InjectForParameter(impl.PreviousValues, meta, param, paramType, impl.Provider); initArgs.Add(value); } - //init.Invoke(instance, initArgs.ToArray()); return initArgs.ToArray(); } } diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index feb72462..04930963 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -11,12 +11,10 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Version = SemVer.Version; -using SemVer; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; using System.Diagnostics; +using IPA.AntiMalware; +using Hive.Versioning; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; @@ -48,20 +46,9 @@ namespace IPA.Loader LoadMetadata(); sw.Stop(); - Logger.loader.Info($"Loading metadata took {sw.Elapsed}"); + Logger.Loader.Info($"Loading metadata took {sw.Elapsed}"); sw.Reset(); - // old loader system -#if false - Resolve(); - InitFeatures(); - ComputeLoadOrder(); - FilterDisabled(); - FilterWithoutFiles(); - - ResolveDependencies(); -#endif - sw.Start(); // Features contribute to load order considerations @@ -69,7 +56,7 @@ namespace IPA.Loader DoOrderResolution(); sw.Stop(); - Logger.loader.Info($"Calculating load order took {sw.Elapsed}"); + Logger.Loader.Info($"Calculating load order took {sw.Elapsed}"); }); internal static void YeetIfNeeded() @@ -117,15 +104,16 @@ namespace IPA.Loader throw new InvalidOperationException())) manifest = manifestReader.ReadToEnd(); - selfMeta.Manifest = JsonConvert.DeserializeObject(manifest); + var manifestObj = JsonConvert.DeserializeObject(manifest); + selfMeta.Manifest = manifestObj ?? throw new InvalidOperationException("Deserialized manifest was null"); PluginsMetadata.Add(selfMeta); SelfMeta = selfMeta; } catch (Exception e) { - Logger.loader.Critical("Error loading own manifest"); - Logger.loader.Critical(e); + Logger.Loader.Critical("Error loading own manifest"); + Logger.Loader.Critical(e); } using var resolver = new CecilLibLoader(); @@ -141,7 +129,19 @@ namespace IPA.Loader try { - var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters + var scanResult = AntiMalwareEngine.Engine.ScanFile(metadata.File); + if (scanResult is ScanResult.Detected) + { + Logger.Loader.Warn($"Scan of {plugin} found malware; not loading"); + continue; + } + if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && scanResult is not ScanResult.KnownSafe and not ScanResult.NotDetected) + { + Logger.Loader.Warn($"Scan of {plugin} found partial threat; not loading. To load this, set AntiMalware.RunPartialThreatCode in the config."); + continue; + } + + var pluginModule = AssemblyDefinition.ReadAssembly(metadata.File.FullName, new ReaderParameters { ReadingMode = ReadingMode.Immediate, ReadWrite = false, @@ -172,56 +172,62 @@ namespace IPA.Loader #if DIRE_LOADER_WARNINGS Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}"); #else - Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}"); + Logger.Loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}"); #endif continue; } if (pluginManifest.Id == null) { - Logger.loader.Warn($"Plugin '{pluginManifest.Name}' does not have a listed ID, using name"); + Logger.Loader.Warn($"Plugin '{pluginManifest.Name}' does not have a listed ID, using name"); pluginManifest.Id = pluginManifest.Name; } metadata.Manifest = pluginManifest; - void TryGetNamespacedPluginType(string ns, PluginMetadata meta) + bool TryPopulatePluginType(TypeDefinition type, PluginMetadata meta) { - foreach (var type in pluginModule.Types) + if (!type.HasCustomAttributes) + return false; + + var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName); + if (attr is null) + return false; + + if (!attr.HasConstructorArguments) { - if (type.Namespace != ns) continue; + Logger.Loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments"); + return false; + } - if (type.HasCustomAttributes) - { - var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName); - if (attr != null) - { - if (!attr.HasConstructorArguments) - { - Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments"); - return; - } + var args = attr.ConstructorArguments; + if (args.Count != 1) + { + Logger.Loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments"); + return false; + } + var rtOptionsArg = args[0]; + if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName) + { + Logger.Loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}"); + return false; + } - var args = attr.ConstructorArguments; - if (args.Count != 1) - { - Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments"); - return; - } - var rtOptionsArg = args[0]; - if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName) - { - Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}"); - return; - } + var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions - var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions + meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt; + meta.PluginType = type; + return true; + } - meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt; - meta.PluginType = type; - return; - } - } + void TryGetNamespacedPluginType(string ns, PluginMetadata meta) + { + foreach (var type in pluginModule.Types) + { + if (type.Namespace != ns) continue; + + if (TryPopulatePluginType(type, meta)) + return; } } @@ -230,7 +236,7 @@ namespace IPA.Loader if (hint != null) { var type = pluginModule.GetType(hint); - if (type != null) + if (type == null || !TryPopulatePluginType(type, metadata)) TryGetNamespacedPluginType(hint, metadata); } @@ -239,20 +245,20 @@ namespace IPA.Loader if (metadata.PluginType == null) { - Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}"); + Logger.Loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}"); continue; } - Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}"); + Logger.Loader.Debug($"Adding info for {Path.GetFileName(plugin)}"); PluginsMetadata.Add(metadata); } catch (Exception e) { - Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); - Logger.loader.Error(e); + Logger.Loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); + Logger.Loader.Error(e); ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error) { - ReasonText = "An error ocurred loading the data", + ReasonText = "An error occurred loading the data", Error = e }); } @@ -271,19 +277,26 @@ namespace IPA.Loader IsBare = true, }; - metadata.Manifest = JsonConvert.DeserializeObject(File.ReadAllText(manifest)); + var manifestObj = JsonConvert.DeserializeObject(File.ReadAllText(manifest)); + if (manifestObj is null) + { + Logger.Loader.Error($"Bare manifest {Path.GetFileName(manifest)} deserialized to null"); + continue; + } + + metadata.Manifest = manifestObj; if (metadata.Manifest.Files.Length < 1) - Logger.loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " + + Logger.Loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " + $"Dependency resolution and verification cannot be completed."); - Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}"); + Logger.Loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}"); PluginsMetadata.Add(metadata); } catch (Exception e) { - Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}"); - Logger.loader.Error(e); + Logger.Loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}"); + Logger.Loader.Error(e); } } @@ -295,7 +308,7 @@ namespace IPA.Loader { if (meta.IsBare) { - Logger.loader.Warn($"Bare manifest cannot specify description file"); + Logger.Loader.Warn($"Bare manifest cannot specify description file"); meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line continue; } @@ -310,7 +323,7 @@ namespace IPA.Loader .FirstOrDefault(r => r.Name == name); if (resc == null) { - Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include"); + Logger.Loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include"); meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line continue; } @@ -338,7 +351,7 @@ namespace IPA.Loader public enum Reason { /// - /// An error was thrown either loading plugin information fomr disk, or when initializing the plugin. + /// An error was thrown either loading plugin information from disk, or when initializing the plugin. /// /// /// When this is the set in an structure, the member @@ -363,7 +376,7 @@ namespace IPA.Loader /// Conflict, /// - /// The plugin this reason is assiciated with is missing a dependency. + /// The plugin this reason is associated with is missing a dependency. /// /// /// Since this is only given when a dependency is missing, will @@ -381,7 +394,7 @@ namespace IPA.Loader /// Feature, /// - /// The plugin this reason is assoicated with is unsupported. + /// The plugin this reason is associated with is unsupported. /// /// /// Currently, there is no path in the loader that emits this , however there may @@ -479,274 +492,19 @@ namespace IPA.Loader // the thing -> the reason internal static Dictionary ignoredPlugins = new(); -#if false - internal static void Resolve() - { // resolves duplicates and conflicts, etc - PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version)); - - var ids = new HashSet(); - var ignore = new Dictionary(); - var resolved = new List(PluginsMetadata.Count); - foreach (var meta in PluginsMetadata) - { - if (meta.Id != null) - { - if (ids.Contains(meta.Id)) - { - Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); - var ireason = new IgnoreReason(Reason.Duplicate) - { - ReasonText = $"Duplicate entry of same ID ({meta.Id})", - RelatedTo = resolved.First(p => p.Id == meta.Id) - }; - ignore.Add(meta, ireason); - ignoredPlugins.Add(meta, ireason); - continue; // because of sorted order, hightest order will always be the first one - } - - bool processedLater = false; - foreach (var meta2 in PluginsMetadata) - { - if (ignore.ContainsKey(meta2)) continue; - if (meta == meta2) - { - processedLater = true; - continue; - } - - if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue; - - var range = meta2.Manifest.Conflicts[meta.Id]; - if (!range.IsSatisfied(meta.Version)) continue; - - Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}"); - - if (processedLater) - { - Logger.loader.Warn($"Ignoring {meta2.Name}"); - ignore.Add(meta2, new IgnoreReason(Reason.Conflict) - { - ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}", - RelatedTo = meta - }); - } - else - { - Logger.loader.Warn($"Ignoring {meta.Name}"); - ignore.Add(meta, new IgnoreReason(Reason.Conflict) - { - ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}", - RelatedTo = meta2 - }); - break; - } - } - } - - if (ignore.TryGetValue(meta, out var reason)) - { - ignoredPlugins.Add(meta, reason); - continue; - } - if (meta.Id != null) - ids.Add(meta.Id); - - resolved.Add(meta); - } - - PluginsMetadata = resolved; - } - - private static void FilterDisabled() - { - var enabled = new List(PluginsMetadata.Count); - - var disabled = DisabledConfig.Instance.DisabledModIds; - foreach (var meta in PluginsMetadata) - { - if (disabled.Contains(meta.Id ?? meta.Name)) - DisabledPlugins.Add(meta); - else - enabled.Add(meta); - } - - PluginsMetadata = enabled; - } - - private static void FilterWithoutFiles() - { - var enabled = new List(PluginsMetadata.Count); - - foreach (var meta in PluginsMetadata) - { - var passed = true; - foreach (var file in meta.AssociatedFiles) - { - if (!file.Exists) - { - passed = false; - ignoredPlugins.Add(meta, new IgnoreReason(Reason.MissingFiles) - { - ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} (declared by {meta.Name}) does not exist" - }); - Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" + - $" (declared by {meta.Name}) does not exist! Mod installation is incomplete, not loading it."); - break; - } - } - - if (passed) - enabled.Add(meta); - } - - PluginsMetadata = enabled; - } - - internal static void ComputeLoadOrder() + internal static void DoOrderResolution() { #if DEBUG - Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); + // print starting order + Logger.Loader.Debug(string.Join(", ", PluginsMetadata.StrJP())); #endif - static bool InsertInto(HashSet root, PluginMetadata meta, bool isRoot = false) - { // this is slow, and hella recursive - bool inserted = false; - foreach (var sr in root) - { - inserted = inserted || InsertInto(sr.Dependencies, meta); - - if (meta.Id != null) - { - if (sr.Manifest.Dependencies.ContainsKey(meta.Id)) - inserted = inserted || sr.Dependencies.Add(meta); - else if (sr.Manifest.LoadAfter.Contains(meta.Id)) - inserted = inserted || sr.LoadsAfter.Add(meta); - } - if (sr.Id != null) - if (meta.Manifest.LoadBefore.Contains(sr.Id)) - inserted = inserted || sr.LoadsAfter.Add(meta); - } - - if (isRoot) - { - foreach (var sr in root) - { - InsertInto(meta.Dependencies, sr); - - if (sr.Id != null) - { - if (meta.Manifest.Dependencies.ContainsKey(sr.Id)) - meta.Dependencies.Add(sr); - else if (meta.Manifest.LoadAfter.Contains(sr.Id)) - meta.LoadsAfter.Add(sr); - } - if (meta.Id != null) - if (sr.Manifest.LoadBefore.Contains(meta.Id)) - meta.LoadsAfter.Add(sr); - } - - root.Add(meta); - } - - return inserted; - } - - var pluginTree = new HashSet(); - foreach (var meta in PluginsMetadata) - InsertInto(pluginTree, meta, true); - - static void DeTree(List into, HashSet tree) - { - foreach (var st in tree) - if (!into.Contains(st)) - { - DeTree(into, st.Dependencies); - DeTree(into, st.LoadsAfter); - into.Add(st); - } - } - - PluginsMetadata = new List(); - DeTree(PluginsMetadata, pluginTree); - -#if DEBUG - Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); -#endif - } + PluginsMetadata.Sort((a, b) => b.HVersion.CompareTo(a.HVersion)); - internal static void ResolveDependencies() - { - var metadata = new List(); - var pluginsToLoad = new Dictionary(); - var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version); - foreach (var meta in PluginsMetadata) - { - bool ignoreBcNoLoader = true; - var missingDeps = new List<(string id, Range version, bool disabled)>(); - foreach (var dep in meta.Manifest.Dependencies) - { #if DEBUG - Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}"); + // print base resolution order + Logger.Loader.Debug(string.Join(", ", PluginsMetadata.StrJP())); #endif - if (dep.Key == SelfMeta.Id) - ignoreBcNoLoader = false; - - if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) - continue; - - if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key])) - { - Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too."); - missingDeps.Add((dep.Key, dep.Value, true)); - } - else - { - Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); - missingDeps.Add((dep.Key, dep.Value, false)); - } - } - - if (meta.PluginType != null && !meta.IsSelf && !meta.IsBare && ignoreBcNoLoader) - { - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) - { - ReasonText = "BSIPA Plugin does not reference BSIPA!" - }); - for (int i = 0; i < 20; i++) - { - Logger.loader.Warn($"HEY {meta.Id} YOU DEPEND ON BSIPA SO DEPEND ON BSIPA"); - } - continue; - } - - if (missingDeps.Count == 0) - { - metadata.Add(meta); - if (meta.Id != null) - pluginsToLoad.Add(meta.Id, meta.Version); - } - else if (missingDeps.Any(t => !t.disabled)) - { // missing deps - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) - { - ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}" - }); - } - else - { - DisabledPlugins.Add(meta); - DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name); - } - } - - DisabledConfig.Instance.Changed(); - PluginsMetadata = metadata; - } -#endif - - internal static void DoOrderResolution() - { - PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version)); var metadataCache = new Dictionary(PluginsMetadata.Count); var pluginsToProcess = new List(PluginsMetadata.Count); @@ -772,7 +530,7 @@ namespace IPA.Loader } else { - Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); + Logger.Loader.Warn($"Found duplicates of {meta.Id}, using newest"); ignoredPlugins.Add(meta, new(Reason.Duplicate) { ReasonText = $"Duplicate entry of same ID ({meta.Id})", @@ -782,51 +540,128 @@ namespace IPA.Loader } // preprocess LoadBefore into LoadAfter - foreach (var kvp in metadataCache) + foreach (var (_, (meta, _)) in metadataCache) { // we iterate the metadata cache because it contains both disabled and enabled plugins - var loadBefore = kvp.Value.Meta.Manifest.LoadBefore; + var loadBefore = meta.Manifest.LoadBefore; foreach (var id in loadBefore) { if (metadataCache.TryGetValue(id, out var plugin)) { // if the id exists in our metadata cache, make sure it knows to load after the plugin in kvp - _ = plugin.Meta.LoadsAfter.Add(kvp.Value.Meta); + _ = plugin.Meta.LoadsAfter.Add(meta); + } + } + } + + // preprocess conflicts to be mutual + foreach (var (_, (meta, _)) in metadataCache) + { + foreach (var (id, range) in meta.Manifest.Conflicts) + { + if (metadataCache.TryGetValue(id, out var plugin) + && range.Matches(plugin.Meta.HVersion)) + { + // make sure that there's a mutual dependency + var targetRange = VersionRange.ForVersion(meta.HVersion); + var targetConflicts = plugin.Meta.Manifest.Conflicts; + if (!targetConflicts.TryGetValue(meta.Id, out var realRange)) + { + // there's not already a listed conflict + targetConflicts.Add(meta.Id, targetRange); + } + else if (!realRange.Matches(meta.HVersion)) + { + // there is already a listed conflict that isn't mutual + targetRange = realRange | targetRange; + targetConflicts[meta.Id] = targetRange; + } } } } var loadedPlugins = new Dictionary(); var outputOrder = new List(PluginsMetadata.Count); + var isProcessing = new HashSet(); { - bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored) + bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored, bool partial = false) { meta = null; disabled = false; ignored = true; + Logger.Loader.Trace($"Trying to resolve plugin '{id}' partial:{partial}"); if (loadedPlugins.TryGetValue(id, out var foundMeta)) { meta = foundMeta.Meta; disabled = foundMeta.Disabled; ignored = foundMeta.Ignored; + Logger.Loader.Trace($"- Found already processed"); return true; } if (metadataCache!.TryGetValue(id, out var plugin)) { + Logger.Loader.Trace($"- In metadata cache"); + if (partial) + { + Logger.Loader.Trace($" - but requested in a partial lookup"); + return false; + } + disabled = !plugin.Enabled; meta = plugin.Meta; if (!disabled) { - Resolve(plugin.Meta, ref disabled, out ignored); + try + { + ignored = false; + Resolve(plugin.Meta, ref disabled, out ignored); + } + catch (Exception e) + { + if (e is not DependencyResolutionLoopException) + { + Logger.Loader.Error($"While performing load order resolution for {id}:"); + Logger.Loader.Error(e); + } + + if (!ignored) + { + ignoredPlugins.Add(plugin.Meta, new(Reason.Error) + { + Error = e + }); + } + + ignored = true; + } + } + + if (!loadedPlugins.ContainsKey(id)) + { + // this condition is specifically for when we fail resolution because of a graph loop + Logger.Loader.Trace($"- '{id}' resolved as ignored:{ignored},disabled:{disabled}"); + loadedPlugins.Add(id, (plugin.Meta, disabled, ignored)); } - loadedPlugins.Add(id, (plugin.Meta, disabled, ignored)); return true; } + Logger.Loader.Trace($"- Not found"); return false; } void Resolve(PluginMetadata plugin, ref bool disabled, out bool ignored) { + Logger.Loader.Trace($">Resolving '{plugin.Name}'"); + + // first we need to check for loops in the resolution graph to prevent stack overflows + if (isProcessing.Contains(plugin)) + { + Logger.Loader.Error($"Loop detected while processing '{plugin.Name}'; flagging as ignored"); + throw new DependencyResolutionLoopException(); + } + + isProcessing.Add(plugin); + using var _removeProcessing = Utils.ScopeGuard(() => isProcessing.Remove(plugin)); + // if this method is being called, this is the first and only time that it has been called for this plugin. ignored = false; @@ -840,8 +675,8 @@ namespace IPA.Loader { ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} does not exist" }); - Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" + - $" (declared by {plugin.Name}) does not exist! Mod installation is incomplete, not loading it."); + Logger.Loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" + + $" (declared by '{plugin.Name}') does not exist! Mod installation is incomplete, not loading it."); ignored = true; return; } @@ -849,16 +684,17 @@ namespace IPA.Loader // first load dependencies var dependsOnSelf = false; - foreach (var dep in plugin.Manifest.Dependencies) + foreach (var (id, range) in plugin.Manifest.Dependencies) { - if (dep.Key == SelfMeta.Id) + if (id == SelfMeta.Id) dependsOnSelf = true; - if (!TryResolveId(dep.Key, out var depMeta, out var depDisabled, out var depIgnored)) + if (!TryResolveId(id, out var depMeta, out var depDisabled, out var depIgnored) + || !range.Matches(depMeta.HVersion)) { - Logger.loader.Warn($"Dependency '{dep.Key}@{dep.Value}' for '{plugin.Id}' does not exist; ignoring '{plugin.Id}'"); + Logger.Loader.Warn($"'{plugin.Id}' is missing dependency '{id}@{range}'; ignoring"); ignoredPlugins.Add(plugin, new(Reason.Dependency) { - ReasonText = $"Dependency '{dep.Key}@{dep.Value}' not found", + ReasonText = $"Dependency '{id}@{range}' not found", }); ignored = true; return; @@ -866,10 +702,10 @@ namespace IPA.Loader // make a point to propagate ignored if (depIgnored) { - Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'"); + Logger.Loader.Warn($"Dependency '{id}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'"); ignoredPlugins.Add(plugin, new(Reason.Dependency) { - ReasonText = $"Dependency '{dep.Key}' ignored", + ReasonText = $"Dependency '{id}' ignored", RelatedTo = depMeta }); ignored = true; @@ -878,7 +714,7 @@ namespace IPA.Loader // make a point to propagate disabled if (depDisabled) { - Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' disabled; disabling"); + Logger.Loader.Warn($"Dependency '{id}' for '{plugin.Id}' disabled; disabling"); disabledPlugins!.Add(plugin); _ = disabledIds!.Add(plugin.Id); disabled = true; @@ -891,7 +727,7 @@ namespace IPA.Loader // make sure the plugin depends on the loader (assuming it actually needs to) if (!dependsOnSelf && !plugin.IsSelf && !plugin.IsBare) { - Logger.loader.Warn($"Plugin '{plugin.Id}' does not depend on any particular loader version; assuming its incompatible"); + Logger.Loader.Warn($"Plugin '{plugin.Id}' does not depend on any particular loader version; assuming its incompatible"); ignoredPlugins.Add(plugin, new(Reason.Dependency) { ReasonText = "Does not depend on any loader version, so it is assumed to be incompatible", @@ -917,7 +753,7 @@ namespace IPA.Loader // then handle loadafters foreach (var id in plugin.Manifest.LoadAfter) { - if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored) && !depIgnored) + if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored)) { // we only want to make sure to loadafter if its not ignored // if its disabled, we still wanna track it where possible @@ -926,15 +762,19 @@ namespace IPA.Loader } // after we handle dependencies and loadafters, then check conflicts - foreach (var conflict in plugin.Manifest.Conflicts) + foreach (var (id, range) in plugin.Manifest.Conflicts) { - if (TryResolveId(conflict.Key, out var meta, out var conflDisabled, out var conflIgnored) && !conflIgnored && !conflDisabled) + Logger.Loader.Trace($">- Checking conflict '{id}' {range}"); + // this lookup must be partial to prevent loadBefore/conflictsWith from creating a recursion loop + if (TryResolveId(id, out var meta, out var conflDisabled, out var conflIgnored, partial: true) + && range.Matches(meta.HVersion) + && !conflIgnored && !conflDisabled) // the conflict is only *actually* a problem if it is both not ignored and not disabled { - // the conflict is only *actually* a problem if it is both not ignored and not disabled - Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.Version}; ignoring '{plugin.Id}'"); + + Logger.Loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.HVersion}; ignoring '{plugin.Id}'"); ignoredPlugins.Add(plugin, new(Reason.Conflict) { - ReasonText = $"Conflicts with {meta.Id}@{meta.Version}", + ReasonText = $"Conflicts with {meta.Id}@{meta.HVersion}", RelatedTo = meta }); ignored = true; @@ -942,10 +782,18 @@ namespace IPA.Loader } } - // we can now load the current plugin - outputOrder!.Add(plugin); + // specifically check if some strange stuff happened (like graph loops) causing this to be ignored + // from some other invocation + if (!ignoredPlugins.ContainsKey(plugin)) + { + // we can now load the current plugin + Logger.Loader.Trace($"->'{plugin.Name}' loads here"); + outputOrder!.Add(plugin); + } // loadbefores have already been preprocessed into loadafters + + Logger.Loader.Trace($">Processed '{plugin.Name}'"); } // run TryResolveId over every plugin, which recursively calculates load order @@ -965,13 +813,15 @@ namespace IPA.Loader { foreach (var meta in PluginsMetadata) { - foreach (var feature in meta.Manifest.Features.Select(f => new Feature.Instance(meta, f.Key, f.Value))) + foreach (var feature in meta.Manifest.Features + .SelectMany(f => f.Value.Select(o => (f.Key, o))) + .Select(t => new Feature.Instance(meta, t.Key, t.o))) { if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null) { // this is a DefineFeature, so we want to initialize it early if (!feature.TryCreate(out var inst)) { - Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}"); + Logger.Features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}"); } else { @@ -1004,7 +854,7 @@ namespace IPA.Loader } else { - Logger.features.Warn($"No such feature {feature.Name}"); + Logger.Features.Warn($"No such feature {feature.Name}"); } } } @@ -1042,8 +892,8 @@ namespace IPA.Loader internal static PluginExecutor? InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) { - if (meta.Manifest.GameVersion != UnityGame.GameVersion) - Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); + if (meta.Manifest.GameVersion is { } gv && gv != UnityGame.GameVersion) + Logger.Loader.Warn($"Mod {meta.Name} developed for game version {gv}, so it may not work properly."); if (meta.IsSelf) return new PluginExecutor(meta, PluginExecutor.Special.Self); @@ -1086,8 +936,8 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Error($"Error creating executor for {meta.Name}"); - Logger.loader.Error(e); + Logger.Loader.Error($"Error creating executor for {meta.Name}"); + Logger.Loader.Error(e); return null; } @@ -1099,8 +949,8 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:"); - Logger.loader.Critical(e); + Logger.Loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:"); + Logger.Loader.Critical(e); } } @@ -1110,11 +960,11 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Error($"Could not init plugin {meta.Name}"); - Logger.loader.Error(e); + Logger.Loader.Error($"Could not init plugin {meta.Name}"); + Logger.Loader.Error(e); ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error) { - ReasonText = "Error ocurred while initializing", + ReasonText = "Error occurred while initializing", Error = e }); return null; @@ -1125,7 +975,7 @@ namespace IPA.Loader { if (!feature.TryCreate(out var inst)) { - Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}"); + Logger.Features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}"); } else { @@ -1142,8 +992,8 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:"); - Logger.loader.Critical(e); + Logger.Loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:"); + Logger.Loader.Critical(e); } return exec; @@ -1170,8 +1020,8 @@ namespace IPA.Loader } catch (Exception e) { - Logger.log.Critical($"Uncaught exception while loading pluign {meta.Name}:"); - Logger.log.Critical(e); + Logger.Default.Critical($"Uncaught exception while loading plugin {meta.Name}:"); + Logger.Default.Critical(e); } } diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 06239cd4..b323366f 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,7 +6,6 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; using IPA.Config; -using IPA.Old; using IPA.Utilities; using Mono.Cecil; using UnityEngine; @@ -52,10 +50,10 @@ namespace IPA.Loader /// /// Gets info about the enabled plugin with the specified ID. /// - /// the ID name of the plugin to get (must be an exact match) + /// the ID name of the plugin to get (must be an exact match) /// the plugin metadata for the requested plugin or if it doesn't exist or is disabled - public static PluginMetadata GetPluginFromId(string name) - => BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Id == name); + public static PluginMetadata GetPluginFromId(string id) + => BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Id == id); /// /// Gets a disabled plugin's metadata by its name. @@ -68,10 +66,10 @@ namespace IPA.Loader /// /// Gets a disabled plugin's metadata by its ID. /// - /// the ID of the disabled plugin to get + /// the ID of the disabled plugin to get /// the metadata for the corresponding plugin - public static PluginMetadata GetDisabledPluginFromId(string name) => - DisabledPlugins.FirstOrDefault(p => p.Id == name); + public static PluginMetadata GetDisabledPluginFromId(string id) => + DisabledPlugins.FirstOrDefault(p => p.Id == id); /// /// Creates a new transaction for mod enabling and disabling mods simultaneously. @@ -153,8 +151,8 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Error($"Error while enabling {meta.Id}:"); - Logger.loader.Error(e); + Logger.Loader.Error($"Error while enabling {meta.Id}:"); + Logger.Loader.Error(e); // this should still be considered enabled, hence its position } } @@ -215,7 +213,7 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterDisable)}: {e}"); + Logger.Loader.Critical($"Feature errored in {nameof(Feature.AfterDisable)}: {e}"); } } }, UnityMainThreadTaskScheduler.Default); @@ -442,26 +440,26 @@ namespace IPA.Loader sw.Stop(); - Logger.log.Info(exeName); - Logger.log.Info($"Running on Unity {Application.unityVersion}"); - Logger.log.Info($"Game version {UnityGame.GameVersion}"); - Logger.log.Info("-----------------------------"); - Logger.log.Info($"Loading plugins from {Utils.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); - Logger.log.Info("-----------------------------"); + Logger.Default.Info(exeName); + Logger.Default.Info($"Running on Unity {Application.unityVersion}"); + Logger.Default.Info($"Game version {UnityGame.GameVersion}"); + Logger.Default.Info("-----------------------------"); + Logger.Default.Info($"Loading plugins from {Utils.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); + Logger.Default.Info("-----------------------------"); foreach (var plugin in _bsPlugins) { - Logger.log.Info($"{plugin.Metadata.Name} ({plugin.Metadata.Id}): {plugin.Metadata.Version}"); + Logger.Default.Info($"{plugin.Metadata.Name} ({plugin.Metadata.Id}): {plugin.Metadata.Version}"); } - Logger.log.Info("-----------------------------"); + Logger.Default.Info("-----------------------------"); if (_ipaPlugins.Count > 0) { foreach (var plugin in _ipaPlugins) { - Logger.log.Info($"{plugin.Name}: {plugin.Version}"); + Logger.Default.Info($"{plugin.Name}: {plugin.Version}"); } - Logger.log.Info("-----------------------------"); + Logger.Default.Info("-----------------------------"); } - Logger.log.Info($"Initializing plugins took {sw.Elapsed}"); + Logger.Default.Info($"Initializing plugins took {sw.Elapsed}"); } private static IEnumerable LoadPluginsFromFile(string file) @@ -482,7 +480,7 @@ namespace IPA.Loader } catch (Exception e) { - Logger.loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}"); + Logger.Loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}"); } } @@ -506,13 +504,13 @@ namespace IPA.Loader } catch (ReflectionTypeLoadException e) { - Logger.loader.Error($"Could not load the following types from {Path.GetFileName(file)}:"); - Logger.loader.Error($" {string.Join("\n ", e.LoaderExceptions?.Select(e1 => e1?.Message).StrJP() ?? Array.Empty())}"); + Logger.Loader.Error($"Could not load the following types from {Path.GetFileName(file)}:"); + Logger.Loader.Error($" {string.Join("\n ", e.LoaderExceptions?.Select(e1 => e1?.Message).StrJP() ?? Array.Empty())}"); } catch (Exception e) { - Logger.loader.Error($"Could not load {Path.GetFileName(file)}!"); - Logger.loader.Error(e); + Logger.Loader.Error($"Could not load {Path.GetFileName(file)}!"); + Logger.Loader.Error(e); } return ipaPlugins; diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index 104b9234..96b6933d 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -1,4 +1,5 @@ #nullable enable +using Hive.Versioning; using IPA.JsonConverters; using IPA.Utilities; using Newtonsoft.Json; @@ -7,7 +8,7 @@ using SemVer; using System; using System.Collections.Generic; using AlmostVersionConverter = IPA.JsonConverters.AlmostVersionConverter; -using Version = SemVer.Version; +using Version = Hive.Versioning.Version; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; @@ -29,20 +30,20 @@ namespace IPA.Loader [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] public Version Version = null!; - [JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(AlmostVersionConverter))] - public AlmostVersion GameVersion = null!; + [JsonProperty("gameVersion", Required = Required.DisallowNull), JsonConverter(typeof(AlmostVersionConverter))] + public AlmostVersion? GameVersion; [JsonProperty("author", Required = Required.Always)] public string Author = null!; [JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Dependencies = new(); + public Dictionary Dependencies = new(); [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Conflicts = new(); + public Dictionary Conflicts = new(); [JsonProperty("features", Required = Required.DisallowNull), JsonConverter(typeof(FeaturesFieldConverter))] - public Dictionary Features = new(); + public Dictionary> Features = new(); [JsonProperty("loadBefore", Required = Required.DisallowNull)] public string[] LoadBefore = Array.Empty(); diff --git a/IPA.Loader/Loader/PluginMetadata.cs b/IPA.Loader/Loader/PluginMetadata.cs index d158edc8..0bb89f9c 100644 --- a/IPA.Loader/Loader/PluginMetadata.cs +++ b/IPA.Loader/Loader/PluginMetadata.cs @@ -7,7 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Version = SemVer.Version; +using SVersion = SemVer.Version; +using Version = Hive.Versioning.Version; #if NET3 using Net3_Proxy; using Path = Net3_Proxy.Path; @@ -60,7 +61,14 @@ namespace IPA.Loader /// The version of the plugin. /// /// the version of the plugin - public Version Version => manifest.Version; + [Obsolete("Use HVersion instead.")] + public SVersion Version => SVersion.ForHiveVersion(manifest.Version); + + /// + /// The version of the plugin. + /// + /// the version of the plugin + public Version HVersion => manifest.Version; /// /// The file the plugin was loaded from. @@ -144,6 +152,6 @@ namespace IPA.Loader /// Gets all of the metadata as a readable string. /// /// the readable printable metadata string - public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, UnityGame.InstallPath)}'"; + public override string ToString() => $"{Name}({Id}@{HVersion})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName ?? "", UnityGame.InstallPath)}'"; } } \ No newline at end of file diff --git a/IPA.Loader/Loader/StateTransitionTransaction.cs b/IPA.Loader/Loader/StateTransitionTransaction.cs index e3209d00..4e74d7c2 100644 --- a/IPA.Loader/Loader/StateTransitionTransaction.cs +++ b/IPA.Loader/Loader/StateTransitionTransaction.cs @@ -1,7 +1,7 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace IPA.Loader @@ -13,9 +13,9 @@ namespace IPA.Loader { private readonly HashSet currentlyEnabled; private readonly HashSet currentlyDisabled; - private readonly HashSet toEnable = new HashSet(); - private readonly HashSet toDisable = new HashSet(); - private bool stateChanged = false; + private readonly HashSet toEnable = new (); + private readonly HashSet toDisable = new (); + private bool stateChanged; internal StateTransitionTransaction(IEnumerable enabled, IEnumerable disabled) { @@ -118,16 +118,17 @@ namespace IPA.Loader /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable - public bool Enable(PluginMetadata meta, out IEnumerable disabledDeps, bool autoDeps = false) + public bool Enable(PluginMetadata meta, out IEnumerable? disabledDeps, bool autoDeps = false) { // returns whether or not state was changed ThrowIfDisposed(); + if (meta is null) throw new ArgumentNullException(nameof(meta)); if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta)) - throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin"); + throw new ArgumentException("Plugin metadata does not represent a loadable plugin", nameof(meta)); disabledDeps = null; if (IsEnabledInternal(meta)) return false; - var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m)); + var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m)).ToArray(); if (autoDeps) { foreach (var dep in needsEnabled) @@ -138,15 +139,15 @@ namespace IPA.Loader return res; } } - else if (needsEnabled.Any()) + else if (needsEnabled.Length > 0) { // there are currently enabled plugins that depend on this disabledDeps = needsEnabled; return false; } - toDisable.Remove(meta); - toEnable.Add(meta); + _ = toDisable.Remove(meta); + _ = toEnable.Add(meta); stateChanged = true; return true; } @@ -175,16 +176,17 @@ namespace IPA.Loader /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable - public bool Disable(PluginMetadata meta, out IEnumerable enabledDependents, bool autoDependents = false) + public bool Disable(PluginMetadata meta, out IEnumerable? enabledDependents, bool autoDependents = false) { // returns whether or not state was changed ThrowIfDisposed(); + if (meta is null) throw new ArgumentNullException(nameof(meta)); if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta)) - throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin"); + throw new ArgumentException("Plugin metadata does not represent a loadable plugin", nameof(meta)); enabledDependents = null; if (IsDisabledInternal(meta)) return false; - var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta)); + var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta)).ToArray(); if (autoDependents) { foreach (var dep in needsDisabled) @@ -195,15 +197,15 @@ namespace IPA.Loader return res; } } - else if (needsDisabled.Any()) + else if (needsDisabled.Length > 0) { // there are currently enabled plugins that depend on this enabledDependents = needsDisabled; return false; } - toDisable.Add(meta); - toEnable.Remove(meta); + _ = toDisable.Add(meta); + _ = toEnable.Remove(meta); stateChanged = true; return true; } @@ -247,22 +249,20 @@ namespace IPA.Loader ThrowIfDisposed(); var copy = new StateTransitionTransaction(CurrentlyEnabled, CurrentlyDisabled); foreach (var toEnable in ToEnable) - copy.toEnable.Add(toEnable); + _ = copy.toEnable.Add(toEnable); foreach (var toDisable in ToDisable) - copy.toDisable.Add(toDisable); + _ = copy.toDisable.Add(toDisable); copy.stateChanged = stateChanged; return copy; } private void ThrowIfDisposed() => ThrowIfDisposed(); - private T ThrowIfDisposed() + private T? ThrowIfDisposed() { - if (disposed) - throw new ObjectDisposedException(nameof(StateTransitionTransaction)); - return default; + return disposed ? throw new ObjectDisposedException(nameof(StateTransitionTransaction)) : default; } - private bool disposed = false; + private bool disposed; /// /// Disposes and discards this transaction without committing it. /// diff --git a/IPA.Loader/Loader/description.md b/IPA.Loader/Loader/description.md index 1dedc776..d88f70b1 100644 --- a/IPA.Loader/Loader/description.md +++ b/IPA.Loader/Loader/description.md @@ -9,8 +9,4 @@ to actually inject itself into the game. **** -The particular method of injection that BSIPA uses lets its user experience be far nicer than most others, not requiring a repatch for every game update. - -With updating the game being seamless, it also makes sense to make updating the mods themselves seamless. BSIPA's internal updater will automatically ask [BeatMods](https://beatmods.com/) -for newer versions of your installed mods. Of course, this is configurable either in the config at `UserData\Beat Saber IPA.json` or with the Mod List, which lets you control the process -to a greater extend than even the configs. \ No newline at end of file +The particular method of injection that BSIPA uses lets its user experience be far nicer than most others, not requiring a repatch for every game update. \ No newline at end of file diff --git a/IPA.Loader/Loader/manifest.json b/IPA.Loader/Loader/manifest.json index 1f6cc03d..5ba5803d 100644 --- a/IPA.Loader/Loader/manifest.json +++ b/IPA.Loader/Loader/manifest.json @@ -5,16 +5,17 @@ "#![IPA.Loader.description.md]", "A mod loader specifically for Beat Saber." ], - "gameVersion": "1.14.0", "id": "BSIPA", "name": "Beat Saber IPA", - "version": "4.1.7-pre.1", + "version": "4.2.2", "icon": "IPA.icon_white.png", "features": { - "IPA.DefineFeature": { - "type": "IPA.Loader.Features.ConfigProviderFeature", - "name": "IPA.ConfigProvider" - } + "IPA.DefineFeature": [ + { + "type": "IPA.Loader.Features.ConfigProviderFeature", + "name": "IPA.ConfigProvider" + } + ] }, "links": { "project-home": "https://bsmg.github.io/BeatSaber-IPA-Reloaded/index.html", @@ -28,6 +29,7 @@ "Libs/Ionic.Zip.dll", "Libs/Newtonsoft.Json.dll", "Libs/Mono.Cecil.dll", - "Libs/SemVer.dll" + "Libs/SemVer.dll", + "Libs/Hive.Versioning.dll" ] } \ No newline at end of file diff --git a/IPA.Loader/Logging/LogPrinter.cs b/IPA.Loader/Logging/LogPrinter.cs index 57a58f4f..bd241e3d 100644 --- a/IPA.Loader/Logging/LogPrinter.cs +++ b/IPA.Loader/Logging/LogPrinter.cs @@ -1,39 +1,40 @@ -using System; - -namespace IPA.Logging -{ - /// - /// The log printer's base class. - /// - public abstract class LogPrinter - { - /// - /// Provides a filter for which log levels to allow through. - /// - /// the level to filter to - public abstract Logger.LogLevel Filter { get; set; } - - /// - /// Prints a provided message from a given log at the specified time. - /// - /// the log level - /// the time the message was composed - /// the name of the log that created this message - /// the message - public abstract void Print(Logger.Level level, DateTime time, string logName, string message); - - /// - /// Called before the first print in a group. May be called multiple times. - /// Use this to create file handles and the like. - /// - public virtual void StartPrint() { } - - /// - /// Called after the last print in a group. May be called multiple times. - /// Use this to dispose file handles and the like. - /// - public virtual void EndPrint() { } - - internal DateTime LastUse { get; set; } - } +#nullable enable +using System; + +namespace IPA.Logging +{ + /// + /// The log printer's base class. + /// + public abstract class LogPrinter + { + /// + /// Provides a filter for which log levels to allow through. + /// + /// the level to filter to + public abstract Logger.LogLevel Filter { get; set; } + + /// + /// Prints a provided message from a given log at the specified time. + /// + /// the log level + /// the time the message was composed + /// the name of the log that created this message + /// the message + public abstract void Print(Logger.Level level, DateTime time, string logName, string message); + + /// + /// Called before the first print in a group. May be called multiple times. + /// Use this to create file handles and the like. + /// + public virtual void StartPrint() { } + + /// + /// Called after the last print in a group. May be called multiple times. + /// Use this to dispose file handles and the like. + /// + public virtual void EndPrint() { } + + internal DateTime LastUse { get; set; } + } } \ No newline at end of file diff --git a/IPA.Loader/Logging/Logger.cs b/IPA.Loader/Logging/Logger.cs index 13d2d8c3..6a37bdca 100644 --- a/IPA.Loader/Logging/Logger.cs +++ b/IPA.Loader/Logging/Logger.cs @@ -1,6 +1,5 @@ -using System; - -// ReSharper disable InconsistentNaming +#nullable enable +using System; namespace IPA.Logging { @@ -9,9 +8,9 @@ namespace IPA.Logging /// public abstract class Logger { - private static Logger _log; + private static Logger? _log; - internal static Logger log + internal static Logger Default { get { @@ -21,7 +20,7 @@ namespace IPA.Logging } } - private static StandardLogger _stdout; + private static StandardLogger? _stdout; internal static StandardLogger stdout { @@ -33,12 +32,16 @@ namespace IPA.Logging } } - internal static Logger updater => log.GetChildLogger("Updater"); - internal static Logger libLoader => log.GetChildLogger("LibraryLoader"); - internal static Logger injector => log.GetChildLogger("Injector"); - internal static Logger loader => log.GetChildLogger("Loader"); - internal static Logger features => loader.GetChildLogger("Features"); - internal static Logger config => log.GetChildLogger("Config"); + private static StandardLogger? lazyHarmony; + internal static StandardLogger Harmony => lazyHarmony ??= new StandardLogger("Harmony"); + + internal static Logger AntiMalware => Default.GetChildLogger("AntiMalware"); + internal static Logger Updater => Default.GetChildLogger("Updater"); + internal static Logger LibLoader => Default.GetChildLogger("LibraryLoader"); + internal static Logger Injector => Default.GetChildLogger("Injector"); + internal static Logger Loader => Default.GetChildLogger("Loader"); + internal static Logger Features => Loader.GetChildLogger("Features"); + internal static Logger Config => Default.GetChildLogger("Config"); internal static bool LogCreated => _log != null; /// diff --git a/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs b/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs index 1c069a94..35abbd45 100644 --- a/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs +++ b/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs @@ -1,116 +1,159 @@ -using System; -using System.Runtime.InteropServices; - -namespace IPA.Logging.Printers -{ - /// - /// Prints a pretty message to the console. - /// - public class ColoredConsolePrinter : LogPrinter - { - private Logger.LogLevel filter = Logger.LogLevel.All; - - /// - /// A filter for this specific printer. - /// - /// the filter to apply to this printer - public override Logger.LogLevel Filter { get => filter; set => filter = value; } - /// - /// The color to print messages as. - /// - /// the color to print this message as - // Initializer calls this function because Unity's .NET 3.5 doesn't have the color properties on Console - public ConsoleColor Color { get; set; } = GetConsoleColor(WinConsole.OutHandle); - - /// - /// Prints an entry to the console window. - /// - /// the of the message - /// the the message was recorded at - /// the name of the log that sent the message - /// the message to print - public override void Print(Logger.Level level, DateTime time, string logName, string message) - { - if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return; - EnsureDefaultsPopulated(WinConsole.OutHandle); - SetColor(Color, WinConsole.OutHandle); - foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) - WinConsole.ConOut.WriteLine(Logger.LogFormat, line, logName, time, level.ToString().ToUpper()); - ResetColor(WinConsole.OutHandle); - } - - private static bool _haveReadDefaultColors; - private static short _defaultColors; - - private void EnsureDefaultsPopulated(IntPtr handle, bool force = false) - { - if (!_haveReadDefaultColors | force) - { - GetConsoleScreenBufferInfo(handle, out var info); - _defaultColors = (short)(info.Attribute & ~15); - _haveReadDefaultColors = true; - } - } - - private void ResetColor(IntPtr handle) - { - GetConsoleScreenBufferInfo(handle, out var info); - var otherAttrs = (short)(info.Attribute & ~15); - SetConsoleTextAttribute(handle, (short)(otherAttrs | _defaultColors)); - } - - private void SetColor(ConsoleColor col, IntPtr handle) - { - GetConsoleScreenBufferInfo(handle, out var info); - var attr = GetAttrForeground(info.Attribute, col); - SetConsoleTextAttribute(handle, attr); - } - - private static short GetAttrForeground(int attr, ConsoleColor color) - { - attr &= ~15; - return (short)(attr | (int)color); - } - - private static ConsoleColor GetConsoleColor(IntPtr handle) - { - GetConsoleScreenBufferInfo(handle, out var info); - return (ConsoleColor)(info.Attribute & 15); - } - - - // ReSharper disable NotAccessedField.Local -#pragma warning disable 649 - private struct Coordinate - { - public short X; - public short Y; - } - - private struct SmallRect - { - public short Left; - public short Top; - public short Right; - public short Bottom; - } - - private struct ConsoleScreenBufferInfo - { - public Coordinate Size; - public Coordinate CursorPosition; - public short Attribute; - public SmallRect Window; - public Coordinate MaxWindowSize; - } - #pragma warning restore 649 - // ReSharper restore NotAccessedField.Local - - - [DllImport("kernel32.dll", EntryPoint = "GetConsoleScreenBufferInfo", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool GetConsoleScreenBufferInfo(IntPtr handle, out ConsoleScreenBufferInfo info); - - [DllImport("kernel32.dll", EntryPoint = "SetConsoleTextAttribute", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool SetConsoleTextAttribute(IntPtr handle, short attribute); - } -} +#nullable enable +using System; +using System.Runtime.InteropServices; + +namespace IPA.Logging.Printers +{ + /// + /// Prints a pretty message to the console. + /// + public class ColoredConsolePrinter : LogPrinter + { + private Logger.LogLevel filter = Logger.LogLevel.All; + + /// + /// A filter for this specific printer. + /// + /// the filter to apply to this printer + public override Logger.LogLevel Filter { get => filter; set => filter = value; } + /// + /// The color to print messages as. + /// + /// the color to print this message as + // Initializer calls this function because Unity's .NET 3.5 doesn't have the color properties on Console + public ConsoleColor Color { get; set; } = GetConsoleColor(WinConsole.OutHandle); + + private static ConsoleColor GetDarkenedColor(ConsoleColor color) + => color switch + { + ConsoleColor.Gray => ConsoleColor.DarkGray, + ConsoleColor.Blue => ConsoleColor.DarkBlue, + ConsoleColor.Green => ConsoleColor.DarkGreen, + ConsoleColor.Cyan => ConsoleColor.DarkCyan, + ConsoleColor.Red => ConsoleColor.DarkRed, + ConsoleColor.Magenta => ConsoleColor.DarkMagenta, + ConsoleColor.Yellow => ConsoleColor.DarkYellow, + ConsoleColor.White => ConsoleColor.Gray, + _ => color, + }; + + private readonly bool darkenSetManually; + private readonly bool darkenMessages; + + + public ColoredConsolePrinter() : this(Config.SelfConfig.Debug_.DarkenMessages_) + { + darkenSetManually = false; + } + + public ColoredConsolePrinter(bool darkenMessages) + { + darkenSetManually = true; + this.darkenMessages = darkenMessages; + } + + /// + /// Prints an entry to the console window. + /// + /// the of the message + /// the the message was recorded at + /// the name of the log that sent the message + /// the message to print + public override void Print(Logger.Level level, DateTime time, string logName, string message) + { + if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return; + EnsureDefaultsPopulated(WinConsole.OutHandle); + SetColor(Color, WinConsole.OutHandle); + + var prefixStr = ""; + var suffixStr = ""; + if ((darkenSetManually && darkenMessages) || (!darkenSetManually && Config.SelfConfig.Debug_.DarkenMessages_)) + { + var darkened = GetDarkenedColor(Color); + if (darkened != Color) + { + prefixStr = StdoutInterceptor.ConsoleColorToForegroundSet(darkened); + suffixStr = StdoutInterceptor.ConsoleColorToForegroundSet(Color); + } + } + + foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) + WinConsole.ConOut.WriteLine(Logger.LogFormat, prefixStr + line + suffixStr, logName, time, level.ToString().ToUpperInvariant()); + ResetColor(WinConsole.OutHandle); + } + + private static bool _haveReadDefaultColors; + private static short _defaultColors; + + private static void EnsureDefaultsPopulated(IntPtr handle, bool force = false) + { + if (!_haveReadDefaultColors | force) + { + _ = GetConsoleScreenBufferInfo(handle, out var info); + _defaultColors = (short)(info.Attribute & ~15); + _haveReadDefaultColors = true; + } + } + + private static void ResetColor(IntPtr handle) + { + _ = GetConsoleScreenBufferInfo(handle, out var info); + var otherAttrs = (short)(info.Attribute & ~15); + _ = SetConsoleTextAttribute(handle, (short)(otherAttrs | _defaultColors)); + } + + private static void SetColor(ConsoleColor col, IntPtr handle) + { + _ = GetConsoleScreenBufferInfo(handle, out var info); + var attr = GetAttrForeground(info.Attribute, col); + _ = SetConsoleTextAttribute(handle, attr); + } + + private static short GetAttrForeground(int attr, ConsoleColor color) + { + attr &= ~15; + return (short)(attr | (int)color); + } + + private static ConsoleColor GetConsoleColor(IntPtr handle) + { + _ = GetConsoleScreenBufferInfo(handle, out var info); + return (ConsoleColor)(info.Attribute & 15); + } + + + // ReSharper disable NotAccessedField.Local +#pragma warning disable 649 + private struct Coordinate + { + public short X; + public short Y; + } + + private struct SmallRect + { + public short Left; + public short Top; + public short Right; + public short Bottom; + } + + private struct ConsoleScreenBufferInfo + { + public Coordinate Size; + public Coordinate CursorPosition; + public short Attribute; + public SmallRect Window; + public Coordinate MaxWindowSize; + } + #pragma warning restore 649 + // ReSharper restore NotAccessedField.Local + + + [DllImport("kernel32.dll", EntryPoint = "GetConsoleScreenBufferInfo", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool GetConsoleScreenBufferInfo(IntPtr handle, out ConsoleScreenBufferInfo info); + + [DllImport("kernel32.dll", EntryPoint = "SetConsoleTextAttribute", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetConsoleTextAttribute(IntPtr handle, short attribute); + } +} diff --git a/IPA.Loader/Logging/Printers/GZFilePrinter.cs b/IPA.Loader/Logging/Printers/GZFilePrinter.cs index 58d50053..93fe65e5 100644 --- a/IPA.Loader/Logging/Printers/GZFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/GZFilePrinter.cs @@ -1,158 +1,170 @@ -using Ionic.Zlib; -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -#if NET3 -using Net3_Proxy; -using Path = Net3_Proxy.Path; -#endif - -namespace IPA.Logging.Printers -{ - /// - /// A abstract class that provides the utilities to write to a GZip file. - /// - public abstract class GZFilePrinter : LogPrinter, IDisposable - { - [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool CreateHardLink( - string lpFileName, - string lpExistingFileName, - IntPtr lpSecurityAttributes - ); - -#if NET4 - private const RegexOptions reOptions = RegexOptions.Compiled; -#elif NET3 // Needed because Compiled doesn't exist in Unity's .NET 3 runtime - private const RegexOptions reOptions = RegexOptions.None; -#endif - - internal static Regex removeControlCodes = new Regex("\x1b\\[\\d+m", reOptions); - - private FileInfo fileInfo; - - /// - /// The that writes to the GZip file. - /// - /// the writer to the underlying filestream - protected StreamWriter FileWriter; - - private FileStream fstream; - - /// - /// Gets the for the file to write to. - /// - /// the file to write to - protected abstract FileInfo GetFileInfo(); - - private const string latestFormat = "_latest{0}"; - - private void InitLog() - { - try - { - if (fileInfo == null) - { // first init - fileInfo = GetFileInfo(); - var ext = fileInfo.Extension; - - var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), string.Format(latestFormat, ext))); - if (symlink.Exists) symlink.Delete(); - - foreach (var file in fileInfo.Directory.EnumerateFiles("*.log", SearchOption.TopDirectoryOnly)) - { - if (file.Equals(fileInfo)) continue; - if (file.Extension == ".gz") continue; - - CompressOldLog(file); - } - - fileInfo.Create().Close(); - - try - { - if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero)) - { - var error = Marshal.GetLastWin32Error(); - Logger.log.Error($"Hardlink creation failed ({error})"); - } - } - catch (Exception e) - { - Logger.log.Error("Error creating latest hardlink!"); - Logger.log.Error(e); - } - } - } - catch (Exception e) - { - Logger.log.Error("Error initializing log!"); - Logger.log.Error(e); - } - } - - private static async void CompressOldLog(FileInfo file) - { - Logger.log.Debug($"Compressing log file {file}"); - - var newFile = new FileInfo(file.FullName + ".gz"); - - using (var istream = file.OpenRead()) - using (var ostream = newFile.Create()) - using (var gz = new GZipStream(ostream, CompressionMode.Compress, CompressionLevel.BestCompression, false)) - await istream.CopyToAsync(gz); - - file.Delete(); - } - - /// - /// Called at the start of any print session. - /// - public sealed override void StartPrint() - { - InitLog(); - - fstream = fileInfo.Open(FileMode.Append, FileAccess.Write); - FileWriter = new StreamWriter(fstream, new UTF8Encoding(false)); - } - - /// - /// Called at the end of any print session. - /// - public sealed override void EndPrint() - { - FileWriter.Flush(); - fstream.Flush(); - FileWriter.Dispose(); - fstream.Dispose(); - FileWriter = null; - fstream = null; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the file printer. - /// - /// does nothing - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - FileWriter.Flush(); - fstream.Flush(); - FileWriter.Close(); - fstream.Close(); - FileWriter.Dispose(); - fstream.Dispose(); - } - } - } +#nullable enable +using Ionic.Zlib; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +#endif + +namespace IPA.Logging.Printers +{ + /// + /// A abstract class that provides the utilities to write to a GZip file. + /// + public abstract class GZFilePrinter : LogPrinter, IDisposable + { + [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CreateHardLink( + string lpFileName, + string lpExistingFileName, + IntPtr lpSecurityAttributes + ); + +#if NET4 + private const RegexOptions reOptions = RegexOptions.Compiled; +#elif NET3 // Needed because Compiled doesn't exist in Unity's .NET 3 runtime + private const RegexOptions reOptions = RegexOptions.None; +#endif + + internal static Regex removeControlCodes = new("\x1b\\[\\d+m", reOptions); + + private FileInfo? fileInfo; + + /// + /// The that writes to the GZip file. + /// + /// the writer to the underlying filestream + protected StreamWriter? FileWriter; + + private FileStream? fstream; + + /// + /// Gets the for the file to write to. + /// + /// the file to write to + protected abstract FileInfo GetFileInfo(); + + private const string latestFormat = "_latest{0}"; + + [MemberNotNull(nameof(fileInfo))] + private void InitLog() + { + try + { + if (fileInfo == null) + { // first init + fileInfo = GetFileInfo(); + var ext = fileInfo.Extension; + + var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), string.Format(latestFormat, ext))); + if (symlink.Exists) symlink.Delete(); + + foreach (var file in fileInfo.Directory.EnumerateFiles("*.log", SearchOption.TopDirectoryOnly)) + { + if (file.Equals(fileInfo)) continue; + if (file.Extension == ".gz") continue; + + CompressOldLog(file); + } + + fileInfo.Create().Close(); + + try + { + if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero)) + { + var error = Marshal.GetLastWin32Error(); + Logger.Default.Error($"Hardlink creation failed ({error})"); + } + } + catch (Exception e) + { + Logger.Default.Error("Error creating latest hardlink!"); + Logger.Default.Error(e); + } + } + } + catch (Exception e) + { + Logger.Default.Error("Error initializing log!"); + Logger.Default.Error(e); + throw; + } + } + + private static async void CompressOldLog(FileInfo file) + { + try + { + Logger.Default.Debug($"Compressing log file {file}"); + + var newFile = new FileInfo(file.FullName + ".gz"); + + using (var istream = file.OpenRead()) + using (var ostream = newFile.Create()) + using (var gz = new GZipStream(ostream, CompressionMode.Compress, CompressionLevel.BestCompression, false)) + await istream.CopyToAsync(gz).ConfigureAwait(false); + + file.Delete(); + } + catch (Exception e) + { + Logger.Default.Error("Error compressing old log file:"); + Logger.Default.Error(e); + } + } + + /// + /// Called at the start of any print session. + /// + public sealed override void StartPrint() + { + InitLog(); + + fstream = fileInfo.Open(FileMode.Append, FileAccess.Write); + FileWriter = new StreamWriter(fstream, new UTF8Encoding(false)); + } + + /// + /// Called at the end of any print session. + /// + public sealed override void EndPrint() + { + FileWriter?.Flush(); + fstream?.Flush(); + FileWriter?.Dispose(); + fstream?.Dispose(); + FileWriter = null; + fstream = null; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the file printer. + /// + /// does nothing + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + FileWriter?.Flush(); + fstream?.Flush(); + FileWriter?.Close(); + fstream?.Close(); + FileWriter?.Dispose(); + fstream?.Dispose(); + } + } + } } \ No newline at end of file diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs index 72a5b3c3..2a253b7e 100644 --- a/IPA.Loader/Logging/StandardLogger.cs +++ b/IPA.Loader/Logging/StandardLogger.cs @@ -25,10 +25,7 @@ namespace IPA.Logging /// public class StandardLogger : Logger { - private static readonly List defaultPrinters = new() - { - new GlobalLogFilePrinter() - }; + private static readonly List defaultPrinters = new(); static StandardLogger() { @@ -115,6 +112,7 @@ namespace IPA.Logging private readonly Dictionary children = new(); + private static bool addedFilePrinter = false; /// /// Configures internal debug settings based on the config passed in. /// @@ -124,6 +122,11 @@ namespace IPA.Logging PrintFilter = SelfConfig.Debug_.ShowDebug_ ? LogLevel.All : LogLevel.InfoUp; showTrace = SelfConfig.Debug_.ShowTrace_; syncLogging = SelfConfig.Debug_.SyncLogging_; + if (SelfConfig.CommandLineValues.WriteLogs && !addedFilePrinter) + { + addedFilePrinter = true; + AddDefaultPrinter(new GlobalLogFilePrinter()); + } } private StandardLogger(StandardLogger parent, string subName) @@ -457,4 +460,4 @@ namespace IPA.Logging _ => throw new InvalidOperationException() }; } -} \ No newline at end of file +} diff --git a/IPA.Loader/Logging/StdoutInterceptor.cs b/IPA.Loader/Logging/StdoutInterceptor.cs index 31efdae2..8ba05d24 100644 --- a/IPA.Loader/Logging/StdoutInterceptor.cs +++ b/IPA.Loader/Logging/StdoutInterceptor.cs @@ -1,9 +1,12 @@ -using HarmonyLib; +#nullable enable +using HarmonyLib; using System; using System.Collections.Generic; using System.IO; using System.Reflection.Emit; using System.Text; +using System.Threading; +using static IPA.Logging.Logger; namespace IPA.Logging { @@ -19,7 +22,7 @@ namespace IPA.Logging } private string lineBuffer = ""; - private readonly object bufferLock = new object(); + private readonly object bufferLock = new(); public override void Write(string value) { @@ -51,7 +54,7 @@ namespace IPA.Logging private const ConsoleColor defaultColor = ConsoleColor.Gray; private ConsoleColor currentColor = defaultColor; - private static string ConsoleColorToForegroundSet(ConsoleColor col) + internal static string ConsoleColorToForegroundSet(ConsoleColor col) { if (!WinConsole.UseVTEscapes) return ""; string code = "0"; // reset @@ -111,8 +114,8 @@ namespace IPA.Logging return "\x1b[" + code + "m"; } - private static StdoutInterceptor stdoutInterceptor; - private static StdoutInterceptor stderrInterceptor; + private static StdoutInterceptor? stdoutInterceptor; + private static StdoutInterceptor? stderrInterceptor; private static class ConsoleHarmonyPatches { @@ -127,24 +130,24 @@ namespace IPA.Logging try { if (resetColor != null) - harmony.Patch(resetColor, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchResetColor))); + _ = harmony.Patch(resetColor, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchResetColor))); if (foregroundProperty != null) { - harmony.Patch(setFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchSetForegroundColor))); - harmony.Patch(getFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchGetForegroundColor))); + _ = harmony.Patch(setFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchSetForegroundColor))); + _ = harmony.Patch(getFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchGetForegroundColor))); } } catch (Exception e) { // Harmony might be fucked because of wierdness in Guid.NewGuid, don't let that kill us - Logger.log.Error("Error installing harmony patches to intercept Console color properties:"); - Logger.log.Error(e); + Logger.Default.Error("Error installing harmony patches to intercept Console color properties:"); + Logger.Default.Error(e); } } - public static ConsoleColor GetColor() => stdoutInterceptor.currentColor; - public static void SetColor(ConsoleColor col) => stdoutInterceptor.currentColor = col; - public static void ResetColor() => stdoutInterceptor.currentColor = defaultColor; + public static ConsoleColor GetColor() => stdoutInterceptor!.currentColor; + public static void SetColor(ConsoleColor col) => stdoutInterceptor!.currentColor = col; + public static void ResetColor() => stdoutInterceptor!.currentColor = defaultColor; public static IEnumerable PatchGetForegroundColor(IEnumerable _) { @@ -178,20 +181,21 @@ namespace IPA.Logging } } - private static Harmony harmony; - private static bool usingInterceptor = false; + private static Harmony? harmony; + private static bool usingInterceptor; public static void Intercept() { if (!usingInterceptor) { usingInterceptor = true; - if (harmony == null) - harmony = new Harmony("BSIPA Console Redirector Patcher"); - if (stdoutInterceptor == null) - stdoutInterceptor = new StdoutInterceptor(); - if (stderrInterceptor == null) - stderrInterceptor = new StdoutInterceptor() { isStdErr = true }; + + EnsureHarmonyLogging(); + + HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll = true; + harmony ??= new Harmony("BSIPA Console Redirector Patcher"); + stdoutInterceptor ??= new StdoutInterceptor(); + stderrInterceptor ??= new StdoutInterceptor { isStdErr = true }; RedirectConsole(); ConsoleHarmonyPatches.Patch(harmony); @@ -206,5 +210,31 @@ namespace IPA.Logging Console.SetError(stderrInterceptor); } } + + private static int harmonyLoggingInited; + // I'm not completely sure this is the best place for this, but whatever + internal static void EnsureHarmonyLogging() + { + if (Interlocked.Exchange(ref harmonyLoggingInited, 1) != 0) + return; + + HarmonyLib.Tools.Logger.ChannelFilter = HarmonyLib.Tools.Logger.LogChannel.All & ~HarmonyLib.Tools.Logger.LogChannel.IL; + HarmonyLib.Tools.Logger.MessageReceived += (s, e) => + { + var msg = e.Message; + var lvl = e.LogChannel switch + { + HarmonyLib.Tools.Logger.LogChannel.None => Level.Notice, + HarmonyLib.Tools.Logger.LogChannel.Info => Level.Trace, // HarmonyX logs a *lot* of Info messages + HarmonyLib.Tools.Logger.LogChannel.IL => Level.Trace, + HarmonyLib.Tools.Logger.LogChannel.Warn => Level.Warning, + HarmonyLib.Tools.Logger.LogChannel.Error => Level.Error, + HarmonyLib.Tools.Logger.LogChannel.Debug => Level.Debug, + HarmonyLib.Tools.Logger.LogChannel.All => Level.Critical, + _ => Level.Critical, + }; + Logger.Harmony.Log(lvl, msg); + }; + } } } diff --git a/IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs b/IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs index 3cd4ca6f..7394b1a6 100644 --- a/IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs +++ b/IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs @@ -109,4 +109,20 @@ namespace IPA { EdgeLifecycleType IEdgeLifecycleAttribute.Type => EdgeLifecycleType.Disable; } + + /// + /// Indicates that the applied plugin class does not need or + /// methods. + /// + /// + /// This is typically only the case when some other utility mod handles their lifecycle for + /// them, such as with SiraUtil and Zenject. + /// + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class NoEnableDisableAttribute : Attribute + { + + } } diff --git a/IPA.Loader/Properties/AssemblyInfo.cs b/IPA.Loader/Properties/AssemblyInfo.cs index 456c770b..6f7ebb52 100644 --- a/IPA.Loader/Properties/AssemblyInfo.cs +++ b/IPA.Loader/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,6 +19,7 @@ using System.Runtime.InteropServices; // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("5ad344f0-01a0-4ca8-92e5-9d095737744d")] diff --git a/IPA.Loader/Utilities/AlmostVersion.cs b/IPA.Loader/Utilities/AlmostVersion.cs index 182c1371..a9527503 100644 --- a/IPA.Loader/Utilities/AlmostVersion.cs +++ b/IPA.Loader/Utilities/AlmostVersion.cs @@ -1,270 +1,323 @@ -using IPA.Config.Data; -using IPA.Config.Stores; -using IPA.Config.Stores.Converters; -using System; -using System.Collections.Generic; -using Version = SemVer.Version; - -namespace IPA.Utilities -{ - /// - /// A type that wraps so that the string of the version is stored when the string is - /// not a valid . - /// - public class AlmostVersion : IComparable, IComparable - { - /// - /// Represents a storage type of either parsed object or raw . - /// - public enum StoredAs - { - /// - /// The version was stored as a . - /// - SemVer, - /// - /// The version was stored as a . - /// - String - } - - /// - /// Creates a new with the version string provided in . - /// - /// the version string to store - public AlmostVersion(string vertext) - { - if (!TryParseFrom(vertext, StoredAs.SemVer)) - TryParseFrom(vertext, StoredAs.String); - } - - /// - /// Creates an from the provided in . - /// - /// the to store - public AlmostVersion(Version ver) - { - SemverValue = ver; - StorageMode = StoredAs.SemVer; - } - - /// - /// Creates an from the version string in stored using - /// the storage mode specified in . - /// - /// the text to parse as an - /// the storage mode to store the version in - public AlmostVersion(string vertext, StoredAs mode) - { - if (!TryParseFrom(vertext, mode)) - throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!"); - } - - /// - /// Creates a new from the version string in stored the - /// same way as the passed in . - /// - /// the text to parse as an - /// an to copy the storage mode of - public AlmostVersion(string vertext, AlmostVersion copyMode) - { - if (copyMode == null) - throw new ArgumentNullException(nameof(copyMode)); - - if (!TryParseFrom(vertext, copyMode.StorageMode)) - TryParseFrom(vertext, StoredAs.String); // silently parse differently - } - - private bool TryParseFrom(string str, StoredAs mode) - { - if (mode == StoredAs.SemVer) - try - { - SemverValue = new Version(str, true); - StorageMode = StoredAs.SemVer; - return true; - } - catch - { - return false; - } - else - { - StringValue = str; - StorageMode = StoredAs.String; - return true; - } - } - - /// - /// The value of the if it was stored as a . - /// - /// the stored value as a , or if not stored as a string. - public string StringValue { get; private set; } = null; - - /// - /// The value of the if it was stored as a . - /// - /// the stored value as a , or if not stored as a version. - public Version SemverValue { get; private set; } = null; - - /// - /// The way the value is stored, whether it be as a or a . - /// - /// the storage mode used to store this value - public StoredAs StorageMode { get; private set; } - - // can I just this? - /// - /// Gets a string representation of the current version. If the value is stored as a string, this returns it. If it is - /// stored as a , it is equivalent to calling . - /// - /// a string representation of the current version - /// - public override string ToString() => - StorageMode == StoredAs.SemVer ? SemverValue.ToString() : StringValue; - - /// - /// Compares to the in using - /// or , depending on the current store. - /// - /// - /// The storage methods of the two objects must be the same, or this will throw an . - /// - /// the to compare to - /// less than 0 if is considered bigger than , 0 if equal, and greater than zero if smaller - /// - public int CompareTo(AlmostVersion other) - { - if (other == null) return -1; - if (StorageMode != other.StorageMode) - throw new InvalidOperationException("Cannot compare AlmostVersions with different stores!"); - - if (StorageMode == StoredAs.SemVer) - return SemverValue.CompareTo(other.SemverValue); - else - return StringValue.CompareTo(other.StringValue); - } - - /// - /// Compares to the in using . - /// - /// - /// The storage method of must be , else an will - /// be thrown. - /// - /// the to compare to - /// less than 0 if is considered bigger than , 0 if equal, and greater than zero if smaller - /// - public int CompareTo(Version other) - { - if (StorageMode != StoredAs.SemVer) - throw new InvalidOperationException("Cannot compare a SemVer version with an AlmostVersion stored as a string!"); - - return SemverValue.CompareTo(other); - } - - /// - /// Performs a strict equality check between and . - /// - /// - /// This may return where returns - /// - /// the object to compare to - /// if they are equal, otherwise - /// - public override bool Equals(object obj) - { - return obj is AlmostVersion version && - SemverValue == version.SemverValue && - StringValue == version.StringValue && - StorageMode == version.StorageMode; - } - - /// - /// Default generated hash code function generated by VS. - /// - /// a value unique to each object, except those that are considered equal by - /// - public override int GetHashCode() - { - var hashCode = -126402897; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SemverValue); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(StringValue); - hashCode = hashCode * -1521134295 + StorageMode.GetHashCode(); - return hashCode; - } - - /// - /// Compares two versions, only taking into account the numeric part of the version if they are stored as s, - /// or strict equality if they are stored as s. - /// - /// - /// This is a looser equality than , meaning that this may return where - /// does not. - /// - /// the first value to compare - /// the second value to compare - /// if they are mostly equal, otherwise - /// - public static bool operator==(AlmostVersion l, AlmostVersion r) - { - if (l is null && r is null) return true; - if (l is null || r is null) return false; - if (l.StorageMode != r.StorageMode) return false; - if (l.StorageMode == StoredAs.SemVer) - return Utils.VersionCompareNoPrerelease(l.SemverValue, r.SemverValue) == 0; - else - return l.StringValue == r.StringValue; - } - - /// - /// The opposite of . Equivalent to !(l == r). - /// - /// the first value to compare - /// the second value to compare - /// if they are not mostly equal, otherwise - /// - public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r); - - // implicitly convertible from Version - /// - /// Implicitly converts a to using . - /// - /// the to convert - /// - public static implicit operator AlmostVersion(Version ver) => new AlmostVersion(ver); - - // implicitly convertible to Version - /// - /// Implicitly converts an to , if applicable, using . - /// If not applicable, returns - /// - /// the to convert to a - /// - public static implicit operator Version(AlmostVersion av) => av?.SemverValue; - } - - /// - /// A for s. - /// - public sealed class AlmostVersionConverter : ValueConverter - { - /// - /// Converts a node into an . - /// - /// the node to convert - /// the owner of the new object - /// - public override AlmostVersion FromValue(Value value, object parent) - => new AlmostVersion(Converter.Default.FromValue(value, parent)); - /// - /// Converts an to a node. - /// - /// the to convert - /// the parent of - /// a node representing - public override Value ToValue(AlmostVersion obj, object parent) - => Value.From(obj.ToString()); - } -} +#nullable enable +using IPA.Config.Data; +using IPA.Config.Stores; +using IPA.Config.Stores.Converters; +using System; +using System.Collections.Generic; +using SVersion = SemVer.Version; +using Version = Hive.Versioning.Version; + +namespace IPA.Utilities +{ + /// + /// A type that wraps so that the string of the version is stored when the string is + /// not a valid . + /// + public class AlmostVersion : IComparable, IComparable, +#pragma warning disable CS0618 // Type or member is obsolete + IComparable +#pragma warning restore CS0618 // Type or member is obsolete + { + /// + /// Represents a storage type of either parsed object or raw . + /// + public enum StoredAs + { + /// + /// The version was stored as a . + /// + SemVer, + /// + /// The version was stored as a . + /// + String + } + + /// + /// Creates a new with the version string provided in . + /// + /// the version string to store + public AlmostVersion(string vertext) + { + if (!TryParseFrom(vertext, StoredAs.SemVer)) + _ = TryParseFrom(vertext, StoredAs.String); + } + + /// + /// Creates an from the provided in . + /// + /// the to store + public AlmostVersion(Version ver) + { + SemverValue = ver; + StorageMode = StoredAs.SemVer; + } + + /// + /// Creates an from the provided in . + /// + /// the to store + [Obsolete("Use Hive.Versioning.Version constructor instead.")] + public AlmostVersion(SVersion ver) : this(ver?.UnderlyingVersion ?? throw new ArgumentNullException(nameof(ver))) { } + + /// + /// Creates an from the version string in stored using + /// the storage mode specified in . + /// + /// the text to parse as an + /// the storage mode to store the version in + public AlmostVersion(string vertext, StoredAs mode) + { + if (!TryParseFrom(vertext, mode)) + throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!"); + } + + /// + /// Creates a new from the version string in stored the + /// same way as the passed in . + /// + /// the text to parse as an + /// an to copy the storage mode of + public AlmostVersion(string vertext, AlmostVersion copyMode) + { + if (copyMode is null) + throw new ArgumentNullException(nameof(copyMode)); + + if (!TryParseFrom(vertext, copyMode.StorageMode)) + _ = TryParseFrom(vertext, StoredAs.String); // silently parse differently + } + + private bool TryParseFrom(string str, StoredAs mode) + { + if (mode == StoredAs.SemVer) + { + StorageMode = StoredAs.SemVer; + var result = Version.TryParse(str, out var version); + SemverValue = version; + return result; + } + else + { + StringValue = str; + StorageMode = StoredAs.String; + return true; + } + } + + /// + /// The value of the if it was stored as a . + /// + /// the stored value as a , or if not stored as a string. + public string? StringValue { get; private set; } + + /// + /// The value of the if it was stored as a . + /// + /// the stored value as a , or if not stored as a version. + public Version? SemverValue { get; private set; } + + /// + /// The way the value is stored, whether it be as a or a . + /// + /// the storage mode used to store this value + public StoredAs StorageMode { get; private set; } + + /// + /// Gets a string representation of the current version. If the value is stored as a string, this returns it. If it is + /// stored as a , it is equivalent to calling . + /// + /// a string representation of the current version + /// + public override string ToString() => + StorageMode == StoredAs.SemVer ? SemverValue!.ToString() : StringValue!; + + /// + /// Compares to the in using + /// or , depending on the current store. + /// + /// + /// The storage methods of the two objects must be the same, or this will throw an . + /// + /// the to compare to + /// less than 0 if is considered bigger than , 0 if equal, and greater than zero if smaller + /// + public int CompareTo(AlmostVersion other) + { + if (other is null) return 1; + + return StorageMode == StoredAs.SemVer && other.StorageMode == StoredAs.SemVer + ? SemverValue!.CompareTo(other.SemverValue!) + : string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); + } + + /// + /// Compares to the in using . + /// + /// + /// The storage method of must be , else an will + /// be thrown. + /// + /// the to compare to + /// less than 0 if is considered bigger than , 0 if equal, and greater than zero if smaller + /// + public int CompareTo(Version other) + { + if (StorageMode != StoredAs.SemVer) + throw new InvalidOperationException("Cannot compare a SemVer version with an AlmostVersion stored as a string!"); + + return SemverValue!.CompareTo(other); + } + + /// + /// Compares to the in using . + /// + /// + /// The storage method of must be , else an will + /// be thrown. + /// + /// the to compare to + /// less than 0 if is considered bigger than , 0 if equal, and greater than zero if smaller + /// + [Obsolete("Use the Hive.Versioning.Version overload instead.")] + public int CompareTo(SVersion other) => CompareTo(other.UnderlyingVersion); + + /// + /// Performs a strict equality check between and . + /// + /// + /// This may return where returns + /// + /// the object to compare to + /// if they are equal, otherwise + /// + public override bool Equals(object? obj) + { + return obj is AlmostVersion version && + SemverValue == version.SemverValue && + StringValue == version.StringValue && + StorageMode == version.StorageMode; + } + + /// + /// Default generated hash code function generated by VS. + /// + /// a value unique to each object, except those that are considered equal by + /// + public override int GetHashCode() + { + var hashCode = -126402897; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(SemverValue); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(StringValue); + hashCode = (hashCode * -1521134295) + StorageMode.GetHashCode(); + return hashCode; + } + + /// + /// Compares two versions, only taking into account the numeric part of the version if they are stored as s, + /// or strict equality if they are stored as s. + /// + /// + /// This is a looser equality than , meaning that this may return where + /// does not. + /// + /// the first value to compare + /// the second value to compare + /// if they are mostly equal, otherwise + /// + public static bool operator==(AlmostVersion l, AlmostVersion r) + { + if (l is null && r is null) return true; + if (l is null || r is null) return false; + if (l.StorageMode != r.StorageMode) return false; + return l.StorageMode == StoredAs.SemVer + ? Utils.VersionCompareNoPrerelease(l.SemverValue!, r.SemverValue!) == 0 + : l.StringValue == r.StringValue; + } + + /// + /// The opposite of . Equivalent to !(l == r). + /// + /// the first value to compare + /// the second value to compare + /// if they are not mostly equal, otherwise + /// + public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r); + + // implicitly convertible from Version +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CA2225 // Operator overloads have named alternates + /// + /// Implicitly converts a to using . + /// + /// the to convert + /// + [Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")] + public static implicit operator AlmostVersion?(SVersion? ver) => ver is null ? null : new(ver); + + // implicitly convertible to Version + /// + /// Implicitly converts an to , if applicable, using . + /// If not applicable, returns + /// + /// the to convert to a + /// + [Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")] + public static implicit operator SVersion?(AlmostVersion? av) => av?.SemverValue is not null ? SVersion.ForHiveVersion(av.SemverValue) : null; +#pragma warning restore CS0618 // Type or member is obsolete + /// + /// Implicitly converts a to using . + /// + /// the to convert + /// + public static implicit operator AlmostVersion?(Version? ver) => ver is null ? null : new(ver); + + // implicitly convertible to Version + /// + /// Implicitly converts an to , if applicable, using . + /// If not applicable, returns + /// + /// the to convert to a + /// + public static implicit operator Version?(AlmostVersion av) => av?.SemverValue; +#pragma warning restore CA2225 // Operator overloads have named alternates + + public static bool operator <(AlmostVersion left, AlmostVersion right) + => left is null ? right is not null : left.CompareTo(right) < 0; + + public static bool operator <=(AlmostVersion left, AlmostVersion right) + => left is null || left.CompareTo(right) <= 0; + + public static bool operator >(AlmostVersion left, AlmostVersion right) + => left is not null && left.CompareTo(right) > 0; + + public static bool operator >=(AlmostVersion left, AlmostVersion right) + => left is null ? right is null : left.CompareTo(right) >= 0; + } + + /// + /// A for s. + /// + public sealed class AlmostVersionConverter : ValueConverter + { + /// + /// Converts a node into an . + /// + /// the node to convert + /// the owner of the new object + /// + public override AlmostVersion? FromValue(Value? value, object parent) + => Converter.Default.FromValue(value, parent) switch + { + { } v => new(v), + _ => null + }; + /// + /// Converts an to a node. + /// + /// the to convert + /// the parent of + /// a node representing + public override Value? ToValue(AlmostVersion? obj, object parent) + => Value.From(obj?.ToString()); + } +} diff --git a/IPA.Loader/Utilities/Async/Coroutines.cs b/IPA.Loader/Utilities/Async/Coroutines.cs index b474e566..2062da28 100644 --- a/IPA.Loader/Utilities/Async/Coroutines.cs +++ b/IPA.Loader/Utilities/Async/Coroutines.cs @@ -54,10 +54,10 @@ namespace IPA.Utilities.Async public static Task AsTask(IEnumerator coroutine) { if (!UnityGame.OnMainThread) - return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine)).Unwrap(); + return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine), default, default, UnityMainThreadTaskScheduler.Default).Unwrap(); var tcs = new TaskCompletionSource(coroutine, AsTaskSourceOptions); - PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs)); + _ = PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs)); return tcs.Task; } @@ -85,7 +85,7 @@ namespace IPA.Utilities.Async enumerators.Push(coroutine); } - private readonly Stack enumerators = new Stack(2); + private readonly Stack enumerators = new(2); public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek @@ -116,7 +116,7 @@ namespace IPA.Utilities.Async } else { // this enumerator completed, so pop it and continue - enumerators.Pop(); + _ = enumerators.Pop(); continue; } } diff --git a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs index e3fb3f54..fa4aea3a 100644 --- a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs +++ b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs @@ -1,12 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace IPA.Utilities.Async @@ -20,15 +19,15 @@ namespace IPA.Utilities.Async /// Gets the default main thread scheduler that is managed by BSIPA. /// /// a scheduler that is managed by BSIPA - public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); + public static new UnityMainThreadTaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); /// /// Gets a factory for creating tasks on . /// /// a factory for creating tasks on the default scheduler public static TaskFactory Factory { get; } = new TaskFactory(Default); - private readonly ConcurrentQueue tasks = new ConcurrentQueue(); - private static readonly ConditionalWeakTable itemTable = new ConditionalWeakTable(); + private readonly ConcurrentQueue tasks = new(); + private static readonly ConditionalWeakTable itemTable = new(); private class QueueItem : IEquatable, IEquatable { @@ -43,7 +42,9 @@ namespace IPA.Utilities.Async } } - public Task Task { get; private set; } = null; + public Task? Task { get; private set; } + + public Action? Action { get; private set; } public QueueItem(Task task) { @@ -51,7 +52,13 @@ namespace IPA.Utilities.Async Task = task; } - public bool Equals(Task other) => HasTask && other.Equals(Task); + public QueueItem(Action action) + { + HasTask = true; + Action = action; + } + + public bool Equals(Task? other) => other is not null && HasTask && other.Equals(Task); public bool Equals(QueueItem other) => other.HasTask == HasTask && Equals(other.Task); } @@ -146,7 +153,11 @@ namespace IPA.Utilities.Async do if (!tasks.TryDequeue(out task)) goto exit; // try dequeue, if we can't exit while (!task.HasTask); // if the dequeued task is empty, try again - TryExecuteTask(task.Task); + if (task.Task is not null) + { + _ = TryExecuteTask(task.Task); + } + task.Action?.Invoke(); } exit: sw.Reset(); @@ -181,7 +192,7 @@ namespace IPA.Utilities.Async /// nothing /// Always. protected override IEnumerable GetScheduledTasks() - => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).ToArray(); + => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).NonNull().ToArray(); /// /// Queues a given to this scheduler. The must be @@ -198,6 +209,13 @@ namespace IPA.Utilities.Async tasks.Enqueue(item); } + internal void QueueAction(Action action) + { + ThrowIfDisposed(); + + tasks.Enqueue(new(action)); + } + /// /// Runs the task inline if the current thread is the Unity main thread. /// diff --git a/IPA.Loader/Utilities/CriticalSection.cs b/IPA.Loader/Utilities/CriticalSection.cs index c5ed1029..61d3d85d 100644 --- a/IPA.Loader/Utilities/CriticalSection.cs +++ b/IPA.Loader/Utilities/CriticalSection.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; +#nullable enable +using System; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using IPA.Logging; namespace IPA.Utilities @@ -16,14 +13,14 @@ namespace IPA.Utilities internal static void Configure() { - Logger.log.Debug("Configuring exit handlers"); + Logger.Default.Debug("Configuring exit handlers"); ResetExitHandlers(); } - private static void Reset(object sender, EventArgs e) + private static void Reset(object? sender, EventArgs? e) { - Win32.SetConsoleCtrlHandler(registeredHandler, false); + _ = Win32.SetConsoleCtrlHandler(registeredHandler, false); WinHttp.SetPeekMessageHook(null); } @@ -32,8 +29,8 @@ namespace IPA.Utilities private static readonly Win32.ConsoleCtrlDelegate registeredHandler = HandleExit; internal static void ResetExitHandlers() { - Win32.SetConsoleCtrlHandler(registeredHandler, false); - Win32.SetConsoleCtrlHandler(registeredHandler, true); + _ = Win32.SetConsoleCtrlHandler(registeredHandler, false); + _ = Win32.SetConsoleCtrlHandler(registeredHandler, true); WinHttp.SetPeekMessageHook(PeekMessageHook); AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; @@ -60,14 +57,14 @@ namespace IPA.Utilities [DllImport("bsipa-doorstop")] public static extern void SetPeekMessageHook( [MarshalAs(UnmanagedType.FunctionPtr)] - PeekMessageHook hook); + PeekMessageHook? hook); [DllImport("bsipa-doorstop")] public static extern void SetIgnoreUnhandledExceptions( [MarshalAs(UnmanagedType.Bool)] bool ignore); } - private static Win32.ConsoleCtrlDelegate _handler = null; + private static Win32.ConsoleCtrlDelegate? _handler = null; private static volatile bool isInExecuteSection = false; // returns true to continue looping and calling PeekMessage diff --git a/IPA.Loader/Utilities/EnumerableExtensions.cs b/IPA.Loader/Utilities/EnumerableExtensions.cs index bf74bc63..e6c84779 100644 --- a/IPA.Loader/Utilities/EnumerableExtensions.cs +++ b/IPA.Loader/Utilities/EnumerableExtensions.cs @@ -13,6 +13,7 @@ namespace IPA.Utilities /// public static class EnumerableExtensions { + /* /// /// Adds a value to the beginning of the sequence. /// @@ -164,6 +165,7 @@ namespace IPA.Utilities IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + */ /// /// LINQ-style extension method that filters elements out of an enumeration. diff --git a/IPA.Loader/Utilities/ReflectionUtil.cs b/IPA.Loader/Utilities/ReflectionUtil.cs index 6f0a0195..9dc16e6a 100644 --- a/IPA.Loader/Utilities/ReflectionUtil.cs +++ b/IPA.Loader/Utilities/ReflectionUtil.cs @@ -130,6 +130,19 @@ namespace IPA.Utilities return copy; } + /// + /// Converts the property name to the one of the compiler-generated backing field. + /// This can be used for the field-based reflection when you want to set the value of a get-only property + /// + /// Name of the property + /// Name of the backing field + /// + /// Only works for properties with compiler-generated backing fields. + /// This is only a simple method and doesn't have any guarantees to work 100% of the time across different compilers/runtimes. + /// See this link for more info. + /// + public static string ToCompilerGeneratedBackingField(string propertyName) => $"<{propertyName}>k__BackingField"; + private static void CopyForType(Type type, Component source, Component destination) { FieldInfo[] myObjectFields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); diff --git a/IPA.Loader/Utilities/UnityGame.cs b/IPA.Loader/Utilities/UnityGame.cs index 259c6305..e87c414d 100644 --- a/IPA.Loader/Utilities/UnityGame.cs +++ b/IPA.Loader/Utilities/UnityGame.cs @@ -1,6 +1,9 @@ -using IPA.Config; +#nullable enable +using IPA.Config; +using IPA.Utilities.Async; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -17,17 +20,17 @@ namespace IPA.Utilities /// public static class UnityGame { - private static AlmostVersion _gameVersion; + private static AlmostVersion? _gameVersion; /// /// Provides the current game version. /// /// the SemVer version of the game - public static AlmostVersion GameVersion => _gameVersion ?? (_gameVersion = new AlmostVersion(ApplicationVersionProxy)); + public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy); internal static void SetEarlyGameVersion(AlmostVersion ver) { _gameVersion = ver; - Logging.Logger.log.Debug($"GameVersion set early to {ver}"); + Logging.Logger.Default.Debug($"GameVersion set early to {ver}"); } private static string ApplicationVersionProxy { @@ -40,15 +43,15 @@ namespace IPA.Utilities } catch(MissingMemberException ex) { - Logging.Logger.log.Error($"Tried to grab 'Application.version' too early, it's probably broken now."); + Logging.Logger.Default.Error($"Tried to grab 'Application.version' too early, it's probably broken now."); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logging.Logger.log.Error(ex); + Logging.Logger.Default.Error(ex); } catch (Exception ex) { - Logging.Logger.log.Error($"Error getting Application.version: {ex.Message}"); + Logging.Logger.Default.Error($"Error getting Application.version: {ex.Message}"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logging.Logger.log.Error(ex); + Logging.Logger.Default.Error(ex); } return string.Empty; } @@ -60,40 +63,46 @@ namespace IPA.Utilities var rtVer = new AlmostVersion(ApplicationVersionProxy); if (!rtVer.Equals(_gameVersion)) // this actually uses stricter equality than == for AlmostVersion { - Logging.Logger.log.Warn($"Early version {_gameVersion} parsed from game files doesn't match runtime version {rtVer}!"); + Logging.Logger.Default.Warn($"Early version {_gameVersion} parsed from game files doesn't match runtime version {rtVer}!"); _gameVersion = rtVer; } } catch (MissingMethodException e) { - Logging.Logger.log.Error("Application.version was not found! Cannot check early parsed version"); + Logging.Logger.Default.Error("Application.version was not found! Cannot check early parsed version"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logging.Logger.log.Error(e); + Logging.Logger.Default.Error(e); var st = new StackTrace(); - Logging.Logger.log.Notice($"{st}"); + Logging.Logger.Default.Notice($"{st}"); } } internal static bool IsGameVersionBoundary { get; private set; } - internal static AlmostVersion OldVersion { get; private set; } + internal static AlmostVersion? OldVersion { get; private set; } internal static void CheckGameVersionBoundary() { var gameVer = GameVersion; var lastVerS = SelfConfig.LastGameVersion_; OldVersion = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null; - IsGameVersionBoundary = OldVersion != null && gameVer != OldVersion; + IsGameVersionBoundary = OldVersion is not null && gameVer != OldVersion; SelfConfig.Instance.LastGameVersion = gameVer.ToString(); } - private static Thread mainThread; + private static Thread? mainThread; /// /// Checks if the currently running code is running on the Unity main thread. /// /// if the curent thread is the Unity main thread, otherwise - public static bool OnMainThread => Thread.CurrentThread.ManagedThreadId == mainThread?.ManagedThreadId; + public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId; + + /// + /// Asynchronously switches the current execution context to the Unity main thread. + /// + /// An awaitable which causes any following code to execute on the main thread. + public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default; internal static void SetMainThread() => mainThread = Thread.CurrentThread; @@ -120,9 +129,9 @@ namespace IPA.Utilities /// This only gives a /// /// the type of release this is - public static Release ReleaseType => (_releaseCache ?? (_releaseCache = CheckIsSteam() ? Release.Steam : Release.Other)).Value; + public static Release ReleaseType => _releaseCache ??= CheckIsSteam() ? Release.Steam : Release.Other; - private static string _installRoot; + private static string? _installRoot; /// /// Gets the path to the game's install directory. /// @@ -165,4 +174,59 @@ namespace IPA.Utilities && installDirInfo.Parent?.Parent?.Name == "steamapps"; } } + + /// + /// An awaitable which, when awaited, switches the current context to the Unity main thread. + /// + /// + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "This type should never be compared.")] + public struct SwitchToUnityMainThreadAwaitable + { + /// + /// Gets the awaiter for this awaitable. + /// + /// The awaiter for this awaitable. + public SwitchToUnityMainThreadAwaiter GetAwaiter() => default; + } + + /// + /// An awaiter which, when awaited, switches the current context to the Unity main thread. + /// + /// + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "This type should never be compared.")] + public struct SwitchToUnityMainThreadAwaiter : INotifyCompletion, ICriticalNotifyCompletion + { + private static readonly ContextCallback InvokeAction = static o => ((Action)o!)(); + + /// + /// Gets whether or not this awaiter is completed. + /// + public bool IsCompleted => UnityGame.OnMainThread; + + /// + /// Gets the result of this awaiter. + /// + public void GetResult() { } + + /// + /// Registers a continuation to be called when this awaiter finishes. + /// + /// The continuation. + public void OnCompleted(Action continuation) + { + var ec = ExecutionContext.Capture(); + UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation)); + } + + /// + /// Registers a continuation to be called when this awaiter finishes, without capturing the execution context. + /// + /// The continuation. + public void UnsafeOnCompleted(Action continuation) + { + UnityMainThreadTaskScheduler.Default.QueueAction(continuation); + } + } } diff --git a/IPA.Loader/Utilities/Utils.cs b/IPA.Loader/Utilities/Utils.cs index 677051a8..75ae9ce2 100644 --- a/IPA.Loader/Utilities/Utils.cs +++ b/IPA.Loader/Utilities/Utils.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.IO; using System.Text; using System.Linq; @@ -6,6 +7,8 @@ using System.Collections.Generic; using Mono.Cecil; using System.Runtime.CompilerServices; using System.Threading; +using System.Diagnostics.CodeAnalysis; +using Version = Hive.Versioning.Version; #if NET3 using File = Net3_Proxy.File; #endif @@ -24,6 +27,8 @@ namespace IPA.Utilities /// the corresponding byte array public static byte[] StringToByteArray(string hex) { + if (hex is null) + throw new ArgumentNullException(nameof(hex)); int numberChars = hex.Length; byte[] bytes = new byte[numberChars / 2]; for (int i = 0; i < numberChars; i += 2) @@ -38,9 +43,11 @@ namespace IPA.Utilities /// the hex form of the array public static string ByteArrayToString(byte[] ba) { - StringBuilder hex = new StringBuilder(ba.Length * 2); + if (ba is null) + throw new ArgumentNullException(nameof(ba)); + var hex = new StringBuilder(ba.Length * 2); foreach (byte b in ba) - hex.AppendFormat("{0:x2}", b); + _ = hex.AppendFormat("{0:x2}", b); return hex.ToString(); } @@ -64,9 +71,9 @@ namespace IPA.Utilities byte* x1 = p1, x2 = p2; int l = a1.Length; for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8) - if (*((long*)x1) != *((long*)x2)) return false; - if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; } - if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; } + if (*(long*)x1 != *(long*)x2) return false; + if ((l & 4) != 0) { if (*(int*)x1 != *(int*)x2) return false; x1 += 4; x2 += 4; } + if ((l & 2) != 0) { if (*(short*)x1 != *(short*)x2) return false; x1 += 2; x2 += 2; } if ((l & 1) != 0) if (*x1 != *x2) return false; return true; } @@ -80,13 +87,18 @@ namespace IPA.Utilities /// a path to get from to public static string GetRelativePath(string file, string folder) { - Uri pathUri = new Uri(file); + if (file is null) + throw new ArgumentNullException(nameof(file)); + if (folder is null) + throw new ArgumentNullException(nameof(folder)); + + var pathUri = new Uri(file); // Folders must end in a slash - if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) + if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) { folder += Path.DirectorySeparatorChar; } - Uri folderUri = new Uri(folder); + var folderUri = new Uri(folder); return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); } @@ -98,9 +110,14 @@ namespace IPA.Utilities /// the filename of the file to append together /// a delegate called when there is an error copying. Return true to keep going. public static void CopyAll(DirectoryInfo source, DirectoryInfo target, string appendFileName = "", - Func onCopyException = null) + Func? onCopyException = null) { - if (source.FullName.ToLower() == target.FullName.ToLower()) + if (source is null) + throw new ArgumentNullException(nameof(source)); + if (target is null) + throw new ArgumentNullException(nameof(target)); + + if (source.FullName.ToUpperInvariant() == target.FullName.ToUpperInvariant()) { return; } @@ -108,18 +125,18 @@ namespace IPA.Utilities // Check if the target directory exists, if not, create it. if (Directory.Exists(target.FullName) == false) { - Directory.CreateDirectory(target.FullName); + _ = Directory.CreateDirectory(target.FullName); } // Copy each file into it's new directory. - foreach (FileInfo fi in source.GetFiles()) + foreach (var fi in source.GetFiles()) { try { if (fi.Name == appendFileName) File.AppendAllLines(Path.Combine(target.ToString(), fi.Name), File.ReadAllLines(fi.FullName)); else - fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true); + _ = fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true); } catch (Exception e) { @@ -130,10 +147,9 @@ namespace IPA.Utilities } // Copy each subdirectory using recursion. - foreach (DirectoryInfo diSourceSubDir in source.GetDirectories()) + foreach (var diSourceSubDir in source.GetDirectories()) { - DirectoryInfo nextTargetSubDir = - target.CreateSubdirectory(diSourceSubDir.Name); + var nextTargetSubDir = target.CreateSubdirectory(diSourceSubDir.Name); CopyAll(diSourceSubDir, nextTargetSubDir, appendFileName, onCopyException); } } @@ -153,7 +169,7 @@ namespace IPA.Utilities { if (DateTimeSafetyUnknown) { - DateTime time = DateTime.MinValue; + var time = DateTime.MinValue; try { time = DateTime.Now; @@ -178,16 +194,30 @@ namespace IPA.Utilities /// the left value /// the right value /// < 0 if l is less than r, 0 if they are equal in the numeric portion, or > 0 if l is greater than r + [Obsolete("Use Hive.Versioning.Version overload instead.")] public static int VersionCompareNoPrerelease(SemVer.Version l, SemVer.Version r) + => VersionCompareNoPrerelease(l?.UnderlyingVersion!, r?.UnderlyingVersion!); + + /// + /// Compares a pair of s ignoring both the prerelease and build fields. + /// + /// the left value + /// the right value + /// < 0 if l is less than r, 0 if they are equal in the numeric portion, or > 0 if l is greater than r + public static int VersionCompareNoPrerelease(Version l, Version r) { - var cmpVal = l.Major - r.Major; + if (l is null) throw new ArgumentNullException(nameof(l)); + if (r is null) throw new ArgumentNullException(nameof(r)); + + var cmpVal = l.Major.CompareTo(r.Major); if (cmpVal != 0) return cmpVal; - cmpVal = l.Minor - r.Minor; + cmpVal = l.Minor.CompareTo(r.Minor); if (cmpVal != 0) return cmpVal; - cmpVal = l.Patch - r.Patch; + cmpVal = l.Patch.CompareTo(r.Patch); return cmpVal; } + /// /// An object used to manage scope guards. /// @@ -197,6 +227,10 @@ namespace IPA.Utilities /// /// /// + [SuppressMessage("Design", "CA1034:Nested types should not be visible", + Justification = "This type needs to be public to avoid allocations")] + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "This type is never supposed to be compared")] public struct ScopeGuardObject : IDisposable { private readonly Action action; @@ -221,9 +255,20 @@ namespace IPA.Utilities /// /// public static ScopeGuardObject ScopeGuard(Action action) - => new ScopeGuardObject(action); + => new(action); + + /// + /// Deconstructs a as its key and value. + /// + /// The type of the key. + /// The type of the value. + /// The to deconstruct. + /// The key in . + /// The value in . + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + => (key, value) = (kvp.Key, kvp.Value); - internal static bool HasInterface(this TypeDefinition type, string interfaceFullName) + internal static bool HasInterface(this TypeDefinition? type, string interfaceFullName) { return (type?.Interfaces?.Any(i => i.InterfaceType.FullName == interfaceFullName) ?? false) || (type?.Interfaces?.Any(t => HasInterface(t?.InterfaceType?.Resolve(), interfaceFullName)) ?? false); diff --git a/IPA/IPA.csproj b/IPA/IPA.csproj index 6e178844..66ec625a 100644 --- a/IPA/IPA.csproj +++ b/IPA/IPA.csproj @@ -4,7 +4,7 @@ Exe - net461;netcoreapp3.1 + net472;netcoreapp3.1 win7-x64;win7-x86;linux-x64 IPA IPA diff --git a/IPA/Program.cs b/IPA/Program.cs index c33365c4..71e55c2e 100644 --- a/IPA/Program.cs +++ b/IPA/Program.cs @@ -21,7 +21,7 @@ namespace IPA Unknown } - public const string FileVersion = "4.1.7.0"; + public const string FileVersion = "4.2.2.0"; public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!; diff --git a/IPA/_Attributes.cs b/IPA/_Attributes.cs deleted file mode 100644 index 72f7b779..00000000 --- a/IPA/_Attributes.cs +++ /dev/null @@ -1,10 +0,0 @@ -#if NET461 -namespace System.Diagnostics.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public sealed class DoesNotReturnAttribute : Attribute - { - public DoesNotReturnAttribute() { } - } -} -#endif \ No newline at end of file diff --git a/Libs/netstandard.dll b/Libs/netstandard.dll new file mode 100644 index 00000000..9350e84c Binary files /dev/null and b/Libs/netstandard.dll differ diff --git a/README.md b/README.md index 21998ae1..ab2c1c53 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# ![BSIPA](docs/images/banner_dark.svg) [![Build](https://github.com/bsmg/BeatSaber-IPA-Reloaded/workflows/Build/badge.svg)](https://github.com/bsmg/BeatSaber-IPA-Reloaded) +# ![BSIPA](docs/images/banner_dark.svg) [![Build](https://github.com/nike4613/BeatSaber-IPA-Reloaded/workflows/Build/badge.svg)](https://github.com/nike4613/BeatSaber-IPA-Reloaded) -[ALL DOCUMENTATION HAS MOVED HERE](https://bsmg.github.io/BeatSaber-IPA-Reloaded/) +[ALL DOCUMENTATION HAS MOVED HERE](https://nike4613.github.io/BeatSaber-IPA-Reloaded/) diff --git a/Refs/UnityEngine.CoreModule.Net4.dll b/Refs/UnityEngine.CoreModule.Net4.dll index 1effedfc..acb52874 100644 Binary files a/Refs/UnityEngine.CoreModule.Net4.dll and b/Refs/UnityEngine.CoreModule.Net4.dll differ diff --git a/Refs/UnityEngine.CoreModule.net3.dll b/Refs/UnityEngine.CoreModule.net3.dll deleted file mode 100644 index 477584ab..00000000 Binary files a/Refs/UnityEngine.CoreModule.net3.dll and /dev/null differ diff --git a/Refs/UnityEngine.Net4.dll b/Refs/UnityEngine.Net4.dll index 071390ac..f0ec9628 100644 Binary files a/Refs/UnityEngine.Net4.dll and b/Refs/UnityEngine.Net4.dll differ diff --git a/Refs/UnityEngine.UnityWebRequestModule.Net4.dll b/Refs/UnityEngine.UnityWebRequestModule.Net4.dll index 6a7c60cd..7c1820bd 100644 Binary files a/Refs/UnityEngine.UnityWebRequestModule.Net4.dll and b/Refs/UnityEngine.UnityWebRequestModule.Net4.dll differ diff --git a/Refs/UnityEngine.UnityWebRequestModule.net3.dll b/Refs/UnityEngine.UnityWebRequestModule.net3.dll deleted file mode 100644 index bcf496ea..00000000 Binary files a/Refs/UnityEngine.UnityWebRequestModule.net3.dll and /dev/null differ diff --git a/Refs/UnityEngine.net3.dll b/Refs/UnityEngine.net3.dll deleted file mode 100644 index bec8c389..00000000 Binary files a/Refs/UnityEngine.net3.dll and /dev/null differ diff --git a/SemVer/Range.cs b/SemVer/Range.cs new file mode 100644 index 00000000..1f7a6f32 --- /dev/null +++ b/SemVer/Range.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Hive.Versioning; +using HVersion = Hive.Versioning.Version; + +namespace SemVer +{ + [Obsolete("Use Hive.Versioning.VersionRange instead.")] + public class Range : IEquatable, IEquatable + { + public VersionRange UnderlyingRange { get; } + + private Range(VersionRange real) => UnderlyingRange = real; + + public Range(string rangeSpec, bool loose = false) : this(new(rangeSpec)) + => _ = loose; // loose is ignored because Hive doesn't have an equivalent + + public static Range ForHiveRange(VersionRange real) => new(real); + + public bool IsSatisfied(Version version) => IsSatisfied(version.UnderlyingVersion); + public bool IsSatisfied(HVersion version) => UnderlyingRange.Matches(version); + public bool IsSatisfied(string versionString, bool loose = false) => IsSatisfied(new Version(versionString, loose)); + + public IEnumerable Satisfying(IEnumerable versions) => versions.Where(IsSatisfied); + public IEnumerable Satisfying(IEnumerable versions, bool loose = false) + => versions.Where(v => IsSatisfied(v, loose)); + public Version? MaxSatisfying(IEnumerable versions) => Satisfying(versions).Max(); + public string? MaxSatisfying(IEnumerable versionStrings, bool loose = false) + => MaxSatisfying(ValidVersions(versionStrings, loose))?.ToString(); + public Range Intersect(Range other) => new(UnderlyingRange & other.UnderlyingRange); // the conjunction is the intersection + public override string ToString() => UnderlyingRange.ToString(); + + public bool Equals(Range? other) => UnderlyingRange.Equals(other?.UnderlyingRange); + public bool Equals(VersionRange? other) => UnderlyingRange.Equals(other); + public override bool Equals(object? obj) + => obj switch + { + Range r => Equals(r), + VersionRange vr => Equals(vr), + _ => false + }; + + public static bool operator ==(Range? a, Range? b) => a?.Equals(b) ?? b is null; + + public static bool operator !=(Range? a, Range? b) => !(a == b); + + public override int GetHashCode() => UnderlyingRange.GetHashCode(); + + public static bool IsSatisfied(string rangeSpec, string versionString, bool loose = false) + => new Range(rangeSpec, loose).IsSatisfied(versionString, loose); + public static IEnumerable Satisfying(string rangeSpec, IEnumerable versions, bool loose = false) + => new Range(rangeSpec, loose).Satisfying(versions, loose); + + public static string? MaxSatisfying(string rangeSpec, IEnumerable versions, bool loose = false) + => new Range(rangeSpec, loose).MaxSatisfying(versions, loose); + + private IEnumerable ValidVersions(IEnumerable versionStrings, bool loose) + { + foreach (string versionString in versionStrings) + { + Version? version = null; + try + { + version = new Version(versionString, loose); + } + catch (ArgumentException) + { + } + + if (version is not null) + { + yield return version; + } + } + } + } +} diff --git a/SemVer/SemVer.csproj b/SemVer/SemVer.csproj new file mode 100644 index 00000000..b25f6244 --- /dev/null +++ b/SemVer/SemVer.csproj @@ -0,0 +1,14 @@ + + + + + + net461 + enable + + + + + + + diff --git a/SemVer/Version.cs b/SemVer/Version.cs new file mode 100644 index 00000000..6db9b027 --- /dev/null +++ b/SemVer/Version.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using HVersion = Hive.Versioning.Version; + +namespace SemVer +{ + [Obsolete("Use Hive.Versioning.Version instead.")] + public class Version : IComparable, IComparable, IComparable, IEquatable, IEquatable + { + public HVersion UnderlyingVersion { get; } + + private Version(HVersion real) => UnderlyingVersion = real; + + public static Version ForHiveVersion(HVersion real) => new(real); + + public Version(string input, bool loose = false) : this(new HVersion(input)) + => _ = loose; // specifically unused because Hive has no equivalent (by design) + public Version(int major, int minor, int patch, string? preRelease = null, string? build = null) + : this(new HVersion(major, minor, patch, + preRelease is null ? Enumerable.Empty() : preRelease.Split('.'), + build is null ? Enumerable.Empty() : build.Split('.'))) + { + } + + public int Major => (int)UnderlyingVersion.Major; + public int Minor => (int)UnderlyingVersion.Minor; + public int Patch => (int)UnderlyingVersion.Patch; + public string PreRelease => string.Join(".", UnderlyingVersion.PreReleaseIds); + public string Build => string.Join(".", UnderlyingVersion.BuildIds); + + public Version BaseVersion() => new(new(UnderlyingVersion.Major, UnderlyingVersion.Minor, UnderlyingVersion.Patch)); + public override string ToString() => UnderlyingVersion.ToString(); + public string Clean() => ToString(); // normally this is the other way around kek + public override int GetHashCode() => UnderlyingVersion.GetHashCode(); + public bool Equals(Version? other) => UnderlyingVersion.Equals(other?.UnderlyingVersion); + public bool Equals(HVersion? other) => UnderlyingVersion.Equals(other); + public override bool Equals(object? obj) + => obj switch + { + Version v => Equals(v), + HVersion h => Equals(h), + _ => false + }; + + public int CompareTo(Version? other) => UnderlyingVersion.CompareTo(other?.UnderlyingVersion); + public int CompareTo(HVersion? other) => UnderlyingVersion.CompareTo(other); + public int CompareTo(object? obj) + => obj switch + { + null => 1, + Version v => CompareTo(v), + HVersion h => CompareTo(h), + _ => throw new ArgumentException("Object is not a Version") + }; + + public static bool operator ==(Version? a, Version? b) + => a?.UnderlyingVersion == b?.UnderlyingVersion; + public static bool operator !=(Version? a, Version? b) + => a?.UnderlyingVersion != b?.UnderlyingVersion; + + public static bool operator >(Version? a, Version? b) + => a is null ? b is not null && b.CompareTo(a) < 0 : a.CompareTo(b) > 0; + public static bool operator >=(Version? a, Version? b) + => !(a < b); + public static bool operator <(Version? a, Version? b) + => a is null ? b is not null && b.CompareTo(a) > 0 : a.CompareTo(b) < 0; + public static bool operator <=(Version? a, Version? b) + => !(a > b); + } +} diff --git a/docs/articles/start-user.md b/docs/articles/start-user.md index 0380c8f6..41656173 100644 --- a/docs/articles/start-user.md +++ b/docs/articles/start-user.md @@ -77,11 +77,14 @@ uid: articles.start.user Many plugins will come in a zip such that the root of the zip represents the game install directory, so all you may have to do is extract the plugin into the game installation folder. -> [!NOTE] +> [!NOTE] For Linux users +> +> By default, WINE loads DLLs differently to Windows, causing issues with the injection. To make BSIPA's injection work +> with Wine, `winhttp` has to have a DLL override set to `native,builtin`. This is best achieved by putting +> `WINEDLLOVERRIDES="winhttp=native,builtin" %command%` in Beat Saber's launch options in Steam. > -> For some reason, by default, Wine does not load DLLs in quite the same way that Windows does, causing issues with the injection. -> To make the injection work with Wine, `winhttp` has to have a DLL override set to `native,builtin`. This can be set either through -> Protontricks, or with the following `.reg` file. +> Alternatively, this can be set either through Protontricks, or using the following +> `.reg` file: > > ```reg > REGEDIT4 @@ -89,8 +92,9 @@ uid: articles.start.user > "winhttp"="native,builtin" > ``` > -> For Steam there's a per-game Wine prefix under `compatdata`. In this case `SteamLibrary/steamapps/compatdata/620980/pfx/user.reg`. -> Changes to this file will likely be ovewritten when the game updates or if local files are validated through Steam. +> For Steam, each game's Wine prefix is located under `compatdata`; in Beat Saber's case `SteamLibrary/steamapps/compatdata/620980/pfx/user.reg`. +> Changes to this file might be ovewritten when the game or Proton are updated however, so the launch options method is +> recommended. Thats really all you have to do! The installation should persist across game updates for as long as `winhttp.dll` is present in the game directory, though your plugins will be moved to a different folder when it does update so things don't break horribly. diff --git a/docs/build.ps1 b/docs/build.ps1 index 903a0a29..a2a0751d 100644 --- a/docs/build.ps1 +++ b/docs/build.ps1 @@ -1,22 +1,44 @@ +if ($PSEdition -eq "Core") { + Write-Error "Build must be run with Windows PowerShell due to the use of CodeDOM" + Write-Output "Running with Windows PowerShell" + powershell.exe "$(Get-Location)\build.ps1" + return +} + # read SelfConfig, remove wierd bits, load it, load Newtonsoft, and turn it into a schema -$newtonsoftLoc = "$(Get-Location)/nuget/Newtonsoft.Json.12.0.2/lib/netstandard2.0/Newtonsoft.Json.dll" -$newtonsoftSchemaLoc = "$(Get-Location)/nuget/Newtonsoft.Json.Schema.3.0.11/lib/netstandard2.0/Newtonsoft.Json.Schema.dll" -$roslynCodeDomBase = "$(Get-Location)/nuget/Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1" + +$newtonsoftVer = "13.0.1" +$newtonsoftSchemaVer = "3.0.15-beta2" +$codeDomProviderVer = "3.6.0" +$roslynVer = "3.10.0" +$nugetBase = "$(Get-Location)/nuget" +$newtonsoftLoc = "$nugetBase/Newtonsoft.Json.$newtonsoftVer/lib/netstandard2.0/Newtonsoft.Json.dll" +$newtonsoftSchemaLoc = "$nugetBase/Newtonsoft.Json.Schema.$newtonsoftSchemaVer/lib/netstandard2.0/Newtonsoft.Json.Schema.dll" +$roslynCodeDomBase = "$nugetBase/Microsoft.CodeDom.Providers.DotNetCompilerPlatform.$codeDomProviderVer" $roslynCodeDom = "$roslynCodeDomBase/lib/net45/Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll" +$roslynBase = "$nugetBase/Microsoft.Net.Compilers.Toolset.$roslynVer" +$roslynInstall = "$roslynBase/tasks/net472/" $selfConfigLoc = "../IPA.Loader/Config/SelfConfig.cs" $ipaRoot = "../IPA" if (!(Test-Path "nuget" -PathType Container)) { - nuget install Newtonsoft.Json -Version 12.0.2 -source https://api.nuget.org/v3/index.json -o "$(Get-Location)/nuget" - nuget install Newtonsoft.Json.Schema -Version 3.0.11 -source https://api.nuget.org/v3/index.json -o "$(Get-Location)/nuget" - nuget install Microsoft.CodeDom.Providers.DotNetCompilerPlatform -Version 2.0.1 -source https://api.nuget.org/v3/index.json -o "$(Get-Location)/nuget" + $nugetExe = "nuget/nuget.exe" + mkdir "nuget" + Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $nugetExe + + &$nugetExe install Newtonsoft.Json -Version $newtonsoftVer -source https://api.nuget.org/v3/index.json -o $nugetBase + &$nugetExe install Newtonsoft.Json.Schema -Version $newtonsoftSchemaVer -source https://api.nuget.org/v3/index.json -o $nugetBase + &$nugetExe install Microsoft.CodeDom.Providers.DotNetCompilerPlatform -Version $codeDomProviderVer -source https://api.nuget.org/v3/index.json -o $nugetBase + &$nugetExe install Microsoft.Net.Compilers.Toolset -Version $roslynVer -source https://api.nuget.org/v3/index.json -o $nugetBase } & docfx metadata -if ((Test-Path $newtonsoftLoc -PathType Leaf) -and (Test-Path $selfConfigLoc -PathType Leaf) -and (Test-Path $roslynCodeDom -PathType Leaf)) { +if ((Test-Path $roslynCodeDom -PathType Leaf) -and (Test-Path $roslynInstall -PathType Container)) { # The files we need exist, lets do this! + Write-Output "Generating Schema JSON" + # Add the Roslyn CodeDom Add-Type -Path $roslynCodeDom @@ -68,7 +90,7 @@ class RoslynCompilerSettings : Microsoft.CodeDom.Providers.DotNetCompilerPlatfor { [string] get_CompilerFullPath() { - return "$roslynCodeDomBase\tools\RoslynLatest\csc.exe" + return "$roslynInstall\csc.exe" } [int] get_CompilerServerTimeToLive() { @@ -88,11 +110,14 @@ class RoslynCompilerSettings : Microsoft.CodeDom.Providers.DotNetCompilerPlatfor $schema = $schemagen.Generate([IPA.Config.SelfConfig]) $schema.ToString() | Out-File "other_api/config/_schema.json" +} else { + Write-Output "Cannot generate schema JSON" } -$ipaExe = "$ipaRoot/bin/Release/net461/IPA.exe" +$ipaExe = "$ipaRoot/bin/Release/net472/IPA.exe" # generate IPA.exe args file if (-not (Test-Path $ipaExe -PathType Leaf)) { + msbuild -t:Restore -p:Configuration=Release -p:Platform=AnyCPU -p:SolutionDir=.. "$ipaRoot/IPA.csproj" msbuild -p:Configuration=Release -p:Platform=AnyCPU -p:SolutionDir=.. "$ipaRoot/IPA.csproj" } diff --git a/docs/docfx.json b/docs/docfx.json index 30623cd4..96426a9a 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -18,7 +18,10 @@ }], "dest": "api", "disableGitFeatures": false, - "disableDefaultFilter": false + "disableDefaultFilter": false, + "properties": { + "TargetFramework": "net472" + } }], "build": { "content": [{ @@ -36,6 +39,9 @@ "other_api/**.yml", "toc.yml", "*.md" + ], + "exclude": [ + "articles/dev-resources/**.md" ] } ],