Browse Source

Merge branch 'master' into virtualize-editor

pull/96/head
nike4613 11 months ago
committed by GitHub
parent
commit
1909332332
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 3772 additions and 2214 deletions
  1. +246
    -0
      .editorconfig
  2. +11
    -9
      .github/workflows/build.yml
  3. +6
    -1
      .github/workflows/docs.yml
  4. +7
    -2
      .github/workflows/tag_docs.yml
  5. +12
    -0
      .vscode/launch.json
  6. +5
    -2
      BSIPA-Meta/BSIPA-Meta.csproj
  7. +21
    -57
      BSIPA.sln
  8. +1
    -1
      BuildTools
  9. +12
    -0
      Common.props
  10. +0
    -4
      Common.targets
  11. +13
    -4
      Doorstop/Proxy/main.c
  12. +2
    -0
      Doorstop/Proxy/mono.h
  13. +4
    -3
      IPA.Injector/GameVersionEarly.cs
  14. +38
    -17
      IPA.Injector/IPA.Injector.csproj
  15. +38
    -44
      IPA.Injector/Injector.cs
  16. +67
    -67
      IPA.Injector/PermissionFix.cs
  17. +38
    -36
      IPA.Injector/Properties/AssemblyInfo.cs
  18. +31
    -15
      IPA.Injector/Updates.cs
  19. +14
    -0
      IPA.Loader/AntiMalware/AmsiConstants.cs
  20. +13
    -0
      IPA.Loader/AntiMalware/AmsiResult.cs
  21. +46
    -0
      IPA.Loader/AntiMalware/AntiMalwareEngine.cs
  22. +25
    -0
      IPA.Loader/AntiMalware/IAntiMalware.cs
  23. +15
    -0
      IPA.Loader/AntiMalware/NoopAntiMalware.cs
  24. +27
    -0
      IPA.Loader/AntiMalware/ScanResult.cs
  25. +111
    -0
      IPA.Loader/AntiMalware/WindowsWin32AntiMalware.cs
  26. +113
    -0
      IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiFileStream.cs
  27. +114
    -0
      IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiMemoryStream.cs
  28. +52
    -0
      IPA.Loader/AntiMalware/_HideInNet3/ComAPI/IAntimalware.cs
  29. +88
    -0
      IPA.Loader/AntiMalware/_HideInNet3/WindowsCOMAntiMalware.cs
  30. +1
    -1
      IPA.Loader/Config/Config.cs
  31. +6
    -6
      IPA.Loader/Config/ConfigRuntime.cs
  32. +16
    -14
      IPA.Loader/Config/Data/List.cs
  33. +19
    -18
      IPA.Loader/Config/Data/Map.cs
  34. +83
    -7
      IPA.Loader/Config/Data/Primitives.cs
  35. +146
    -145
      IPA.Loader/Config/Data/Value.cs
  36. +9
    -9
      IPA.Loader/Config/Providers/JsonConfigProvider.cs
  37. +58
    -7
      IPA.Loader/Config/SelfConfig.cs
  38. +15
    -5
      IPA.Loader/Config/Stores/Attributes.cs
  39. +17
    -16
      IPA.Loader/Config/Stores/CollectionConverter.cs
  40. +76
    -75
      IPA.Loader/Config/Stores/Converters.cs
  41. +17
    -14
      IPA.Loader/Config/Stores/CustomObjectConverter.cs
  42. +5
    -4
      IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs
  43. +2
    -1
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs
  44. +8
    -3
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs
  45. +15
    -11
      IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs
  46. +25
    -24
      IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs
  47. +9
    -8
      IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs
  48. +25
    -20
      IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs
  49. +16
    -4
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs
  50. +21
    -18
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs
  51. +2
    -1
      IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs
  52. +8
    -7
      IPA.Loader/Config/Stores/ValueConverter.cs
  53. +22
    -10
      IPA.Loader/IPA.Loader.csproj
  54. +30
    -8
      IPA.Loader/JsonConverters/FeaturesFieldConverter.cs
  55. +20
    -15
      IPA.Loader/JsonConverters/SemverRangeConverter.cs
  56. +19
    -17
      IPA.Loader/JsonConverters/SemverVersionConverter.cs
  57. +1
    -1
      IPA.Loader/Loader/Composite/CompositeBSPlugin.cs
  58. +1
    -1
      IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs
  59. +21
    -0
      IPA.Loader/Loader/DependencyResolutionLoopException.cs
  60. +3
    -3
      IPA.Loader/Loader/DisabledConfig.cs
  61. +4
    -3
      IPA.Loader/Loader/Features/ConfigProviderFeature.cs
  62. +13
    -12
      IPA.Loader/Loader/Features/DefineFeature.cs
  63. +0
    -1
      IPA.Loader/Loader/HarmonyProtector.cs
  64. +233
    -244
      IPA.Loader/Loader/LibLoader.cs
  65. +9
    -5
      IPA.Loader/Loader/PluginExecutor.cs
  66. +120
    -51
      IPA.Loader/Loader/PluginInitInjector.cs
  67. +224
    -374
      IPA.Loader/Loader/PluginLoader.cs
  68. +25
    -27
      IPA.Loader/Loader/PluginManager.cs
  69. +7
    -6
      IPA.Loader/Loader/PluginManifest.cs
  70. +11
    -3
      IPA.Loader/Loader/PluginMetadata.cs
  71. +24
    -24
      IPA.Loader/Loader/StateTransitionTransaction.cs
  72. +1
    -5
      IPA.Loader/Loader/description.md
  73. +9
    -7
      IPA.Loader/Loader/manifest.json
  74. +39
    -38
      IPA.Loader/Logging/LogPrinter.cs
  75. +15
    -12
      IPA.Loader/Logging/Logger.cs
  76. +159
    -116
      IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs
  77. +169
    -157
      IPA.Loader/Logging/Printers/GZFilePrinter.cs
  78. +8
    -5
      IPA.Loader/Logging/StandardLogger.cs
  79. +51
    -21
      IPA.Loader/Logging/StdoutInterceptor.cs
  80. +16
    -0
      IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs
  81. +3
    -1
      IPA.Loader/Properties/AssemblyInfo.cs
  82. +323
    -270
      IPA.Loader/Utilities/AlmostVersion.cs
  83. +4
    -4
      IPA.Loader/Utilities/Async/Coroutines.cs
  84. +28
    -10
      IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
  85. +9
    -12
      IPA.Loader/Utilities/CriticalSection.cs
  86. +2
    -0
      IPA.Loader/Utilities/EnumerableExtensions.cs
  87. +13
    -0
      IPA.Loader/Utilities/ReflectionUtil.cs
  88. +82
    -18
      IPA.Loader/Utilities/UnityGame.cs
  89. +68
    -23
      IPA.Loader/Utilities/Utils.cs
  90. +1
    -1
      IPA/IPA.csproj
  91. +1
    -1
      IPA/Program.cs
  92. +0
    -10
      IPA/_Attributes.cs
  93. BIN
      Libs/netstandard.dll
  94. +2
    -2
      README.md
  95. BIN
      Refs/UnityEngine.CoreModule.Net4.dll
  96. BIN
      Refs/UnityEngine.CoreModule.net3.dll
  97. BIN
      Refs/UnityEngine.Net4.dll
  98. BIN
      Refs/UnityEngine.UnityWebRequestModule.Net4.dll
  99. BIN
      Refs/UnityEngine.UnityWebRequestModule.net3.dll
  100. BIN
      Refs/UnityEngine.net3.dll

+ 246
- 0
.editorconfig View File

@ -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

+ 11
- 9
.github/workflows/build.yml View File

@ -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/

+ 6
- 1
.github/workflows/docs.yml View File

@ -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


+ 7
- 2
.github/workflows/tag_docs.yml View File

@ -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:


+ 12
- 0
.vscode/launch.json View File

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to BSIPA",
"type": "mono",
"request": "attach",
"address": "localhost",
"port": 10000
}
]
}

+ 5
- 2
BSIPA-Meta/BSIPA-Meta.csproj View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="Build">
<PropertyGroup>
<TargetFrameworks>net35;net461</TargetFrameworks>
<TargetFrameworks>net472</TargetFrameworks>
<Platforms>x86;x64</Platforms>
<Configurations>Debug;Release;Verbose;Verbose_Release</Configurations>
@ -59,9 +59,11 @@
</ItemGroup>
<Delete Files="@(Files)" />
</Target>
<Target Name="CoreCompile">
<CallTarget Targets="Assemble" />
</Target>
<Target Name="Assemble" Returns="$(OutputPath)">
<Message Text="Packing..." Importance="High" />
<Message Text="Results will be put in $(OutputPath)" Importance="High" />
@ -69,7 +71,7 @@
<InputDlls Include="$(SolutionDir)IPA.Injector\bin\$(NoVerboseConfig)\$(TargetFramework)\**\*" Exclude="$(SolutionDir)IPA.Injector\bin\$(NoVerboseConfig)\$(TargetFramework)\Libraries\**\*" />
<InputDlls Include="$(SolutionDir)Doorstop\Proxy\bin\$(ProxyPlatform)\$(ProxyConfig)\**\*.dll" />
<InputDlls Include="$(SolutionDir)Doorstop\Proxy\bin\$(ProxyPlatform)\$(ProxyConfig)\**\*.pdb" />
<InputIPAFiles Include="$(SolutionDir)IPA\bin\$(Configuration)\net461\**\*" Exclude="$(SolutionDir)IPA\bin\$(Configuration)\net461\*.runtimeconfig.*" />
<InputIPAFiles Include="$(SolutionDir)IPA\bin\$(Configuration)\net472\**\*" Exclude="$(SolutionDir)IPA\bin\$(Configuration)\net472\*.runtimeconfig.*" />
<InputIPAFiles Include="$(SolutionDir)IPA\bin\$(Configuration)\netcoreapp3.1\IPA.runtimeconfig.json" />
</ItemGroup>
<Copy SourceFiles="@(InputDlls)" DestinationFolder="$(OutputPath)IPA\%(RecursiveDir)" />
@ -87,3 +89,4 @@
</Target>
</Project>

+ 21
- 57
BSIPA.sln View File

@ -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


+ 1
- 1
BuildTools

@ -1 +1 @@
Subproject commit 41c3a12d56de96a3495893d1fea4a485a98c67af
Subproject commit b04769a3aebdd111b81f5a59d438907310e83207

+ 12
- 0
Common.props View File

@ -11,5 +11,17 @@
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" />
<PackageReference Include="Nullable" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="IsExternalInit" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

+ 0
- 4
Common.targets View File

@ -1,8 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)\System.Diagnostics.CodeAnalysis.cs"/>
</ItemGroup>
</Project>

+ 13
- 4
Doorstop/Proxy/main.c View File

@ -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


+ 2
- 0
Doorstop/Proxy/mono.h View File

@ -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);


+ 4
- 3
IPA.Injector/GameVersionEarly.cs View File

@ -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()
{


+ 38
- 17
IPA.Injector/IPA.Injector.csproj View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Common.props"/>
<Import Project="..\Common.props" />
<PropertyGroup>
<TargetFrameworks>net461;net35</TargetFrameworks>
<TargetFramework>net472</TargetFramework>
<RootNamespace>IPA.Injector</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net461' ">true</BuildForBeatSaber>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net472' ">true</BuildForBeatSaber>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
<DefineConstants>$(DefineConstants);NET4</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net35'">
@ -21,14 +21,14 @@
<PropertyGroup Condition="'$(BuildForBeatSaber)' == 'true'">
<DefineConstants>$(DefineConstants);BeatSaber</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj" />
<ProjectReference Include="..\Net3-Proxy\Net3-Proxy.csproj" Condition=" '$(TargetFramework)' == 'net35' " />
</ItemGroup>
<ItemGroup>
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net461' ">
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net472' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net4.dll</HintPath>
<Private>False</Private>
</Reference>
@ -37,8 +37,8 @@
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Content Include="..\Libs\I18N.Net4.dll">
<Link>Libraries\Mono\I18N.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@ -55,7 +55,19 @@
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\netstandard.dll">
<Link>Libraries\Mono\netstandard.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="..\Libs\thirdparty\*">
<Link>Libraries\Thirdparty\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
<Content Include="..\Libs\I18N.Net3.dll">
<Link>Libraries\Mono\I18N.dll</Link>
@ -70,13 +82,22 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.10.4" />
<PackageReference Include="SemanticVersioning" Version="1.2.2" />
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="AsyncBridge" Version="0.3.1" />
<ProjectReference Include="..\SemVer\SemVer.csproj" />
</ItemGroup>
<Target Name="CopyDocumentation" BeforeTargets="Build">
<ItemGroup>
<ReferenceFiles Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
</ItemGroup>
<Message Text="Copying documentation" />
<Copy SourceFiles="@(ReferenceFiles)" DestinationFolder="$(OutputPath)Libs" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Message Text="Relocating" Importance="normal" />
<ItemGroup>
@ -91,7 +112,7 @@
</ItemGroup>
<Move SourceFiles="@(SystemFiles)" DestinationFolder="$(OutputPath)Data\Managed" />
<RemoveDir Directories="$(OutputPath)Libraries\Mono" />
<Delete Files="@(OldLibFiles)" />
<RemoveDir Directories="$(OutputPath)Libs" />
<ItemGroup>
@ -102,6 +123,6 @@
<RemoveDir Directories="$(OutputPath)Libraries" />
</Target>
<Import Project="..\Common.targets"/>
<Import Project="..\Common.targets" />
</Project>

+ 38
- 44
IPA.Injector/Injector.cs View File

@ -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();
}
}
}

+ 67
- 67
IPA.Injector/PermissionFix.cs View File

@ -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}");
});
}
}
}

+ 38
- 36
IPA.Injector/Properties/AssemblyInfo.cs View File

@ -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)]

+ 31
- 15
IPA.Injector/Updates.cs View File

@ -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<string>();
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);
}
}
}


+ 14
- 0
IPA.Loader/AntiMalware/AmsiConstants.cs View File

@ -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");
}
}

+ 13
- 0
IPA.Loader/AntiMalware/AmsiResult.cs View File

@ -0,0 +1,13 @@
#nullable enable
namespace IPA.AntiMalware
{
internal enum AmsiResult
{
Clean = 0,
NotDetected = 1,
BlockedByAdminStart = 0x4000,
BlockedByAdminEnd = 0x4fff,
Detected = 32768
}
}

+ 46
- 0
IPA.Loader/AntiMalware/AntiMalwareEngine.cs View File

@ -0,0 +1,46 @@
#nullable enable
using IPA.Config;
using IPA.Logging;
using System;
namespace IPA.AntiMalware
{
/// <summary>
/// Provides a way to access BSIPA's Anti-Malware engine.
/// </summary>
/// <see cref="Engine"/>
/// <see cref="IAntiMalware"/>
public static class AntiMalwareEngine
{
private static IAntiMalware? engine;
/// <summary>
/// Gets the current Anti-Malware engine.
/// </summary>
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;
}
}
}

+ 25
- 0
IPA.Loader/AntiMalware/IAntiMalware.cs View File

@ -0,0 +1,25 @@
#nullable enable
using System.IO;
namespace IPA.AntiMalware
{
/// <summary>
/// An Anti-Malware engine that can be used to scan and detect potentially harmful files.
/// </summary>
public interface IAntiMalware
{
/// <summary>
/// Scans a particular file for malware.
/// </summary>
/// <param name="file">The file to scan.</param>
/// <returns>A <see cref="ScanResult"/> indicating whether the file is safe or not.</returns>
ScanResult ScanFile(FileInfo file);
/// <summary>
/// Scans a particular in-memory blob for malware.
/// </summary>
/// <param name="data">The binary blob to scan.</param>
/// <param name="contentName">The name of the content. If this is left <see langword="null"/>, one will be automatically generated.</param>
/// <returns>A <see cref="ScanResult"/> indicating whether the file is safe or not.</returns>
ScanResult ScanData(byte[] data, string? contentName = null);
}
}

+ 15
- 0
IPA.Loader/AntiMalware/NoopAntiMalware.cs View File

@ -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;
}
}

+ 27
- 0
IPA.Loader/AntiMalware/ScanResult.cs View File

@ -0,0 +1,27 @@

namespace IPA.AntiMalware
{
/// <summary>
/// The result of an Anti-Malware scan.
/// </summary>
public enum ScanResult
{
/// <summary>
/// The object is known to be safe.
/// </summary>
KnownSafe,
/// <summary>
/// No malware was detected, but it is not known to be safe.
/// </summary>
NotDetected,
/// <summary>
/// Malware was detected, and the content should not be executed.
/// </summary>
Detected,
/// <summary>
/// The malware engine returned a threat level less than the max, so this object may be dangerous.
/// Proceed with caution.
/// </summary>
MaybeMalware
}
}

+ 111
- 0
IPA.Loader/AntiMalware/WindowsWin32AntiMalware.cs View File

@ -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);
}
}

+ 113
- 0
IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiFileStream.cs View File

@ -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);
}
}
}

+ 114
- 0
IPA.Loader/AntiMalware/_HideInNet3/ComAPI/AmsiMemoryStream.cs View File

@ -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);
}
}
}

+ 52
- 0
IPA.Loader/AntiMalware/_HideInNet3/ComAPI/IAntimalware.cs View File

@ -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,
}
}

+ 88
- 0
IPA.Loader/AntiMalware/_HideInNet3/WindowsCOMAntiMalware.cs View File

@ -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);
}
}
}

+ 1
- 1
IPA.Loader/Config/Config.cs View File

@ -81,7 +81,7 @@ namespace IPA.Config
/// Registers a <see cref="IConfigProvider"/> to use for configs.
/// </summary>
/// <typeparam name="T">the type to register</typeparam>
public static void Register<T>() where T : IConfigProvider => Register(typeof(T));
public static void Register<T>() where T : IConfigProvider, new() => Register(typeof(T));
/// <summary>
/// Registers a <see cref="IConfigProvider"/> to use for configs.


+ 6
- 6
IPA.Loader/Config/ConfigRuntime.cs View File

@ -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));
}


+ 16
- 14
IPA.Loader/Config/Data/List.cs View File

@ -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 <see cref="Value"/>s for serialization by an <see cref="IConfigProvider"/>.
/// Use <see cref="Value.List"/> or <see cref="Value.From(IEnumerable{Value})"/> to create.
/// </summary>
public sealed class List : Value, IList<Value>
public sealed class List : Value, IList<Value?>
{
private readonly List<Value> values = new List<Value>();
private readonly List<Value?> values = new();
internal List() { }
@ -22,7 +23,7 @@ namespace IPA.Config.Data
/// <param name="index">the index to retrieve the <see cref="Value"/> at</param>
/// <returns>the <see cref="Value"/> at <paramref name="index"/></returns>
/// <seealso cref="IList{T}.this[int]"/>
public Value this[int index] { get => values[index]; set => values[index] = value; }
public Value? this[int index] { get => values[index]; set => values[index] = value; }
/// <summary>
/// Gets the number of elements in the <see cref="List"/>.
@ -30,21 +31,22 @@ namespace IPA.Config.Data
/// <seealso cref="ICollection{T}.Count"/>
public int Count => values.Count;
bool ICollection<Value>.IsReadOnly => ((IList<Value>)values).IsReadOnly;
bool ICollection<Value?>.IsReadOnly => ((IList<Value?>)values).IsReadOnly;
/// <summary>
/// Adds a <see cref="Value"/> to the end of this <see cref="List"/>.
/// </summary>
/// <param name="item">the <see cref="Value"/> to add</param>
/// <seealso cref="ICollection{T}.Add(T)"/>
public void Add(Value item) => values.Add(item);
public void Add(Value? item) => values.Add(item);
/// <summary>
/// Adds a range of <see cref="Value"/>s to the end of this <see cref="List"/>.
/// </summary>
/// <param name="vals">the range of <see cref="Value"/>s to add</param>
public void AddRange(IEnumerable<Value> vals)
public void AddRange(IEnumerable<Value?> 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
/// <param name="item">the <see cref="Value"/> to check for</param>
/// <returns><see langword="true"/> if the item was founc, otherwise <see langword="false"/></returns>
/// <seealso cref="ICollection{T}.Contains(T)"/>
public bool Contains(Value item) => values.Contains(item);
public bool Contains(Value? item) => values.Contains(item);
/// <summary>
/// Copies the <see cref="Value"/>s in the <see cref="List"/> to the <see cref="Array"/> in <paramref name="array"/>.
@ -68,14 +70,14 @@ namespace IPA.Config.Data
/// <param name="array">the <see cref="Array"/> to copy to</param>
/// <param name="arrayIndex">the starting index to copy to</param>
/// <seealso cref="ICollection{T}.CopyTo(T[], int)"/>
public void CopyTo(Value[] array, int arrayIndex) => values.CopyTo(array, arrayIndex);
public void CopyTo(Value?[] array, int arrayIndex) => values.CopyTo(array, arrayIndex);
/// <summary>
/// Gets an enumerator to enumerate the <see cref="List"/>.
/// </summary>
/// <returns>an <see cref="IEnumerator{T}"/> for this <see cref="List"/></returns>
/// <seealso cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<Value> GetEnumerator() => ((IList<Value>)values).GetEnumerator();
public IEnumerator<Value?> GetEnumerator() => ((IList<Value?>)values).GetEnumerator();
/// <summary>
/// Gets the index that a given <see cref="Value"/> is in the <see cref="List"/>.
@ -83,7 +85,7 @@ namespace IPA.Config.Data
/// <param name="item">the <see cref="Value"/> to search for</param>
/// <returns>the index that the <paramref name="item"/> was at, or -1.</returns>
/// <seealso cref="IList{T}.IndexOf(T)"/>
public int IndexOf(Value item) => values.IndexOf(item);
public int IndexOf(Value? item) => values.IndexOf(item);
/// <summary>
/// Inserts a <see cref="Value"/> at an index.
@ -91,7 +93,7 @@ namespace IPA.Config.Data
/// <param name="index">the index to insert at</param>
/// <param name="item">the <see cref="Value"/> to insert</param>
/// <seealso cref="IList{T}.Insert(int, T)"/>
public void Insert(int index, Value item) => values.Insert(index, item);
public void Insert(int index, Value? item) => values.Insert(index, item);
/// <summary>
/// Removes a <see cref="Value"/> from the <see cref="List"/>.
@ -99,7 +101,7 @@ namespace IPA.Config.Data
/// <param name="item">the <see cref="Value"/> to remove</param>
/// <returns><see langword="true"/> if the item was removed, <see langword="false"/> otherwise</returns>
/// <seealso cref="ICollection{T}.Remove(T)"/>
public bool Remove(Value item) => values.Remove(item);
public bool Remove(Value? item) => values.Remove(item);
/// <summary>
/// Removes a <see cref="Value"/> 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<Value>)values).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


+ 19
- 18
IPA.Loader/Config/Data/Map.cs View File

@ -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 <see cref="string"/> to <see cref="Value"/> for serialization by an <see cref="IConfigProvider"/>.
/// Use <see cref="Value.Map"/> or <see cref="Value.From(IDictionary{string, Value})"/> to create.
/// </summary>
public sealed class Map : Value, IDictionary<string, Value>
public sealed class Map : Value, IDictionary<string, Value?>
{
private readonly Dictionary<string, Value> values = new Dictionary<string, Value>();
private readonly List<string> keyOrder = new List<string>();
private readonly Dictionary<string, Value?> values = new();
private readonly List<string> keyOrder = new();
internal Map() { }
@ -23,7 +24,7 @@ namespace IPA.Config.Data
/// <param name="key">the key to get the value associated with</param>
/// <returns>the value associated with the <paramref name="key"/></returns>
/// <seealso cref="IDictionary{TKey, TValue}.this[TKey]"/>
public Value this[string key] { get => values[key]; set => values[key] = value; }
public Value? this[string key] { get => values[key]; set => values[key] = value; }
/// <summary>
/// Gets a collection of the keys for the <see cref="Map"/>.
@ -39,7 +40,7 @@ namespace IPA.Config.Data
/// guarantee that order is maintained.
/// </remarks>
/// <seealso cref="IDictionary{TKey, TValue}.Values"/>
public ICollection<Value> Values => values.Values;
public ICollection<Value?> Values => values.Values;
/// <summary>
/// Gets the number of key-value pairs in this <see cref="Map"/>.
@ -47,7 +48,7 @@ namespace IPA.Config.Data
/// <seealso cref="ICollection{T}.Count"/>
public int Count => values.Count;
bool ICollection<KeyValuePair<string, Value>>.IsReadOnly => ((IDictionary<string, Value>)values).IsReadOnly;
bool ICollection<KeyValuePair<string, Value?>>.IsReadOnly => ((IDictionary<string, Value?>)values).IsReadOnly;
/// <summary>
/// Adds a new <see cref="Value"/> with a given key.
@ -55,13 +56,13 @@ namespace IPA.Config.Data
/// <param name="key">the key to put the value at</param>
/// <param name="value">the <see cref="Value"/> to add</param>
/// <seealso cref="IDictionary{TKey, TValue}.Add(TKey, TValue)"/>
public void Add(string key, Value value)
public void Add(string key, Value? value)
{
values.Add(key, value);
keyOrder.Add(key);
}
void ICollection<KeyValuePair<string, Value>>.Add(KeyValuePair<string, Value> item)
void ICollection<KeyValuePair<string, Value?>>.Add(KeyValuePair<string, Value?> item)
=> Add(item.Key, item.Value);
/// <summary>
@ -74,8 +75,8 @@ namespace IPA.Config.Data
keyOrder.Clear();
}
bool ICollection<KeyValuePair<string, Value>>.Contains(KeyValuePair<string, Value> item)
=> ((IDictionary<string, Value>)values).Contains(item);
bool ICollection<KeyValuePair<string, Value?>>.Contains(KeyValuePair<string, Value?> item)
=> ((IDictionary<string, Value?>)values).Contains(item);
/// <summary>
/// Checks if the <see cref="Map"/> contains a given <paramref name="key"/>.
@ -85,18 +86,18 @@ namespace IPA.Config.Data
/// <seealso cref="IDictionary{TKey, TValue}.ContainsKey(TKey)"/>
public bool ContainsKey(string key) => values.ContainsKey(key);
void ICollection<KeyValuePair<string, Value>>.CopyTo(KeyValuePair<string, Value>[] array, int arrayIndex)
=> ((IDictionary<string, Value>)values).CopyTo(array, arrayIndex);
void ICollection<KeyValuePair<string, Value?>>.CopyTo(KeyValuePair<string, Value?>[] array, int arrayIndex)
=> ((IDictionary<string, Value?>)values).CopyTo(array, arrayIndex);
/// <summary>
/// Enumerates the <see cref="Map"/>'s key-value pairs.
/// </summary>
/// <returns>an <see cref="IEnumerator{T}"/> of key-value pairs in this <see cref="Map"/></returns>
/// <seealso cref="IEnumerable{T}.GetEnumerator()"/>
public IEnumerator<KeyValuePair<string, Value>> GetEnumerator()
public IEnumerator<KeyValuePair<string, Value?>> GetEnumerator()
{
foreach (var key in keyOrder)
yield return new KeyValuePair<string, Value>(key, this[key]);
yield return new KeyValuePair<string, Value?>(key, this[key]);
}
/// <summary>
@ -107,8 +108,8 @@ namespace IPA.Config.Data
/// <seealso cref="IDictionary{TKey, TValue}.Remove(TKey)"/>
public bool Remove(string key) => values.Remove(key) && keyOrder.Remove(key);
bool ICollection<KeyValuePair<string, Value>>.Remove(KeyValuePair<string, Value> item)
=> ((IDictionary<string, Value>)values).Remove(item) && (keyOrder.Remove(item.Key) || true);
bool ICollection<KeyValuePair<string, Value?>>.Remove(KeyValuePair<string, Value?> item)
=> ((IDictionary<string, Value?>)values).Remove(item) && (keyOrder.Remove(item.Key) || true);
/// <summary>
/// Gets the value associated with the specified key.
@ -117,7 +118,7 @@ namespace IPA.Config.Data
/// <param name="value">the target location of the retrieved object</param>
/// <returns><see langword="true"/> if the key was found and <paramref name="value"/> set, <see langword="false"/> otherwise</returns>
/// <seealso cref="IDictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
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();


+ 83
- 7
IPA.Loader/Config/Data/Primitives.cs View File

@ -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
/// </summary>
public sealed class Text : Value
{
/// <summary>
/// Constructs an empty <see cref="Text"/> object.
/// </summary>
[Obsolete("Use the String constructor.")]
public Text()
{
Value = null!;
}
/// <summary>
/// Constructs a <see cref="Text"/> object containing the provided value.
/// </summary>
/// <param name="value">The value to construct with.</param>
public Text(string value)
{
Value = value;
}
/// <summary>
/// The actual value of this <see cref="Text"/> object.
/// </summary>
public string Value { get; set; }
public string Value { get; init; }
/// <summary>
/// Converts this <see cref="Data.Value"/> into a human-readable format.
@ -30,6 +49,24 @@ namespace IPA.Config.Data
/// </summary>
public sealed class Integer : Value
{
/// <summary>
/// Constructs an empty <see cref="Integer"/> object.
/// </summary>
[Obsolete("Use the long constructor.")]
public Integer()
{
Value = 0;
}
/// <summary>
/// Constructs a <see cref="Integer"/> object containing the provided value.
/// </summary>
/// <param name="value">The value to construct with.</param>
public Integer(long value)
{
Value = value;
}
/// <summary>
/// The actual value of the <see cref="Integer"/> object.
/// </summary>
@ -50,10 +87,28 @@ namespace IPA.Config.Data
/// <summary>
/// A <see cref="Value"/> representing a floating point value. This may hold a
/// <see cref="decimal"/>'s worth of data.
/// <see cref="decimal"/>'s worth of data.
/// </summary>
public sealed class FloatingPoint : Value
{
/// <summary>
/// Constructs an empty <see cref="FloatingPoint"/> object.
/// </summary>
[Obsolete("Use the long constructor.")]
public FloatingPoint()
{
Value = 0;
}
/// <summary>
/// Constructs a <see cref="FloatingPoint"/> object containing the provided value.
/// </summary>
/// <param name="value">The value to construct with.</param>
public FloatingPoint(decimal value)
{
Value = value;
}
/// <summary>
/// The actual value fo this <see cref="FloatingPoint"/> object.
/// </summary>
@ -76,16 +131,37 @@ namespace IPA.Config.Data
/// A <see cref="Value"/> representing a boolean value.
/// </summary>
public sealed class Boolean : Value
{
{
/// <summary>
/// Constructs an empty <see cref="Boolean"/> object.
/// </summary>
[Obsolete("Use the long constructor.")]
public Boolean()
{
Value = false;
}
/// <summary>
/// Constructs a <see cref="Boolean"/> object containing the provided value.
/// </summary>
/// <param name="value">The value to construct with.</param>
public Boolean(bool value)
{
Value = value;
}
/// <summary>
/// The actual value fo this <see cref="Boolean"/> object.
/// </summary>
public bool Value { get; set; }
public bool Value { get; set; }
/// <summary>
/// Converts this <see cref="Data.Value"/> into a human-readable format.
/// </summary>
/// <returns>the result of <c>Value.ToString().ToLower()</c></returns>
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);
}
}

+ 146
- 145
IPA.Loader/Config/Data/Value.cs View File

@ -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
{
/// <summary>
/// A base value type for config data abstract representations, to be serialized with an
/// <see cref="IConfigProvider"/>. If a <see cref="Value"/> is <see langword="null"/>, then
/// that represents just that: a <c>null</c> in whatever serialization is being used.
/// Also contains factory functions for all derived types.
/// </summary>
public abstract class Value
{
/// <summary>
/// Converts this <see cref="Value"/> into a human-readable format.
/// </summary>
/// <returns>a human-readable string containing the value provided</returns>
public abstract override string ToString();
/// <summary>
/// Creates a Null <see cref="Value"/>.
/// </summary>
/// <returns><see langword="null"/></returns>
public static Value Null() => null;
/// <summary>
/// Creates an empty <see cref="List"/>.
/// </summary>
/// <returns>an empty <see cref="List"/></returns>
/// <seealso cref="From(IEnumerable{Value})"/>
public static List List() => new List();
/// <summary>
/// Creates an empty <see cref="Map"/>.
/// </summary>
/// <returns>an empty <see cref="Map"/></returns>
/// <seealso cref="From(IDictionary{string, Value})"/>
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/>
public static Map Map() => new Map();
/// <summary>
/// Creates a new <see cref="Value"/> representing a <see cref="string"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Text(string)"/>
public static Text From(string val) => Text(val);
/// <summary>
/// Creates a new <see cref="Data.Text"/> object wrapping a <see cref="string"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(string)"/>
public static Text Text(string val) => val == null ? null : new Text { Value = val };
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="long"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Integer(long)"/>
public static Integer From(long val) => Integer(val);
/// <summary>
/// Creates a new <see cref="Data.Integer"/> wrapping a <see cref="long"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(long)"/>
public static Integer Integer(long val) => new Integer { Value = val };
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="double"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Float(decimal)"/>
public static FloatingPoint From(decimal val) => Float(val);
/// <summary>
/// Creates a new <see cref="FloatingPoint"/> wrapping a <see cref="decimal"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(decimal)"/>
public static FloatingPoint Float(decimal val) => new FloatingPoint { Value = val };
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="bool"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Bool(bool)"/>
public static Boolean From(bool val) => Bool(val);
/// <summary>
/// Creates a new <see cref="Boolean"/> wrapping a <see cref="bool"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(bool)"/>
public static Boolean Bool(bool val) => new Boolean { Value = val };
/// <summary>
/// Creates a new <see cref="Data.List"/> holding the content of an <see cref="IEnumerable{T}"/>
/// of <see cref="Value"/>.
/// </summary>
/// <param name="vals">the <see cref="Value"/>s to initialize the <see cref="Data.List"/> with</param>
/// <returns>a <see cref="Data.List"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="List"/>
public static List From(IEnumerable<Value> vals)
{
if (vals == null) return null;
var l = List();
l.AddRange(vals);
return l;
}
/// <summary>
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IDictionary{TKey, TValue}"/>
/// of <see cref="string"/> to <see cref="Value"/>.
/// </summary>
/// <param name="vals">the dictionary of <see cref="Value"/>s to initialize the <see cref="Data.Map"/> wtih</param>
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="Map"/>
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/>
public static Map From(IDictionary<string, Value> vals) => From(vals as IEnumerable<KeyValuePair<string, Value>>);
/// <summary>
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IEnumerable{T}"/>
/// of <see cref="KeyValuePair{TKey, TValue}"/> of <see cref="string"/> to <see cref="Value"/>.
/// </summary>
/// <param name="vals">the enumerable of <see cref="KeyValuePair{TKey, TValue}"/> of name to <see cref="Value"/></param>
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="Map"/>
/// <seealso cref="From(IDictionary{string, Value})"/>
public static Map From(IEnumerable<KeyValuePair<string, Value>> 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
{
/// <summary>
/// A base value type for config data abstract representations, to be serialized with an
/// <see cref="IConfigProvider"/>. If a <see cref="Value"/> is <see langword="null"/>, then
/// that represents just that: a <c>null</c> in whatever serialization is being used.
/// Also contains factory functions for all derived types.
/// </summary>
public abstract class Value
{
/// <summary>
/// Converts this <see cref="Value"/> into a human-readable format.
/// </summary>
/// <returns>a human-readable string containing the value provided</returns>
public abstract override string ToString();
/// <summary>
/// Creates a Null <see cref="Value"/>.
/// </summary>
/// <returns><see langword="null"/></returns>
public static Value? Null() => null;
/// <summary>
/// Creates an empty <see cref="List"/>.
/// </summary>
/// <returns>an empty <see cref="List"/></returns>
/// <seealso cref="From(IEnumerable{Value})"/>
public static List List() => new();
/// <summary>
/// Creates an empty <see cref="Map"/>.
/// </summary>
/// <returns>an empty <see cref="Map"/></returns>
/// <seealso cref="From(IDictionary{string, Value})"/>
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/>
public static Map Map() => new();
/// <summary>
/// Creates a new <see cref="Value"/> representing a <see cref="string"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Text(string)"/>
[return: NotNullIfNotNull("val")]
public static Text? From(string? val) => Text(val);
/// <summary>
/// Creates a new <see cref="Data.Text"/> object wrapping a <see cref="string"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(string)"/>
[return: NotNullIfNotNull("val")]
public static Text? Text(string? val) => val == null ? null : new(val);
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="long"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Integer(long)"/>
public static Integer From(long val) => Integer(val);
/// <summary>
/// Creates a new <see cref="Data.Integer"/> wrapping a <see cref="long"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(long)"/>
public static Integer Integer(long val) => new(val);
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="double"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Float(decimal)"/>
public static FloatingPoint From(decimal val) => Float(val);
/// <summary>
/// Creates a new <see cref="FloatingPoint"/> wrapping a <see cref="decimal"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(decimal)"/>
public static FloatingPoint Float(decimal val) => new(val);
/// <summary>
/// Creates a new <see cref="Value"/> wrapping a <see cref="bool"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="Bool(bool)"/>
public static Boolean From(bool val) => Bool(val);
/// <summary>
/// Creates a new <see cref="Boolean"/> wrapping a <see cref="bool"/>.
/// </summary>
/// <param name="val">the value to wrap</param>
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns>
/// <seealso cref="From(bool)"/>
public static Boolean Bool(bool val) => new(val);
/// <summary>
/// Creates a new <see cref="Data.List"/> holding the content of an <see cref="IEnumerable{T}"/>
/// of <see cref="Value"/>.
/// </summary>
/// <param name="vals">the <see cref="Value"/>s to initialize the <see cref="Data.List"/> with</param>
/// <returns>a <see cref="Data.List"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="List"/>
[return: NotNullIfNotNull("vals")]
public static List? From(IEnumerable<Value?>? vals)
{
if (vals is null) return null;
var l = List();
l.AddRange(vals);
return l;
}
/// <summary>
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IDictionary{TKey, TValue}"/>
/// of <see cref="string"/> to <see cref="Value"/>.
/// </summary>
/// <param name="vals">the dictionary of <see cref="Value"/>s to initialize the <see cref="Data.Map"/> wtih</param>
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="Map"/>
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/>
public static Map From(IDictionary<string, Value?> vals) => From(vals as IEnumerable<KeyValuePair<string, Value?>>);
/// <summary>
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IEnumerable{T}"/>
/// of <see cref="KeyValuePair{TKey, TValue}"/> of <see cref="string"/> to <see cref="Value"/>.
/// </summary>
/// <param name="vals">the enumerable of <see cref="KeyValuePair{TKey, TValue}"/> of name to <see cref="Value"/></param>
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns>
/// <seealso cref="Map"/>
/// <seealso cref="From(IDictionary{string, Value})"/>
[return: NotNullIfNotNull("vals")]
public static Map? From(IEnumerable<KeyValuePair<string, Value?>>? vals)
{
if (vals is null) return null;
var m = Map();
foreach (var v in vals) m.Add(v.Key, v.Value);
return m;
}
}
}

+ 9
- 9
IPA.Loader/Config/Providers/JsonConfigProvider.cs View File

@ -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);
}
}


+ 58
- 7
IPA.Loader/Config/SelfConfig.cs View File

@ -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<string, HashSet<string>>))]
public virtual HashSet<string> GameAssemblies { get; set; } = new HashSet<string>
[NonNullable, UseConverter(typeof(CollectionConverter<string, HashSet<string?>>))]
public virtual HashSet<string> GameAssemblies { get; set; } = GetDefaultGameAssemblies();
// BEGIN: section ignore
public static HashSet<string> 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<string> GetDefaultGameAssemblies() => null;
// LINE: ignore
#endif
// LINE: ignore
public static HashSet<string> GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet<string> { "Assembly-CSharp.dll" };


+ 15
- 5
IPA.Loader/Config/Stores/Attributes.cs View File

@ -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
/// <summary>
/// Gets whether or not to use the default converter for the member type instead of the specified type.
/// </summary>
[MemberNotNullWhen(false, nameof(ConverterType))]
public bool UseDefaultConverterForType { get; }
/// <summary>
/// Gets the type of the converter to use.
/// </summary>
public Type ConverterType { get; }
public Type? ConverterType { get; }
/// <summary>
/// Gets the target type of the converter if it is avaliable at instantiation time, otherwise
/// <see langword="null"/>.
/// </summary>
public Type ConverterTargetType { get; }
public Type? ConverterTargetType { get; }
/// <summary>
/// Gets whether or not this converter is a generic <see cref="ValueConverter{T}"/>.
/// </summary>
public bool IsGenericConverter => ConverterTargetType != null;
[MemberNotNullWhen(true, nameof(ConverterTargetType))]
public bool IsGenericConverter => ConverterTargetType is not null;
/// <summary>
/// Creates a new <see cref="UseConverterAttribute"/> specifying to use the default converter type for the target member.
@ -67,11 +71,17 @@ namespace IPA.Config.Stores.Attributes
/// <param name="converterType">the type to assign to <see cref="ConverterType"/></param>
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;


+ 17
- 16
IPA.Loader/Config/Stores/CollectionConverter.cs View File

@ -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
/// <typeparam name="T">the type of the items in the collection</typeparam>
/// <typeparam name="TCollection">the instantiated type of collection</typeparam>
public class CollectionConverter<T, TCollection> : ValueConverter<TCollection>
where TCollection : ICollection<T>
where TCollection : ICollection<T?>
{
/// <summary>
/// Creates a <see cref="CollectionConverter{T, TCollection}"/> using the default converter for the
@ -54,7 +55,7 @@ namespace IPA.Config.Stores.Converters
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
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
/// <param name="parent">the object that will own the resulting <typeparamref name="TCollection"/></param>
/// <returns>a new <typeparamref name="TCollection"/> holding the deserialized content of <paramref name="value"/></returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
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
/// <param name="parent">the object owning <paramref name="obj"/></param>
/// <returns>the <see cref="List"/> that <paramref name="obj"/> was serialized into</returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
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)));
}
/// <summary>
@ -92,7 +93,7 @@ namespace IPA.Config.Stores.Converters
/// <typeparam name="TConverter">the type of the converter to use for <typeparamref name="T"/></typeparam>
/// <seealso cref="CollectionConverter{T, TCollection}"/>
public sealed class CollectionConverter<T, TCollection, TConverter> : CollectionConverter<T, TCollection>
where TCollection : ICollection<T>
where TCollection : ICollection<T?>
where TConverter : ValueConverter<T>, new()
{
/// <summary>
@ -110,7 +111,7 @@ namespace IPA.Config.Stores.Converters
/// </summary>
/// <typeparam name="T">the element type of the <see cref="ISet{T}"/></typeparam>
/// <seealso cref="CollectionConverter{T, TCollection}"/>
public class ISetConverter<T> : CollectionConverter<T, ISet<T>>
public class ISetConverter<T> : CollectionConverter<T, ISet<T?>>
{
/// <summary>
/// Creates an <see cref="ISetConverter{T}"/> using the default converter for <typeparamref name="T"/>.
@ -128,8 +129,8 @@ namespace IPA.Config.Stores.Converters
/// <param name="size">the size to initialize it to</param>
/// <param name="parent">the object that will own the new object</param>
/// <returns>the new <see cref="ISet{T}"/></returns>
protected override ISet<T> Create(int size, object parent)
=> new HashSet<T>();
protected override ISet<T?> Create(int size, object parent)
=> new HashSet<T?>();
}
/// <summary>
/// An <see cref="ISetConverter{T}"/> which default constructs a converter for use as the value converter.
@ -155,7 +156,7 @@ namespace IPA.Config.Stores.Converters
/// </summary>
/// <typeparam name="T">the element type of the <see cref="List{T}"/></typeparam>
/// <seealso cref="CollectionConverter{T, TCollection}"/>
public class ListConverter<T> : CollectionConverter<T, List<T>>
public class ListConverter<T> : CollectionConverter<T, List<T?>>
{
/// <summary>
/// Creates an <see cref="ListConverter{T}"/> using the default converter for <typeparamref name="T"/>.
@ -173,8 +174,8 @@ namespace IPA.Config.Stores.Converters
/// <param name="size">the size to initialize it to</param>
/// <param name="parent">the object that will own the new object</param>
/// <returns>the new <see cref="List{T}"/></returns>
protected override List<T> Create(int size, object parent)
=> new List<T>(size);
protected override List<T?> Create(int size, object parent)
=> new(size);
}
/// <summary>
/// A <see cref="ListConverter{T}"/> which default constructs a converter for use as the value converter.
@ -199,7 +200,7 @@ namespace IPA.Config.Stores.Converters
/// </summary>
/// <typeparam name="T">the element type of the <see cref="IList{T}"/></typeparam>
/// <seealso cref="CollectionConverter{T, TCollection}"/>
public class IListConverter<T> : CollectionConverter<T, IList<T>>
public class IListConverter<T> : CollectionConverter<T, IList<T?>>
{
/// <summary>
/// Creates an <see cref="IListConverter{T}"/> using the default converter for <typeparamref name="T"/>.
@ -217,8 +218,8 @@ namespace IPA.Config.Stores.Converters
/// <param name="size">the size to initialize it to</param>
/// <param name="parent">the object that will own the new object</param>
/// <returns>the new <see cref="IList{T}"/></returns>
protected override IList<T> Create(int size, object parent)
=> new List<T>(size);
protected override IList<T?> Create(int size, object parent)
=> new List<T?>(size);
}
/// <summary>
/// An <see cref="IListConverter{T}"/> which default constructs a converter for use as the value converter.


+ 76
- 75
IPA.Loader/Config/Stores/Converters.cs View File

@ -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
/// </summary>
/// <param name="val">the <see cref="Value"/> to get the integral value of</param>
/// <returns>the integral value of <paramref name="val"/>, or <see langword="null"/></returns>
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
/// </summary>
/// <param name="val">the <see cref="Value"/> to get the floaing point value of</param>
/// <returns>the floaing point value of <paramref name="val"/>, or <see langword="null"/></returns>
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<DateTime>, IValConv<DateTimeOffset>,
IValConv<TimeSpan>
{
internal static readonly ValConvImpls Impl = new ValConvImpls();
internal static readonly ValConvImpls Impl = new();
Type IValConv<char>.Get() => typeof(CharConverter);
Type IValConv<long>.Get() => typeof(LongConverter);
Type IValConv<ulong>.Get() => typeof(ULongConverter);
@ -145,7 +146,7 @@ namespace IPA.Config.Stores.Converters
/// <typeparam name="T">the type of the <see cref="ValueConverter{T}"/> that this works on</typeparam>
public static class Converter<T>
{
private static ValueConverter<T> defaultConverter = null;
private static ValueConverter<T>? defaultConverter;
/// <summary>
/// Gets the default <see cref="ValueConverter{T}"/> for the current type.
/// </summary>
@ -158,7 +159,7 @@ namespace IPA.Config.Stores.Converters
//Logger.log.Debug($"Converter<{t}>.MakeDefault()");
static ValueConverter<T> MakeInstOf(Type ty)
=> Activator.CreateInstance(ty) as ValueConverter<T>;
=> (ValueConverter<T>)Activator.CreateInstance(ty);
return MakeInstOf(Converter.GetDefaultConverterType(t));
}
@ -193,16 +194,16 @@ namespace IPA.Config.Stores.Converters
/// <param name="value">the <see cref="Value"/> tree to convert</param>
/// <param name="parent">the object which will own the created object</param>
/// <returns>the object represented by <paramref name="value"/></returns>
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));
/// <summary>
/// Converts a nullable <typeparamref name="T"/> to a <see cref="Value"/> tree.
/// </summary>
/// <param name="obj">the value to serialize</param>
/// <param name="parent">the object which owns <paramref name="obj"/></param>
/// <returns>a <see cref="Value"/> tree representing <paramref name="obj"/>.</returns>
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);
}
/// <summary>
@ -237,7 +238,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the object which will own the created object</param>
/// <returns>the deserialized enum value</returns>
/// <exception cref="ArgumentException">if <paramref name="value"/> is not a <see cref="Text"/> node</exception>
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
/// <param name="obj">the value to serialize</param>
/// <param name="parent">the object which owns <paramref name="obj"/></param>
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns>
public override Value ToValue(T obj, object parent)
=> Value.Text(obj.ToString());
public override Value? ToValue(T? obj, object parent)
=> Value.Text(obj?.ToString());
}
/// <summary>
@ -267,7 +268,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the object which will own the created object</param>
/// <returns>the deserialized enum value</returns>
/// <exception cref="ArgumentException">if <paramref name="value"/> is not a <see cref="Text"/> node</exception>
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
/// <param name="obj">the value to serialize</param>
/// <param name="parent">the object which owns <paramref name="obj"/></param>
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns>
public override Value ToValue(T obj, object parent)
=> Value.Text(obj.ToString());
public override Value? ToValue(T? obj, object parent)
=> Value.Text(obj?.ToString());
}
/// <summary>
@ -296,7 +297,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the object which will own the created object</param>
/// <returns>the deserialized enum value</returns>
/// <exception cref="ArgumentException">if <paramref name="value"/> is not a numeric node</exception>
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
/// <param name="obj">the value to serialize</param>
/// <param name="parent">the object which owns <paramref name="obj"/></param>
/// <returns>an <see cref="Integer"/> node representing <paramref name="obj"/></returns>
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 <see cref="IDictionary{TKey, TValue}"/>.
/// </summary>
/// <typeparam name="TValue">the value type of the dictionary</typeparam>
public class IDictionaryConverter<TValue> : ValueConverter<IDictionary<string, TValue>>
public class IDictionaryConverter<TValue> : ValueConverter<IDictionary<string, TValue?>>
{
/// <summary>
/// Gets the converter for the dictionary's value type.
@ -338,9 +339,9 @@ namespace IPA.Config.Stores.Converters
/// <param name="value">the <see cref="Map"/> to convert</param>
/// <param name="parent">the parent that will own the resulting object</param>
/// <returns>the deserialized dictionary</returns>
public override IDictionary<string, TValue> 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<string, TValue?> 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));
/// <summary>
@ -349,8 +350,8 @@ namespace IPA.Config.Stores.Converters
/// <param name="obj">the dictionary to serialize</param>
/// <param name="parent">the object that owns the dictionary</param>
/// <returns>the dictionary serialized as a <see cref="Map"/></returns>
public override Value ToValue(IDictionary<string, TValue> obj, object parent)
=> Value.From(obj.Select(p => new KeyValuePair<string, Value>(p.Key, BaseConverter.ToValue(p.Value, parent))));
public override Value? ToValue(IDictionary<string, TValue?>? obj, object parent)
=> Value.From(obj.Select(p => new KeyValuePair<string, Value?>(p.Key, BaseConverter.ToValue(p.Value, parent))));
}
/// <summary>
@ -373,7 +374,7 @@ namespace IPA.Config.Stores.Converters
/// A converter for instances of <see cref="Dictionary{TKey, TValue}"/>.
/// </summary>
/// <typeparam name="TValue">the value type of the dictionary</typeparam>
public class DictionaryConverter<TValue> : ValueConverter<Dictionary<string, TValue>>
public class DictionaryConverter<TValue> : ValueConverter<Dictionary<string, TValue?>>
{
/// <summary>
/// Gets the converter for the dictionary's value type.
@ -397,7 +398,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="value">the <see cref="Map"/> to convert</param>
/// <param name="parent">the parent that will own the resulting object</param>
/// <returns>the deserialized dictionary</returns>
public override Dictionary<string, TValue> FromValue(Value value, object parent)
public override Dictionary<string, TValue?> 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
/// <param name="obj">the dictionary to serialize</param>
/// <param name="parent">the object that owns the dictionary</param>
/// <returns>the dictionary serialized as a <see cref="Map"/></returns>
public override Value ToValue(Dictionary<string, TValue> obj, object parent)
=> Value.From(obj.Select(p => new KeyValuePair<string, Value>(p.Key, BaseConverter.ToValue(p.Value, parent))));
public override Value? ToValue(Dictionary<string, TValue?>? obj, object parent)
=> Value.From(obj?.Select(p => new KeyValuePair<string, Value?>(p.Key, BaseConverter.ToValue(p.Value, parent))));
}
/// <summary>
@ -433,7 +434,7 @@ namespace IPA.Config.Stores.Converters
/// A converter for instances of <see cref="IReadOnlyDictionary{TKey, TValue}"/>.
/// </summary>
/// <typeparam name="TValue">the value type of the dictionary</typeparam>
public class IReadOnlyDictionaryConverter<TValue> : ValueConverter<IReadOnlyDictionary<string, TValue>>
public class IReadOnlyDictionaryConverter<TValue> : ValueConverter<IReadOnlyDictionary<string, TValue?>>
{
/// <summary>
/// Gets the converter for the dictionary's value type.
@ -457,7 +458,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="value">the <see cref="Map"/> to convert</param>
/// <param name="parent">the parent that will own the resulting object</param>
/// <returns>the deserialized dictionary</returns>
public override IReadOnlyDictionary<string, TValue> FromValue(Value value, object parent)
public override IReadOnlyDictionary<string, TValue?> 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
/// <param name="obj">the dictionary to serialize</param>
/// <param name="parent">the object that owns the dictionary</param>
/// <returns>the dictionary serialized as a <see cref="Map"/></returns>
public override Value ToValue(IReadOnlyDictionary<string, TValue> obj, object parent)
=> Value.From(obj.Select(p => new KeyValuePair<string, Value>(p.Key, BaseConverter.ToValue(p.Value, parent))));
public override Value? ToValue(IReadOnlyDictionary<string, TValue?>? obj, object parent)
=> Value.From(obj?.Select(p => new KeyValuePair<string, Value?>(p.Key, BaseConverter.ToValue(p.Value, parent))));
}
/// <summary>
@ -500,7 +501,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the object which will own the created object</param>
/// <returns>the deserialized Color object</returns>
/// <exception cref="ArgumentException">if <paramref name="value"/> is not a <see cref="Text"/> node or couldn't be parsed into a Color object</exception>
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<string>
{
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<char>
{
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<long>
{
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<ulong>
{
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<IntPtr>
{
public override IntPtr FromValue(Value value, object parent)
public override IntPtr FromValue(Value? value, object parent)
=> (IntPtr)Converter<long>.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<UIntPtr>
{
public override UIntPtr FromValue(Value value, object parent)
public override UIntPtr FromValue(Value? value, object parent)
=> (UIntPtr)Converter<ulong>.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<int>
{
public override int FromValue(Value value, object parent)
public override int FromValue(Value? value, object parent)
=> (int)Converter<long>.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<uint>
{
public override uint FromValue(Value value, object parent)
public override uint FromValue(Value? value, object parent)
=> (uint)Converter<long>.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<short>
{
public override short FromValue(Value value, object parent)
public override short FromValue(Value? value, object parent)
=> (short)Converter<long>.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<ushort>
{
public override ushort FromValue(Value value, object parent)
public override ushort FromValue(Value? value, object parent)
=> (ushort)Converter<long>.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<byte>
{
public override byte FromValue(Value value, object parent)
public override byte FromValue(Value? value, object parent)
=> (byte)Converter<long>.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<sbyte>
{
public override sbyte FromValue(Value value, object parent)
public override sbyte FromValue(Value? value, object parent)
=> (sbyte)Converter<long>.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<decimal>
{
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<float>
{
public override float FromValue(Value value, object parent)
public override float FromValue(Value? value, object parent)
=> (float)Converter<decimal>.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<double>
{
public override double FromValue(Value value, object parent)
public override double FromValue(Value? value, object parent)
=> (double)Converter<decimal>.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<bool>
{
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<DateTime>
{
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<DateTimeOffset>
{
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<TimeSpan>
{
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());
}
}

+ 17
- 14
IPA.Loader/Config/Stores/CustomObjectConverter.cs View File

@ -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<U> : IImpl where U : class, GeneratedStoreImpl.IGeneratedStore<T>, 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
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public static T Deserialize(Value value, object parent)
public static T? Deserialize(Value? value, object parent)
=> impl.FromValue(value, parent);
/// <summary>
@ -61,7 +64,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the parent object that owns <paramref name="obj"/></param>
/// <returns>the <see cref="Value"/> tree that represents <paramref name="obj"/></returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public static Value Serialize(T obj, object parent)
public static Value? Serialize(T? obj, object parent)
=> impl.ToValue(obj, parent);
/// <summary>
@ -71,7 +74,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public override T FromValue(Value value, object parent)
public override T? FromValue(Value? value, object parent)
=> Deserialize(value, parent);
/// <summary>
@ -81,7 +84,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the parent object that owns <paramref name="obj"/></param>
/// <returns>the <see cref="Value"/> tree that represents <paramref name="obj"/></returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
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
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public static T Deserialize(Value value, object parent)
public static T Deserialize(Value? value, object parent)
=> deserialize(value, parent);
/// <summary>
@ -123,7 +126,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the parent object that will own the deserialized value</param>
/// <returns>the deserialized value</returns>
/// <seealso cref="ValueConverter{T}.FromValue(Value, object)"/>
public override T FromValue(Value value, object parent)
public override T FromValue(Value? value, object parent)
=> Deserialize(value, parent);
/// <summary>
@ -133,7 +136,7 @@ namespace IPA.Config.Stores.Converters
/// <param name="parent">the parent object that owns <paramref name="obj"/></param>
/// <returns>the <see cref="Value"/> tree that represents <paramref name="obj"/></returns>
/// <seealso cref="ValueConverter{T}.ToValue(T, object)"/>
public override Value ToValue(T obj, object parent)
public override Value? ToValue(T obj, object parent)
=> Serialize(obj);
}


+ 5
- 4
IPA.Loader/Config/Stores/GeneratedStoreImpl/ConversionDelegates.cs View File

@ -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>(T obj);
internal delegate T DeserializeObject<T>(Value val, object parent);
internal delegate T DeserializeObject<T>(Value? val, object parent);
private static class DelegateStore<T>
{
public static SerializeObject<T> Serialize;
public static DeserializeObject<T> Deserialize;
public static SerializeObject<T>? Serialize;
public static DeserializeObject<T>? Deserialize;
}
internal static SerializeObject<T> GetSerializerDelegate<T>()


+ 2
- 1
IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs View File

@ -1,4 +1,5 @@
using IPA.Config.Data;
#nullable enable
using IPA.Config.Data;
using IPA.Config.Stores.Attributes;
using IPA.Logging;
using System;


+ 8
- 3
IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs View File

@ -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<ILGenerator> thisarg, Action<ILGenerator> 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<ILGenerator> thisobj, Action<ILGenerator> 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);


+ 15
- 11
IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs View File

@ -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<ParameterModifier>());
internal static T Create<T>(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent);
internal static T Create<T>(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<Type, (GeneratedStoreCreator ctor, Type type)> generatedCreators
= new SingleCreationValueCache<Type, (GeneratedStoreCreator ctor, Type type)>();
private static readonly SingleCreationValueCache<Type, (GeneratedStoreCreator ctor, Type type)> 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<Type, Dictionary<Type, FieldInfo>> TypeRequiredConverters = new Dictionary<Type, Dictionary<Type, FieldInfo>>();
private static readonly Dictionary<Type, Dictionary<Type, FieldInfo>> TypeRequiredConverters = new();
private static void CreateAndInitializeConvertersFor(Type type, IEnumerable<SerializedMemberInfo> structure)
{
if (!TypeRequiredConverters.TryGetValue(type, out var converters))
@ -96,7 +96,8 @@ namespace IPA.Config.Stores
var converterFieldType = Module.DefineType($"{type.FullName}<Converters>",
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<Type, FieldInfo>(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];
}
}
}
}

+ 25
- 24
IPA.Loader/Config/Stores/GeneratedStoreImpl/IGeneratedStore.cs View File

@ -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<ChangeTransactionObj> freeTransactionObjs = new Stack<ChangeTransactionObj>();
private static readonly Stack<ChangeTransactionObj> 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);


+ 9
- 8
IPA.Loader/Config/Stores/GeneratedStoreImpl/MakeCreator.cs View File

@ -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}<Generated>",
@ -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($"<g>{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($"<g>{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);


+ 25
- 20
IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs View File

@ -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<T>
[MemberNotNullWhen(true, nameof(NullableWrappedType))]
public bool IsNullable { get; set; } // signifies whether this is a Nullable<T>
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<int>.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<Type, SerializedMemberInfo[]> objectStructureCache
= new SingleCreationValueCache<Type, SerializedMemberInfo[]>();
private static readonly SingleCreationValueCache<Type, SerializedMemberInfo[]> objectStructureCache = new();
private static IEnumerable<SerializedMemberInfo> ReadObjectMembers(Type type)
=> objectStructureCache.GetOrAdd(type, t => ReadObjectMembersInternal(type).ToArray());


+ 16
- 4
IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs View File

@ -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);
}


+ 21
- 18
IPA.Loader/Config/Stores/GeneratedStoreImpl/Utility.cs View File

@ -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<Type, Stack<LocalBuilder>> unallocatedLocals = new Dictionary<Type, Stack<LocalBuilder>>();
private readonly Dictionary<Type, Stack<LocalBuilder>> 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<ILGenerator> expected = null, Action<ILGenerator> found = null)
private static void EmitLogError(ILGenerator il, string message, bool tailcall = false, Action<ILGenerator>? expected = null, Action<ILGenerator>? found = null)
{
if (expected == null) expected = il => il.Emit(OpCodes.Ldnull);
if (found == null) found = il => il.Emit(OpCodes.Ldnull);


+ 2
- 1
IPA.Loader/Config/Stores/GeneratedStorePublicInterface.cs View File

@ -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;


+ 8
- 7
IPA.Loader/Config/Stores/ValueConverter.cs View File

@ -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
/// <param name="obj">the object to convert</param>
/// <param name="parent">the owning object of <paramref name="obj"/></param>
/// <returns>a representation of <paramref name="obj"/> as a <see cref="Value"/> structure</returns>
Value ToValue(object obj, object parent);
Value? ToValue(object? obj, object parent);
/// <summary>
/// Converts the given <see cref="Value"/> to the object type handled by this converter.
/// </summary>
/// <param name="value">the <see cref="Value"/> to deserialize</param>
/// <param name="parent">the object that will own the result</param>
/// <returns>the deserialized object</returns>
object FromValue(Value value, object parent);
object? FromValue(Value? value, object parent);
/// <summary>
/// Gets the type that this <see cref="IValueConverter"/> handles.
/// </summary>
@ -59,7 +60,7 @@ namespace IPA.Config.Stores
/// <param name="parent">the owning object of <paramref name="obj"/></param>
/// <returns>a representation of <paramref name="obj"/> as a <see cref="Value"/> structure</returns>
/// <seealso cref="IValueConverter.ToValue"/>
public abstract Value ToValue(T obj, object parent);
public abstract Value? ToValue(T? obj, object parent);
/// <summary>
/// Converts the given <see cref="Value"/> to the object type handled by this converter.
/// </summary>
@ -67,10 +68,10 @@ namespace IPA.Config.Stores
/// <param name="parent">the object that will own the result</param>
/// <returns>the deserialized object</returns>
/// <seealso cref="IValueConverter.FromValue"/>
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);
}
}

+ 22
- 10
IPA.Loader/IPA.Loader.csproj View File

@ -3,16 +3,16 @@
<Import Project="..\Common.props" />
<PropertyGroup>
<TargetFrameworks>net461;net35</TargetFrameworks>
<TargetFrameworks>net472</TargetFrameworks>
<RootNamespace>IPA</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net461' ">true</BuildForBeatSaber>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net472' ">true</BuildForBeatSaber>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
<DefineConstants>$(DefineConstants);NET4</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net35'">
@ -22,7 +22,7 @@
<DefineConstants>$(DefineConstants);BeatSaber</DefineConstants>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Refs\UnityEngine.CoreModule.Net4.dll</HintPath>
<Private>False</Private>
@ -49,12 +49,19 @@
<ItemGroup>
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" />
<PackageReference Include="Lib.Harmony" Version="2.0.2" />
<PackageReference Include="Mono.Cecil" Version="0.10.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Portable.System.ValueTuple" Version="1.0.1" />
<PackageReference Include="SemanticVersioning" Version="1.2.2" />
<PackageReference Include="AsyncBridge" Version="0.3.1" />
<!--<PackageReference Include="Lib.Harmony" Version="2.0.2" />-->
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<!--<PackageReference Include="AsyncBridge" Version="0.3.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0">
<ExcludeAssets>buildtransitive</ExcludeAssets>
</PackageReference>-->
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="HarmonyX" Version="2.7.0" />
<PackageReference Include="MonoMod.RuntimeDetour" Version="21.12.13.1" />
<PackageReference Include="Hive.Versioning.Standalone" Version="0.1.0-gh846.1" />
<ProjectReference Include="..\SemVer\SemVer.csproj" />
</ItemGroup>
<ItemGroup>
@ -69,6 +76,11 @@
<None Include="Updating\BeatMods\*.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net35'">
<Compile Remove="AntiMalware\_HideInNet3\**" />
<None Include="AntiMalware\_HideInNet3\**" />
</ItemGroup>
<Import Project="..\Common.targets" />
</Project>

+ 30
- 8
IPA.Loader/JsonConverters/FeaturesFieldConverter.cs View File

@ -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<Dictionary<string, JObject>>
internal class FeaturesFieldConverter : JsonConverter<Dictionary<string, List<JObject>>>
{
public override Dictionary<string, JObject> ReadJson(JsonReader reader, Type objectType, Dictionary<string, JObject> existingValue, bool hasExistingValue, JsonSerializer serializer)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Assert([DoesNotReturnIf(false)] bool condition)
{
if (!condition)
throw new InvalidOperationException();
}
public override Dictionary<string, List<JObject>> ReadJson(JsonReader reader, Type objectType, Dictionary<string, List<JObject>> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartArray)
{
_ = serializer.Deserialize<string[]>(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<Dictionary<string, JObject>>(reader);
var dict = new Dictionary<string, List<JObject>>();
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<JObject>(reader) })
: serializer.Deserialize<List<JObject>>(reader);
dict.Add(name, list);
Assert(reader.Read());
}
return dict;
}
public override void WriteJson(JsonWriter writer, Dictionary<string, JObject> value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, Dictionary<string, List<JObject>> value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}


+ 20
- 15
IPA.Loader/JsonConverters/SemverRangeConverter.cs View File

@ -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<Range>
{
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<VersionRange?>
{
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());
}
}
}

+ 19
- 17
IPA.Loader/JsonConverters/SemverVersionConverter.cs View File

@ -1,17 +1,19 @@
using System;
using Newtonsoft.Json;
using Version = SemVer.Version;
namespace IPA.JsonConverters
{
internal class SemverVersionConverter : JsonConverter<Version>
{
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<Version?>
{
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());
}
}
}

+ 1
- 1
IPA.Loader/Loader/Composite/CompositeBSPlugin.cs View File

@ -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}");
}
}
}


+ 1
- 1
IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs View File

@ -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}");
}
}
}


+ 21
- 0
IPA.Loader/Loader/DependencyResolutionLoopException.cs View File

@ -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()
{
}
}
}

+ 3
- 3
IPA.Loader/Loader/DisabledConfig.cs View File

@ -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);
}
});
}


+ 4
- 3
IPA.Loader/Loader/Features/ConfigProviderFeature.cs View File

@ -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<DataModel>();
data = featureData.ToObject<DataModel>() ?? 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;


+ 13
- 12
IPA.Loader/Loader/Features/DefineFeature.cs View File

@ -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<DataModel>();
data = featureData.ToObject<DataModel>() ?? 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;
}
}


+ 0
- 1
IPA.Loader/Loader/HarmonyProtector.cs View File

@ -1,5 +1,4 @@
using HarmonyLib;
using System.Collections.Generic;
using System.Reflection;
namespace IPA.Loader


+ 233
- 244
IPA.Loader/Loader/LibLoader.cs View File

@ -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<string, string> 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<string, string>();
#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<string, string> 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<string, string>();
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<FileInfo> TraverseTree(string root, Func<string, bool> dirValidator = null)
{
if (dirValidator == null) dirValidator = s => true;
var dirs = new Stack<string>(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<FileInfo> TraverseTree(string root, Func<string, bool>? dirValidator = null)
{
if (dirValidator == null) dirValidator = s => true;
var dirs = new Stack<string>(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;
}
}
}
}
}

+ 9
- 5
IPA.Loader/Loader/PluginExecutor.cs View File

@ -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<object> MakeLifecycleEnableFunc(Type type, string name)
{
var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() 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<IEdgeLifecycleAttribute>()))
@ -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<object, Task> MakeLifecycleDisableFunc(Type type, string name)
{
var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() 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<IEdgeLifecycleAttribute>()))
@ -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);


+ 120
- 51
IPA.Loader/Loader/PluginInitInjector.cs View File

@ -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
/// </item>
/// <item>
/// <term><see cref="Config.Config"/></term>
/// <description>
/// <para>A <see cref="Config.Config"/> object for the plugin being injected.</para>
/// <description>A <see cref="Config.Config"/> object for the plugin being injected.
/// <para>
/// These parameters may have <see cref="Config.Config.NameAttribute"/> and <see cref="Config.Config.PreferAttribute"/> to control
/// how it is constructed.
/// </para>
/// </description>
/// </item>
/// <item>
/// <term><see cref="IAntiMalware"/></term>
/// <description>The <see cref="IAntiMalware"/> instance which should be used for any potentially dangerous files.</description>
/// </item>
/// </list>
/// 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.
/// </remarks>
@ -57,7 +60,40 @@ namespace IPA.Loader
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginMetadata"/> for the plugin being loaded.</param>
/// <returns>the value to inject into that parameter.</returns>
public delegate object InjectParameter(object previous, ParameterInfo param, PluginMetadata meta);
public delegate object? InjectParameter(object? previous, ParameterInfo param, PluginMetadata meta);
/// <summary>
/// A provider for parameter injectors to request injected values themselves.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="forParam">the parameter that this is providing for.</param>
/// <param name="typeOverride">an optional override for the parameter type.</param>
/// <returns>the value that would otherwise be injected.</returns>
public delegate object? InjectedValueProvider(ParameterInfo forParam, Type? typeOverride = null);
/// <summary>
/// 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.
/// </summary>
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param>
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginMetadata"/> for the plugin being loaded.</param>
/// <param name="provider">an <see cref="InjectedValueProvider"/> to allow the injector to request injected values.</param>
/// <returns>the value to inject into that parameter.</returns>
public delegate object? InjectParameterNested(object? previous, ParameterInfo param, PluginMetadata meta, InjectedValueProvider provider);
/// <summary>
/// Invokes the provider with <paramref name="param"/> and <typeparamref name="T"/> and casts the result to <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">the type of object to be injected</typeparam>
/// <param name="provider">the provider to invoke.</param>
/// <param name="param">the parameter to provide for</param>
/// <returns>the value requested, or <see langword="null"/>.</returns>
public static T? Inject<T>(this InjectedValueProvider provider, ParameterInfo param)
=> (T?)provider?.Invoke(param, typeof(T));
/// <summary>
/// Adds an injector to be used when calling future plugins' Init methods.
@ -65,6 +101,14 @@ namespace IPA.Loader
/// <param name="type">the type of the parameter.</param>
/// <param name="injector">the function to call for injection.</param>
public static void AddInjector(Type type, InjectParameter injector)
=> AddInjector(type, (pre, par, met, pro) => injector(pre, par, met));
/// <summary>
/// Adds an injector to be used when calling future plugins' Init methods.
/// </summary>
/// <param name="type">the type of the parameter.</param>
/// <param name="injector">the function to call for injection.</param>
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<TypedInjector>
{
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<TypedInjector> injectors = new List<TypedInjector>
private static readonly List<TypedInjector> 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<TypedInjector, object?> previousValues,
PluginMetadata meta,
ParameterInfo param,
Type paramType,
InjectedValueProvider provider)
{
var initArgs = new List<object>();
var value = paramType.GetDefault();
var previousValues = persist as Dictionary<TypedInjector, object>;
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<TypedInjector, object>(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<TypedInjector, object?> 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<object?>();
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();
}
}


+ 224
- 374
IPA.Loader/Loader/PluginLoader.cs View File

@ -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<PluginManifest>(manifest);
var manifestObj = JsonConvert.DeserializeObject<PluginManifest>(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<PluginManifest>(File.ReadAllText(manifest));
var manifestObj = JsonConvert.DeserializeObject<PluginManifest>(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
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
@ -363,7 +376,7 @@ namespace IPA.Loader
/// </remarks>
Conflict,
/// <summary>
/// The plugin this reason is assiciated with is missing a dependency.
/// The plugin this reason is associated with is missing a dependency.
/// </summary>
/// <remarks>
/// Since this is only given when a dependency is missing, <see cref="IgnoreReason.RelatedTo"/> will
@ -381,7 +394,7 @@ namespace IPA.Loader
/// </summary>
Feature,
/// <summary>
/// The plugin this reason is assoicated with is unsupported.
/// The plugin this reason is associated with is unsupported.
/// </summary>
/// <remarks>
/// Currently, there is no path in the loader that emits this <see cref="Reason"/>, however there may
@ -479,274 +492,19 @@ namespace IPA.Loader
// the thing -> the reason
internal static Dictionary<PluginMetadata, IgnoreReason> 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<string>();
var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
var resolved = new List<PluginMetadata>(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<PluginMetadata>(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<PluginMetadata>(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<PluginMetadata> 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<PluginMetadata>();
foreach (var meta in PluginsMetadata)
InsertInto(pluginTree, meta, true);
static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
{
foreach (var st in tree)
if (!into.Contains(st))
{
DeTree(into, st.Dependencies);
DeTree(into, st.LoadsAfter);
into.Add(st);
}
}
PluginsMetadata = new List<PluginMetadata>();
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<PluginMetadata>();
var pluginsToLoad = new Dictionary<string, Version>();
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<string, (PluginMetadata Meta, bool Enabled)>(PluginsMetadata.Count);
var pluginsToProcess = new List<PluginMetadata>(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<string, (PluginMetadata Meta, bool Disabled, bool Ignored)>();
var outputOrder = new List<PluginMetadata>(PluginsMetadata.Count);
var isProcessing = new HashSet<PluginMetadata>();
{
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<PluginMetadata> 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);
}
}


+ 25
- 27
IPA.Loader/Loader/PluginManager.cs View File

@ -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
/// <summary>
/// Gets info about the enabled plugin with the specified ID.
/// </summary>
/// <param name="name">the ID name of the plugin to get (must be an exact match)</param>
/// <param name="id">the ID name of the plugin to get (must be an exact match)</param>
/// <returns>the plugin metadata for the requested plugin or <see langword="null"/> if it doesn't exist or is disabled</returns>
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);
/// <summary>
/// Gets a disabled plugin's metadata by its name.
@ -68,10 +66,10 @@ namespace IPA.Loader
/// <summary>
/// Gets a disabled plugin's metadata by its ID.
/// </summary>
/// <param name="name">the ID of the disabled plugin to get</param>
/// <param name="id">the ID of the disabled plugin to get</param>
/// <returns>the metadata for the corresponding plugin</returns>
public static PluginMetadata GetDisabledPluginFromId(string name) =>
DisabledPlugins.FirstOrDefault(p => p.Id == name);
public static PluginMetadata GetDisabledPluginFromId(string id) =>
DisabledPlugins.FirstOrDefault(p => p.Id == id);
/// <summary>
/// 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<Old.IPlugin> 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<string>())}");
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<string>())}");
}
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;


+ 7
- 6
IPA.Loader/Loader/PluginManifest.cs View File

@ -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<string, Range> Dependencies = new();
public Dictionary<string, VersionRange> Dependencies = new();
[JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))]
public Dictionary<string, Range> Conflicts = new();
public Dictionary<string, VersionRange> Conflicts = new();
[JsonProperty("features", Required = Required.DisallowNull), JsonConverter(typeof(FeaturesFieldConverter))]
public Dictionary<string, JObject> Features = new();
public Dictionary<string, List<JObject>> Features = new();
[JsonProperty("loadBefore", Required = Required.DisallowNull)]
public string[] LoadBefore = Array.Empty<string>();


+ 11
- 3
IPA.Loader/Loader/PluginMetadata.cs View File

@ -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.
/// </summary>
/// <value>the version of the plugin</value>
public Version Version => manifest.Version;
[Obsolete("Use HVersion instead.")]
public SVersion Version => SVersion.ForHiveVersion(manifest.Version);
/// <summary>
/// The version of the plugin.
/// </summary>
/// <value>the version of the plugin</value>
public Version HVersion => manifest.Version;
/// <summary>
/// The file the plugin was loaded from.
@ -144,6 +152,6 @@ namespace IPA.Loader
/// Gets all of the metadata as a readable string.
/// </summary>
/// <returns>the readable printable metadata string</returns>
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)}'";
}
}

+ 24
- 24
IPA.Loader/Loader/StateTransitionTransaction.cs View File

@ -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<PluginMetadata> currentlyEnabled;
private readonly HashSet<PluginMetadata> currentlyDisabled;
private readonly HashSet<PluginMetadata> toEnable = new HashSet<PluginMetadata>();
private readonly HashSet<PluginMetadata> toDisable = new HashSet<PluginMetadata>();
private bool stateChanged = false;
private readonly HashSet<PluginMetadata> toEnable = new ();
private readonly HashSet<PluginMetadata> toDisable = new ();
private bool stateChanged;
internal StateTransitionTransaction(IEnumerable<PluginMetadata> enabled, IEnumerable<PluginMetadata> disabled)
{
@ -118,16 +118,17 @@ namespace IPA.Loader
/// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
/// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
/// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
public bool Enable(PluginMetadata meta, out IEnumerable<PluginMetadata> disabledDeps, bool autoDeps = false)
public bool Enable(PluginMetadata meta, out IEnumerable<PluginMetadata>? 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
/// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
/// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
/// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
public bool Disable(PluginMetadata meta, out IEnumerable<PluginMetadata> enabledDependents, bool autoDependents = false)
public bool Disable(PluginMetadata meta, out IEnumerable<PluginMetadata>? 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<byte>();
private T ThrowIfDisposed<T>()
private T? ThrowIfDisposed<T>()
{
if (disposed)
throw new ObjectDisposedException(nameof(StateTransitionTransaction));
return default;
return disposed ? throw new ObjectDisposedException(nameof(StateTransitionTransaction)) : default;
}
private bool disposed = false;
private bool disposed;
/// <summary>
/// Disposes and discards this transaction without committing it.
/// </summary>


+ 1
- 5
IPA.Loader/Loader/description.md View File

@ -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.
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.

+ 9
- 7
IPA.Loader/Loader/manifest.json View File

@ -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"
]
}

+ 39
- 38
IPA.Loader/Logging/LogPrinter.cs View File

@ -1,39 +1,40 @@
using System;
namespace IPA.Logging
{
/// <summary>
/// The log printer's base class.
/// </summary>
public abstract class LogPrinter
{
/// <summary>
/// Provides a filter for which log levels to allow through.
/// </summary>
/// <value>the level to filter to</value>
public abstract Logger.LogLevel Filter { get; set; }
/// <summary>
/// Prints a provided message from a given log at the specified time.
/// </summary>
/// <param name="level">the log level</param>
/// <param name="time">the time the message was composed</param>
/// <param name="logName">the name of the log that created this message</param>
/// <param name="message">the message</param>
public abstract void Print(Logger.Level level, DateTime time, string logName, string message);
/// <summary>
/// Called before the first print in a group. May be called multiple times.
/// Use this to create file handles and the like.
/// </summary>
public virtual void StartPrint() { }
/// <summary>
/// Called after the last print in a group. May be called multiple times.
/// Use this to dispose file handles and the like.
/// </summary>
public virtual void EndPrint() { }
internal DateTime LastUse { get; set; }
}
#nullable enable
using System;
namespace IPA.Logging
{
/// <summary>
/// The log printer's base class.
/// </summary>
public abstract class LogPrinter
{
/// <summary>
/// Provides a filter for which log levels to allow through.
/// </summary>
/// <value>the level to filter to</value>
public abstract Logger.LogLevel Filter { get; set; }
/// <summary>
/// Prints a provided message from a given log at the specified time.
/// </summary>
/// <param name="level">the log level</param>
/// <param name="time">the time the message was composed</param>
/// <param name="logName">the name of the log that created this message</param>
/// <param name="message">the message</param>
public abstract void Print(Logger.Level level, DateTime time, string logName, string message);
/// <summary>
/// Called before the first print in a group. May be called multiple times.
/// Use this to create file handles and the like.
/// </summary>
public virtual void StartPrint() { }
/// <summary>
/// Called after the last print in a group. May be called multiple times.
/// Use this to dispose file handles and the like.
/// </summary>
public virtual void EndPrint() { }
internal DateTime LastUse { get; set; }
}
}

+ 15
- 12
IPA.Loader/Logging/Logger.cs View File

@ -1,6 +1,5 @@
using System;
// ReSharper disable InconsistentNaming
#nullable enable
using System;
namespace IPA.Logging
{
@ -9,9 +8,9 @@ namespace IPA.Logging
/// </summary>
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;
/// <summary>


+ 159
- 116
IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs View File

@ -1,116 +1,159 @@
using System;
using System.Runtime.InteropServices;
namespace IPA.Logging.Printers
{
/// <summary>
/// Prints a pretty message to the console.
/// </summary>
public class ColoredConsolePrinter : LogPrinter
{
private Logger.LogLevel filter = Logger.LogLevel.All;
/// <summary>
/// A filter for this specific printer.
/// </summary>
/// <value>the filter to apply to this printer</value>
public override Logger.LogLevel Filter { get => filter; set => filter = value; }
/// <summary>
/// The color to print messages as.
/// </summary>
/// <value>the color to print this message as</value>
// 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);
/// <summary>
/// Prints an entry to the console window.
/// </summary>
/// <param name="level">the <see cref="Logger.Level"/> of the message</param>
/// <param name="time">the <see cref="DateTime"/> the message was recorded at</param>
/// <param name="logName">the name of the log that sent the message</param>
/// <param name="message">the message to print</param>
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
{
/// <summary>
/// Prints a pretty message to the console.
/// </summary>
public class ColoredConsolePrinter : LogPrinter
{
private Logger.LogLevel filter = Logger.LogLevel.All;
/// <summary>
/// A filter for this specific printer.
/// </summary>
/// <value>the filter to apply to this printer</value>
public override Logger.LogLevel Filter { get => filter; set => filter = value; }
/// <summary>
/// The color to print messages as.
/// </summary>
/// <value>the color to print this message as</value>
// 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;
}
/// <summary>
/// Prints an entry to the console window.
/// </summary>
/// <param name="level">the <see cref="Logger.Level"/> of the message</param>
/// <param name="time">the <see cref="DateTime"/> the message was recorded at</param>
/// <param name="logName">the name of the log that sent the message</param>
/// <param name="message">the message to print</param>
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);
}
}

+ 169
- 157
IPA.Loader/Logging/Printers/GZFilePrinter.cs View File

@ -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
{
/// <summary>
/// A <see cref="LogPrinter"/> abstract class that provides the utilities to write to a GZip file.
/// </summary>
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;
/// <summary>
/// The <see cref="StreamWriter"/> that writes to the GZip file.
/// </summary>
/// <value>the writer to the underlying filestream</value>
protected StreamWriter FileWriter;
private FileStream fstream;
/// <summary>
/// Gets the <see cref="FileInfo"/> for the file to write to.
/// </summary>
/// <returns>the file to write to</returns>
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();
}
/// <summary>
/// Called at the start of any print session.
/// </summary>
public sealed override void StartPrint()
{
InitLog();
fstream = fileInfo.Open(FileMode.Append, FileAccess.Write);
FileWriter = new StreamWriter(fstream, new UTF8Encoding(false));
}
/// <summary>
/// Called at the end of any print session.
/// </summary>
public sealed override void EndPrint()
{
FileWriter.Flush();
fstream.Flush();
FileWriter.Dispose();
fstream.Dispose();
FileWriter = null;
fstream = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the file printer.
/// </summary>
/// <param name="disposing">does nothing</param>
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
{
/// <summary>
/// A <see cref="LogPrinter"/> abstract class that provides the utilities to write to a GZip file.
/// </summary>
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;
/// <summary>
/// The <see cref="StreamWriter"/> that writes to the GZip file.
/// </summary>
/// <value>the writer to the underlying filestream</value>
protected StreamWriter? FileWriter;
private FileStream? fstream;
/// <summary>
/// Gets the <see cref="FileInfo"/> for the file to write to.
/// </summary>
/// <returns>the file to write to</returns>
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);
}
}
/// <summary>
/// Called at the start of any print session.
/// </summary>
public sealed override void StartPrint()
{
InitLog();
fstream = fileInfo.Open(FileMode.Append, FileAccess.Write);
FileWriter = new StreamWriter(fstream, new UTF8Encoding(false));
}
/// <summary>
/// Called at the end of any print session.
/// </summary>
public sealed override void EndPrint()
{
FileWriter?.Flush();
fstream?.Flush();
FileWriter?.Dispose();
fstream?.Dispose();
FileWriter = null;
fstream = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the file printer.
/// </summary>
/// <param name="disposing">does nothing</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
FileWriter?.Flush();
fstream?.Flush();
FileWriter?.Close();
fstream?.Close();
FileWriter?.Dispose();
fstream?.Dispose();
}
}
}
}

+ 8
- 5
IPA.Loader/Logging/StandardLogger.cs View File

@ -25,10 +25,7 @@ namespace IPA.Logging
/// </remarks>
public class StandardLogger : Logger
{
private static readonly List<LogPrinter> defaultPrinters = new()
{
new GlobalLogFilePrinter()
};
private static readonly List<LogPrinter> defaultPrinters = new();
static StandardLogger()
{
@ -115,6 +112,7 @@ namespace IPA.Logging
private readonly Dictionary<string, StandardLogger> children = new();
private static bool addedFilePrinter = false;
/// <summary>
/// Configures internal debug settings based on the config passed in.
/// </summary>
@ -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()
};
}
}
}

+ 51
- 21
IPA.Loader/Logging/StdoutInterceptor.cs View File

@ -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<CodeInstruction> PatchGetForegroundColor(IEnumerable<CodeInstruction> _)
{
@ -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);
};
}
}
}

+ 16
- 0
IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs View File

@ -109,4 +109,20 @@ namespace IPA
{
EdgeLifecycleType IEdgeLifecycleAttribute.Type => EdgeLifecycleType.Disable;
}
/// <summary>
/// Indicates that the applied plugin class does not need <see cref="OnEnableAttribute"/> or
/// <see cref="OnDisableAttribute"/> methods.
/// </summary>
/// <remarks>
/// This is typically only the case when some other utility mod handles their lifecycle for
/// them, such as with SiraUtil and Zenject.
/// </remarks>
/// <seealso cref="OnEnableAttribute"/>
/// <seealso cref="OnDisableAttribute"/>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class NoEnableDisableAttribute : Attribute
{
}
}

+ 3
- 1
IPA.Loader/Properties/AssemblyInfo.cs View File

@ -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")]


+ 323
- 270
IPA.Loader/Utilities/AlmostVersion.cs View File

@ -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
{
/// <summary>
/// A type that wraps <see cref="Version"/> so that the string of the version is stored when the string is
/// not a valid <see cref="Version"/>.
/// </summary>
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version>
{
/// <summary>
/// Represents a storage type of either parsed <see cref="Version"/> object or raw <see cref="String"/>.
/// </summary>
public enum StoredAs
{
/// <summary>
/// The version was stored as a <see cref="Version"/>.
/// </summary>
SemVer,
/// <summary>
/// The version was stored as a <see cref="String"/>.
/// </summary>
String
}
/// <summary>
/// Creates a new <see cref="AlmostVersion"/> with the version string provided in <paramref name="vertext"/>.
/// </summary>
/// <param name="vertext">the version string to store</param>
public AlmostVersion(string vertext)
{
if (!TryParseFrom(vertext, StoredAs.SemVer))
TryParseFrom(vertext, StoredAs.String);
}
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the <see cref="Version"/> provided in <paramref name="ver"/>.
/// </summary>
/// <param name="ver">the <see cref="Version"/> to store</param>
public AlmostVersion(Version ver)
{
SemverValue = ver;
StorageMode = StoredAs.SemVer;
}
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored using
/// the storage mode specified in <paramref name="mode"/>.
/// </summary>
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param>
/// <param name="mode">the storage mode to store the version in</param>
public AlmostVersion(string vertext, StoredAs mode)
{
if (!TryParseFrom(vertext, mode))
throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!");
}
/// <summary>
/// Creates a new <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored the
/// same way as the <see cref="AlmostVersion"/> passed in <paramref name="copyMode"/>.
/// </summary>
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param>
/// <param name="copyMode">an <see cref="AlmostVersion"/> to copy the storage mode of</param>
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;
}
}
/// <summary>
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="string"/>.
/// </summary>
/// <value>the stored value as a <see cref="string"/>, or <see langword="null"/> if not stored as a string.</value>
public string StringValue { get; private set; } = null;
/// <summary>
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="Version"/>.
/// </summary>
/// <value>the stored value as a <see cref="Version"/>, or <see langword="null"/> if not stored as a version.</value>
public Version SemverValue { get; private set; } = null;
/// <summary>
/// The way the value is stored, whether it be as a <see cref="Version"/> or a <see cref="string"/>.
/// </summary>
/// <value>the storage mode used to store this value</value>
public StoredAs StorageMode { get; private set; }
// can I just <inheritdoc /> this?
/// <summary>
/// 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 <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString"/>.
/// </summary>
/// <returns>a string representation of the current version</returns>
/// <seealso cref="object.ToString"/>
public override string ToString() =>
StorageMode == StoredAs.SemVer ? SemverValue.ToString() : StringValue;
/// <summary>
/// Compares <see langword="this"/> to the <see cref="AlmostVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>
/// or <see cref="string.CompareTo(string)"/>, depending on the current store.
/// </summary>
/// <remarks>
/// The storage methods of the two objects must be the same, or this will throw an <see cref="InvalidOperationException"/>.
/// </remarks>
/// <param name="other">the <see cref="AlmostVersion"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(Version)"/>
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);
}
/// <summary>
/// Compares <see langword="this"/> to the <see cref="Version"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>.
/// </summary>
/// <remarks>
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will
/// be thrown.
/// </remarks>
/// <param name="other">the <see cref="Version"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(AlmostVersion)"/>
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);
}
/// <summary>
/// Performs a strict equality check between <see langword="this"/> and <paramref name="obj"/>.
/// </summary>
/// <remarks>
/// This may return <see langword="false"/> where <see cref="operator ==(AlmostVersion, AlmostVersion)"/> returns <see langword="true"/>
/// </remarks>
/// <param name="obj">the object to compare to</param>
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="object.Equals(object)"/>
public override bool Equals(object obj)
{
return obj is AlmostVersion version &&
SemverValue == version.SemverValue &&
StringValue == version.StringValue &&
StorageMode == version.StorageMode;
}
/// <summary>
/// Default generated hash code function generated by VS.
/// </summary>
/// <returns>a value unique to each object, except those that are considered equal by <see cref="Equals(object)"/></returns>
/// <seealso cref="object.GetHashCode"/>
public override int GetHashCode()
{
var hashCode = -126402897;
hashCode = hashCode * -1521134295 + EqualityComparer<Version>.Default.GetHashCode(SemverValue);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(StringValue);
hashCode = hashCode * -1521134295 + StorageMode.GetHashCode();
return hashCode;
}
/// <summary>
/// Compares two versions, only taking into account the numeric part of the version if they are stored as <see cref="Version"/>s,
/// or strict equality if they are stored as <see cref="string"/>s.
/// </summary>
/// <remarks>
/// This is a looser equality than <see cref="Equals(object)"/>, meaning that this may return <see langword="true"/> where <see cref="Equals(object)"/>
/// does not.
/// </remarks>
/// <param name="l">the first value to compare</param>
/// <param name="r">the second value to compare</param>
/// <returns><see langword="true"/> if they are mostly equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="Equals(object)"/>
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;
}
/// <summary>
/// The opposite of <see cref="operator ==(AlmostVersion, AlmostVersion)"/>. Equivalent to <c>!(l == r)</c>.
/// </summary>
/// <param name="l">the first value to compare</param>
/// <param name="r">the second value to compare</param>
/// <returns><see langword="true"/> if they are not mostly equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="operator ==(AlmostVersion, AlmostVersion)"/>
public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r);
// implicitly convertible from Version
/// <summary>
/// Implicitly converts a <see cref="Version"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(Version)"/>.
/// </summary>
/// <param name="ver">the <see cref="Version"/> to convert</param>
/// <seealso cref="AlmostVersion(Version)"/>
public static implicit operator AlmostVersion(Version ver) => new AlmostVersion(ver);
// implicitly convertible to Version
/// <summary>
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="Version"/>, if applicable, using <see cref="SemverValue"/>.
/// If not applicable, returns <see langword="null"/>
/// </summary>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="Version"/></param>
/// <seealso cref="SemverValue"/>
public static implicit operator Version(AlmostVersion av) => av?.SemverValue;
}
/// <summary>
/// A <see cref="ValueConverter{T}"/> for <see cref="AlmostVersion"/>s.
/// </summary>
public sealed class AlmostVersionConverter : ValueConverter<AlmostVersion>
{
/// <summary>
/// Converts a <see cref="Text"/> node into an <see cref="AlmostVersion"/>.
/// </summary>
/// <param name="value">the <see cref="Text"/> node to convert</param>
/// <param name="parent">the owner of the new object</param>
/// <returns></returns>
public override AlmostVersion FromValue(Value value, object parent)
=> new AlmostVersion(Converter<string>.Default.FromValue(value, parent));
/// <summary>
/// Converts an <see cref="AlmostVersion"/> to a <see cref="Text"/> node.
/// </summary>
/// <param name="obj">the <see cref="AlmostVersion"/> to convert</param>
/// <param name="parent">the parent of <paramref name="obj"/></param>
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns>
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
{
/// <summary>
/// A type that wraps <see cref="Version"/> so that the string of the version is stored when the string is
/// not a valid <see cref="Version"/>.
/// </summary>
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version>,
#pragma warning disable CS0618 // Type or member is obsolete
IComparable<SVersion>
#pragma warning restore CS0618 // Type or member is obsolete
{
/// <summary>
/// Represents a storage type of either parsed <see cref="Version"/> object or raw <see cref="String"/>.
/// </summary>
public enum StoredAs
{
/// <summary>
/// The version was stored as a <see cref="SVersion"/>.
/// </summary>
SemVer,
/// <summary>
/// The version was stored as a <see cref="String"/>.
/// </summary>
String
}
/// <summary>
/// Creates a new <see cref="AlmostVersion"/> with the version string provided in <paramref name="vertext"/>.
/// </summary>
/// <param name="vertext">the version string to store</param>
public AlmostVersion(string vertext)
{
if (!TryParseFrom(vertext, StoredAs.SemVer))
_ = TryParseFrom(vertext, StoredAs.String);
}
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the <see cref="Version"/> provided in <paramref name="ver"/>.
/// </summary>
/// <param name="ver">the <see cref="Version"/> to store</param>
public AlmostVersion(Version ver)
{
SemverValue = ver;
StorageMode = StoredAs.SemVer;
}
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the <see cref="SVersion"/> provided in <paramref name="ver"/>.
/// </summary>
/// <param name="ver">the <see cref="SVersion"/> to store</param>
[Obsolete("Use Hive.Versioning.Version constructor instead.")]
public AlmostVersion(SVersion ver) : this(ver?.UnderlyingVersion ?? throw new ArgumentNullException(nameof(ver))) { }
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored using
/// the storage mode specified in <paramref name="mode"/>.
/// </summary>
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param>
/// <param name="mode">the storage mode to store the version in</param>
public AlmostVersion(string vertext, StoredAs mode)
{
if (!TryParseFrom(vertext, mode))
throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!");
}
/// <summary>
/// Creates a new <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored the
/// same way as the <see cref="AlmostVersion"/> passed in <paramref name="copyMode"/>.
/// </summary>
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param>
/// <param name="copyMode">an <see cref="AlmostVersion"/> to copy the storage mode of</param>
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;
}
}
/// <summary>
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="string"/>.
/// </summary>
/// <value>the stored value as a <see cref="string"/>, or <see langword="null"/> if not stored as a string.</value>
public string? StringValue { get; private set; }
/// <summary>
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="Version"/>.
/// </summary>
/// <value>the stored value as a <see cref="Version"/>, or <see langword="null"/> if not stored as a version.</value>
public Version? SemverValue { get; private set; }
/// <summary>
/// The way the value is stored, whether it be as a <see cref="Version"/> or a <see cref="string"/>.
/// </summary>
/// <value>the storage mode used to store this value</value>
public StoredAs StorageMode { get; private set; }
/// <summary>
/// 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 <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString()"/>.
/// </summary>
/// <returns>a string representation of the current version</returns>
/// <seealso cref="object.ToString"/>
public override string ToString() =>
StorageMode == StoredAs.SemVer ? SemverValue!.ToString() : StringValue!;
/// <summary>
/// Compares <see langword="this"/> to the <see cref="AlmostVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>
/// or <see cref="string.CompareTo(string)"/>, depending on the current store.
/// </summary>
/// <remarks>
/// The storage methods of the two objects must be the same, or this will throw an <see cref="InvalidOperationException"/>.
/// </remarks>
/// <param name="other">the <see cref="AlmostVersion"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(Version)"/>
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);
}
/// <summary>
/// Compares <see langword="this"/> to the <see cref="Version"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>.
/// </summary>
/// <remarks>
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will
/// be thrown.
/// </remarks>
/// <param name="other">the <see cref="Version"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(AlmostVersion)"/>
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);
}
/// <summary>
/// Compares <see langword="this"/> to the <see cref="SVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>.
/// </summary>
/// <remarks>
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will
/// be thrown.
/// </remarks>
/// <param name="other">the <see cref="SVersion"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(AlmostVersion)"/>
[Obsolete("Use the Hive.Versioning.Version overload instead.")]
public int CompareTo(SVersion other) => CompareTo(other.UnderlyingVersion);
/// <summary>
/// Performs a strict equality check between <see langword="this"/> and <paramref name="obj"/>.
/// </summary>
/// <remarks>
/// This may return <see langword="false"/> where <see cref="operator ==(AlmostVersion, AlmostVersion)"/> returns <see langword="true"/>
/// </remarks>
/// <param name="obj">the object to compare to</param>
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="object.Equals(object)"/>
public override bool Equals(object? obj)
{
return obj is AlmostVersion version &&
SemverValue == version.SemverValue &&
StringValue == version.StringValue &&
StorageMode == version.StorageMode;
}
/// <summary>
/// Default generated hash code function generated by VS.
/// </summary>
/// <returns>a value unique to each object, except those that are considered equal by <see cref="Equals(object)"/></returns>
/// <seealso cref="object.GetHashCode"/>
public override int GetHashCode()
{
var hashCode = -126402897;
hashCode = (hashCode * -1521134295) + EqualityComparer<Version?>.Default.GetHashCode(SemverValue);
hashCode = (hashCode * -1521134295) + EqualityComparer<string?>.Default.GetHashCode(StringValue);
hashCode = (hashCode * -1521134295) + StorageMode.GetHashCode();
return hashCode;
}
/// <summary>
/// Compares two versions, only taking into account the numeric part of the version if they are stored as <see cref="Version"/>s,
/// or strict equality if they are stored as <see cref="string"/>s.
/// </summary>
/// <remarks>
/// This is a looser equality than <see cref="Equals(object)"/>, meaning that this may return <see langword="true"/> where <see cref="Equals(object)"/>
/// does not.
/// </remarks>
/// <param name="l">the first value to compare</param>
/// <param name="r">the second value to compare</param>
/// <returns><see langword="true"/> if they are mostly equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="Equals(object)"/>
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;
}
/// <summary>
/// The opposite of <see cref="operator ==(AlmostVersion, AlmostVersion)"/>. Equivalent to <c>!(l == r)</c>.
/// </summary>
/// <param name="l">the first value to compare</param>
/// <param name="r">the second value to compare</param>
/// <returns><see langword="true"/> if they are not mostly equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="operator ==(AlmostVersion, AlmostVersion)"/>
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
/// <summary>
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>.
/// </summary>
/// <param name="ver">the <see cref="SVersion"/> to convert</param>
/// <seealso cref="AlmostVersion(SVersion)"/>
[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
/// <summary>
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>.
/// If not applicable, returns <see langword="null"/>
/// </summary>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param>
/// <seealso cref="SemverValue"/>
[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
/// <summary>
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>.
/// </summary>
/// <param name="ver">the <see cref="SVersion"/> to convert</param>
/// <seealso cref="AlmostVersion(SVersion)"/>
public static implicit operator AlmostVersion?(Version? ver) => ver is null ? null : new(ver);
// implicitly convertible to Version
/// <summary>
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>.
/// If not applicable, returns <see langword="null"/>
/// </summary>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param>
/// <seealso cref="SemverValue"/>
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;
}
/// <summary>
/// A <see cref="ValueConverter{T}"/> for <see cref="AlmostVersion"/>s.
/// </summary>
public sealed class AlmostVersionConverter : ValueConverter<AlmostVersion>
{
/// <summary>
/// Converts a <see cref="Text"/> node into an <see cref="AlmostVersion"/>.
/// </summary>
/// <param name="value">the <see cref="Text"/> node to convert</param>
/// <param name="parent">the owner of the new object</param>
/// <returns></returns>
public override AlmostVersion? FromValue(Value? value, object parent)
=> Converter<string>.Default.FromValue(value, parent) switch
{
{ } v => new(v),
_ => null
};
/// <summary>
/// Converts an <see cref="AlmostVersion"/> to a <see cref="Text"/> node.
/// </summary>
/// <param name="obj">the <see cref="AlmostVersion"/> to convert</param>
/// <param name="parent">the parent of <paramref name="obj"/></param>
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns>
public override Value? ToValue(AlmostVersion? obj, object parent)
=> Value.From(obj?.ToString());
}
}

+ 4
- 4
IPA.Loader/Utilities/Async/Coroutines.cs View File

@ -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<VoidStruct>(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<IEnumerator> enumerators = new Stack<IEnumerator>(2);
private readonly Stack<IEnumerator> 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;
}
}


+ 28
- 10
IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs View File

@ -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.
/// </summary>
/// <value>a scheduler that is managed by BSIPA</value>
public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
public static new UnityMainThreadTaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
/// <summary>
/// Gets a factory for creating tasks on <see cref="Default"/>.
/// </summary>
/// <value>a factory for creating tasks on the default scheduler</value>
public static TaskFactory Factory { get; } = new TaskFactory(Default);
private readonly ConcurrentQueue<QueueItem> tasks = new ConcurrentQueue<QueueItem>();
private static readonly ConditionalWeakTable<Task, QueueItem> itemTable = new ConditionalWeakTable<Task, QueueItem>();
private readonly ConcurrentQueue<QueueItem> tasks = new();
private static readonly ConditionalWeakTable<Task, QueueItem> itemTable = new();
private class QueueItem : IEquatable<Task>, IEquatable<QueueItem>
{
@ -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
/// <returns>nothing</returns>
/// <exception cref="NotSupportedException">Always.</exception>
protected override IEnumerable<Task> GetScheduledTasks()
=> tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).ToArray();
=> tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).NonNull().ToArray();
/// <summary>
/// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must</i> be
@ -198,6 +209,13 @@ namespace IPA.Utilities.Async
tasks.Enqueue(item);
}
internal void QueueAction(Action action)
{
ThrowIfDisposed();
tasks.Enqueue(new(action));
}
/// <summary>
/// Runs the task inline if the current thread is the Unity main thread.
/// </summary>


+ 9
- 12
IPA.Loader/Utilities/CriticalSection.cs View File

@ -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


+ 2
- 0
IPA.Loader/Utilities/EnumerableExtensions.cs View File

@ -13,6 +13,7 @@ namespace IPA.Utilities
/// </summary>
public static class EnumerableExtensions
{
/*
/// <summary>
/// Adds a value to the beginning of the sequence.
/// </summary>
@ -164,6 +165,7 @@ namespace IPA.Utilities
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
*/
/// <summary>
/// LINQ-style extension method that filters <see langword="null"/> elements out of an enumeration.


+ 13
- 0
IPA.Loader/Utilities/ReflectionUtil.cs View File

@ -130,6 +130,19 @@ namespace IPA.Utilities
return copy;
}
/// <summary>
/// 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
/// </summary>
/// <param name="propertyName">Name of the property</param>
/// <returns>Name of the backing field</returns>
/// <remarks>
/// 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 <a href="https://github.com/dotnet/roslyn/blob/1497e87d967c5b7797edb5f782131508607139e5/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs#L24-L28">this link</a> for more info.
/// </remarks>
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);


+ 82
- 18
IPA.Loader/Utilities/UnityGame.cs View File

@ -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
/// </summary>
public static class UnityGame
{
private static AlmostVersion _gameVersion;
private static AlmostVersion? _gameVersion;
/// <summary>
/// Provides the current game version.
/// </summary>
/// <value>the SemVer version of the game</value>
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;
/// <summary>
/// Checks if the currently running code is running on the Unity main thread.
/// </summary>
/// <value><see langword="true"/> if the curent thread is the Unity main thread, <see langword="false"/> otherwise</value>
public static bool OnMainThread => Thread.CurrentThread.ManagedThreadId == mainThread?.ManagedThreadId;
public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId;
/// <summary>
/// Asynchronously switches the current execution context to the Unity main thread.
/// </summary>
/// <returns>An awaitable which causes any following code to execute on the main thread.</returns>
public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default;
internal static void SetMainThread()
=> mainThread = Thread.CurrentThread;
@ -120,9 +129,9 @@ namespace IPA.Utilities
/// This only gives a
/// </remarks>
/// <value>the type of release this is</value>
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;
/// <summary>
/// Gets the path to the game's install directory.
/// </summary>
@ -165,4 +174,59 @@ namespace IPA.Utilities
&& installDirInfo.Parent?.Parent?.Name == "steamapps";
}
}
/// <summary>
/// An awaitable which, when awaited, switches the current context to the Unity main thread.
/// </summary>
/// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
Justification = "This type should never be compared.")]
public struct SwitchToUnityMainThreadAwaitable
{
/// <summary>
/// Gets the awaiter for this awaitable.
/// </summary>
/// <returns>The awaiter for this awaitable.</returns>
public SwitchToUnityMainThreadAwaiter GetAwaiter() => default;
}
/// <summary>
/// An awaiter which, when awaited, switches the current context to the Unity main thread.
/// </summary>
/// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
[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!)();
/// <summary>
/// Gets whether or not this awaiter is completed.
/// </summary>
public bool IsCompleted => UnityGame.OnMainThread;
/// <summary>
/// Gets the result of this awaiter.
/// </summary>
public void GetResult() { }
/// <summary>
/// Registers a continuation to be called when this awaiter finishes.
/// </summary>
/// <param name="continuation">The continuation.</param>
public void OnCompleted(Action continuation)
{
var ec = ExecutionContext.Capture();
UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation));
}
/// <summary>
/// Registers a continuation to be called when this awaiter finishes, without capturing the execution context.
/// </summary>
/// <param name="continuation">The continuation.</param>
public void UnsafeOnCompleted(Action continuation)
{
UnityMainThreadTaskScheduler.Default.QueueAction(continuation);
}
}
}

+ 68
- 23
IPA.Loader/Utilities/Utils.cs View File

@ -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
/// <returns>the corresponding byte array</returns>
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
/// <returns>the hex form of the array</returns>
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
/// <returns>a path to get from <paramref name="folder"/> to <paramref name="file"/></returns>
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
/// <param name="appendFileName">the filename of the file to append together</param>
/// <param name="onCopyException">a delegate called when there is an error copying. Return true to keep going.</param>
public static void CopyAll(DirectoryInfo source, DirectoryInfo target, string appendFileName = "",
Func<Exception, FileInfo, bool> onCopyException = null)
Func<Exception, FileInfo, bool>? 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
/// <param name="l">the left value</param>
/// <param name="r">the right value</param>
/// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
[Obsolete("Use Hive.Versioning.Version overload instead.")]
public static int VersionCompareNoPrerelease(SemVer.Version l, SemVer.Version r)
=> VersionCompareNoPrerelease(l?.UnderlyingVersion!, r?.UnderlyingVersion!);
/// <summary>
/// Compares a pair of <see cref="Version"/>s ignoring both the prerelease and build fields.
/// </summary>
/// <param name="l">the left value</param>
/// <param name="r">the right value</param>
/// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
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;
}
/// <summary>
/// An object used to manage scope guards.
/// </summary>
@ -197,6 +227,10 @@ namespace IPA.Utilities
/// </code>
/// </example>
/// <seealso cref="ScopeGuard(Action)"/>
[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
/// </code>
/// </example>
public static ScopeGuardObject ScopeGuard(Action action)
=> new ScopeGuardObject(action);
=> new(action);
/// <summary>
/// Deconstructs a <see cref="KeyValuePair{TKey, TValue}"/> as its key and value.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="kvp">The <see cref="KeyValuePair{TKey, TValue}"/> to deconstruct.</param>
/// <param name="key">The key in <paramref name="kvp"/>.</param>
/// <param name="value">The value in <paramref name="kvp"/>.</param>
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> 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);


+ 1
- 1
IPA/IPA.csproj View File

@ -4,7 +4,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
<RuntimeIdentifiers>win7-x64;win7-x86;linux-x64</RuntimeIdentifiers>
<RootNamespace>IPA</RootNamespace>
<AssemblyName>IPA</AssemblyName>


+ 1
- 1
IPA/Program.cs View File

@ -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!;


+ 0
- 10
IPA/_Attributes.cs View File

@ -1,10 +0,0 @@
#if NET461
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class DoesNotReturnAttribute : Attribute
{
public DoesNotReturnAttribute() { }
}
}
#endif

BIN
Libs/netstandard.dll View File


+ 2
- 2
README.md View File

@ -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/)

BIN
Refs/UnityEngine.CoreModule.Net4.dll View File


BIN
Refs/UnityEngine.CoreModule.net3.dll View File


BIN
Refs/UnityEngine.Net4.dll View File


BIN
Refs/UnityEngine.UnityWebRequestModule.Net4.dll View File


BIN
Refs/UnityEngine.UnityWebRequestModule.net3.dll View File


BIN
Refs/UnityEngine.net3.dll View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save