Browse Source

Merge branch 'master' into Plugin-subfolder-support

pull/102/head
Arimodu 7 months ago
committed by GitHub
parent
commit
8b3842a754
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 946 additions and 768 deletions
  1. +246
    -0
      .editorconfig
  2. +1
    -0
      BSIPA.sln
  3. +2
    -2
      Common.props
  4. +9
    -2
      Doorstop/Proxy/main.c
  5. +2
    -0
      Doorstop/Proxy/mono.h
  6. +4
    -3
      IPA.Injector/GameVersionEarly.cs
  7. +19
    -11
      IPA.Injector/IPA.Injector.csproj
  8. +28
    -31
      IPA.Injector/Injector.cs
  9. +0
    -219
      IPA.Injector/Virtualizer.cs
  10. +145
    -150
      IPA.Loader/Config/Data/Value.cs
  11. +40
    -7
      IPA.Loader/Config/SelfConfig.cs
  12. +1
    -1
      IPA.Loader/Config/Stores/Converters.cs
  13. +1
    -0
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs
  14. +1
    -0
      IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs
  15. +1
    -1
      IPA.Loader/IPA.Loader.csproj
  16. +0
    -44
      IPA.Loader/Loader/HarmonyProtector.cs
  17. +13
    -4
      IPA.Loader/Loader/PluginLoader.cs
  18. +4
    -2
      IPA.Loader/Loader/PluginManager.cs
  19. +1
    -1
      IPA.Loader/Loader/manifest.json
  20. +31
    -53
      IPA.Loader/Logging/ConsoleWindow.cs
  21. +39
    -38
      IPA.Loader/Logging/LogPrinter.cs
  22. +159
    -116
      IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs
  23. +34
    -22
      IPA.Loader/Logging/Printers/GZFilePrinter.cs
  24. +8
    -5
      IPA.Loader/Logging/StandardLogger.cs
  25. +1
    -1
      IPA.Loader/Logging/StdoutInterceptor.cs
  26. +1
    -1
      IPA.Loader/Utilities/AlmostVersion.cs
  27. +4
    -4
      IPA.Loader/Utilities/Async/Coroutines.cs
  28. +28
    -10
      IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
  29. +8
    -11
      IPA.Loader/Utilities/CriticalSection.cs
  30. +13
    -0
      IPA.Loader/Utilities/ReflectionUtil.cs
  31. +73
    -9
      IPA.Loader/Utilities/UnityGame.cs
  32. +1
    -1
      IPA/Program.cs
  33. +2
    -2
      README.md
  34. BIN
      Refs/UnityEngine.CoreModule.Net4.dll
  35. BIN
      Refs/UnityEngine.Net4.dll
  36. BIN
      Refs/UnityEngine.UnityWebRequestModule.Net4.dll
  37. +13
    -8
      docs/articles/command-line.md
  38. +10
    -6
      docs/articles/start-user.md
  39. +2
    -2
      docs/build.ps1
  40. +1
    -1
      docs/docfx.json

+ 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

+ 1
- 0
BSIPA.sln View File

@ -18,6 +18,7 @@ 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


+ 2
- 2
Common.props View File

@ -14,11 +14,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" />
<PackageReference Include="Nullable" Version="1.3.0">
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="IsExternalInit" Version="1.0.2">
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>


+ 9
- 2
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


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


+ 19
- 11
IPA.Injector/IPA.Injector.csproj View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Common.props" />
<PropertyGroup>
<TargetFrameworks>net472</TargetFrameworks>
<TargetFramework>net472</TargetFramework>
<RootNamespace>IPA.Injector</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
@ -21,12 +21,12 @@
<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)' == 'net472' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net4.dll</HintPath>
@ -37,7 +37,7 @@
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Content Include="..\Libs\I18N.Net4.dll">
<Link>Libraries\Mono\I18N.dll</Link>
@ -67,7 +67,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
<Content Include="..\Libs\I18N.Net3.dll">
<Link>Libraries\Mono\I18N.dll</Link>
@ -82,14 +82,22 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<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>
@ -104,7 +112,7 @@
</ItemGroup>
<Move SourceFiles="@(SystemFiles)" DestinationFolder="$(OutputPath)Data\Managed" />
<RemoveDir Directories="$(OutputPath)Libraries\Mono" />
<Delete Files="@(OldLibFiles)" />
<RemoveDir Directories="$(OutputPath)Libs" />
<ItemGroup>


+ 28
- 31
IPA.Injector/Injector.cs View File

@ -45,8 +45,8 @@ namespace IPA.Injector
_ = args;
try
{
if (Environment.GetCommandLineArgs().Contains("--verbose"))
WinConsole.Initialize();
var arguments = Environment.GetCommandLineArgs();
MaybeInitializeConsole(arguments);
SetupLibraryLoading();
@ -55,8 +55,8 @@ namespace IPA.Injector
// this is weird, but it prevents Mono from having issues loading the type.
// IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS
var unused = StandardLogger.PrintFilter;
#region // Above hack explaination
/*
#region // Above hack explanation
/*
* Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger
* is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to
* the above static property (or maybe any, but I don't really know) it behaves as expected and works fine.
@ -65,7 +65,7 @@ namespace IPA.Injector
Default.Debug("Initializing logger");
SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs());
SelfConfig.ReadCommandLine(arguments);
SelfConfig.Load();
DisabledConfig.Load();
@ -80,11 +80,13 @@ namespace IPA.Injector
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();
@ -100,6 +102,25 @@ namespace IPA.Injector
}
}
private static void MaybeInitializeConsole(string[] arguments)
{
var i = 0;
while (i < arguments.Length)
{
if (arguments[i++] == "--verbose")
{
if (i == arguments.Length)
{
WinConsole.Initialize(WinConsole.AttachParent);
return;
}
WinConsole.Initialize(int.TryParse(arguments[i], out int processId) ? processId : WinConsole.AttachParent);
return;
}
}
}
private static void EnsureDirectories()
{
string path;
@ -116,11 +137,6 @@ namespace IPA.Injector
Loader.LibLoader.Configure();
}
private static void InstallHarmonyProtections()
{ // proxy function to delay resolution
HarmonyProtectorProxy.ProtectNull();
}
private static void InstallBootstrapPatch()
{
var sw = Stopwatch.StartNew();
@ -238,9 +254,6 @@ namespace IPA.Injector
endPatchCoreModule:
#endregion Insert patch into UnityEngine.CoreModule.dll
Logging.Logger.Injector.Debug("Ensuring game assemblies are virtualized");
#region Virtualize game assemblies
bool isFirst = true;
foreach (var name in SelfConfig.GameAssemblies_)
{
@ -248,19 +261,6 @@ namespace IPA.Injector
using var execSec = CriticalSection.ExecuteSection();
try
{
Logging.Logger.Injector.Debug($"Virtualizing {name}");
using var ascModule = VirtualizedModule.Load(ascPath);
ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath));
}
catch (Exception e)
{
Logging.Logger.Injector.Error($"Could not virtualize {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
Logging.Logger.Injector.Error(e);
}
#if BeatSaber
if (isFirst)
{
@ -292,7 +292,6 @@ namespace IPA.Injector
}
#endif
}
#endregion
sw.Stop();
Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}");
@ -317,8 +316,6 @@ namespace IPA.Injector
// need to reinit streams singe Unity seems to redirect stdout
StdoutInterceptor.RedirectConsole();
InstallHarmonyProtections();
var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}


+ 0
- 219
IPA.Injector/Virtualizer.cs View File

@ -1,219 +0,0 @@
using Mono.Cecil;
using Mono.Cecil.Rocks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace IPA.Injector
{
internal class VirtualizedModule : IDisposable
{
private readonly FileInfo file;
private ModuleDefinition module;
public static VirtualizedModule Load(string engineFile)
{
return new VirtualizedModule(engineFile);
}
private VirtualizedModule(string assemblyFile)
{
file = new FileInfo(assemblyFile);
LoadModules();
}
private void LoadModules()
{
module = ModuleDefinition.ReadModule(file.FullName, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
}
public void Virtualize(AssemblyName selfName, Action beforeChangeCallback = null)
{
var changed = false;
var virtualize = true;
foreach (var r in module.AssemblyReferences)
{
if (r.Name == selfName.Name)
{
virtualize = false;
if (r.Version != selfName.Version)
{
r.Version = selfName.Version;
changed = true;
}
}
}
if (virtualize)
{
changed = true;
module.AssemblyReferences.Add(new AssemblyNameReference(selfName.Name, selfName.Version));
foreach (var type in module.Types)
{
VirtualizeType(type);
}
}
if (changed)
{
beforeChangeCallback?.Invoke();
module.Write(file.FullName);
}
}
private TypeReference inModreqRef;
// private TypeReference outModreqRef;
private void VirtualizeType(TypeDefinition type)
{
if(type.IsSealed)
{
// Unseal
type.IsSealed = false;
}
if (type.IsNestedPrivate)
{
type.IsNestedPrivate = false;
type.IsNestedPublic = true;
}
if (type.IsInterface) return;
if (type.IsAbstract) return;
// These two don't seem to work.
if (type.Name == "SceneControl" || type.Name == "ConfigUI") return;
// Take care of sub types
foreach (var subType in type.NestedTypes)
{
VirtualizeType(subType);
}
foreach (var method in type.Methods)
{
if (method.IsManaged
&& method.IsIL
&& !method.IsStatic
&& (!method.IsVirtual || method.IsFinal)
&& !method.IsAbstract
&& !method.IsAddOn
&& !method.IsConstructor
&& !method.IsSpecialName
&& !method.IsGenericInstance
&& !method.HasOverrides)
{
// fix In parameters to have the modreqs required by the compiler
foreach (var param in method.Parameters)
{
if (param.IsIn)
{
inModreqRef ??= module.ImportReference(typeof(System.Runtime.InteropServices.InAttribute));
param.ParameterType = AddModreqIfNotExist(param.ParameterType, inModreqRef);
}
// Breaks override methods if modreq is applied to `out` parameters
//if (param.IsOut)
//{
// outModreqRef ??= module.ImportReference(typeof(System.Runtime.InteropServices.OutAttribute));
// param.ParameterType = AddModreqIfNotExist(param.ParameterType, outModreqRef);
//}
}
method.IsVirtual = true;
method.IsFinal = false;
method.IsPublic = true;
method.IsPrivate = false;
method.IsNewSlot = true;
method.IsHideBySig = true;
}
}
foreach (var field in type.Fields)
{
if (field.IsPrivate) field.IsFamily = true;
}
}
private TypeReference AddModreqIfNotExist(TypeReference type, TypeReference mod)
{
var (element, opt, req) = GetDecomposedModifiers(type);
if (!req.Contains(mod))
{
req.Add(mod);
}
return BuildModifiedType(element, opt, req);
}
private (TypeReference Element, List<TypeReference> ModOpt, List<TypeReference> ModReq) GetDecomposedModifiers(TypeReference type)
{
var opt = new List<TypeReference>();
var req = new List<TypeReference>();
while (type is IModifierType modif)
{
if (type.IsOptionalModifier)
opt.Add(modif.ModifierType);
if (type.IsRequiredModifier)
req.Add(modif.ModifierType);
type = modif.ElementType;
}
return (type, opt, req);
}
private TypeReference BuildModifiedType(TypeReference type, IEnumerable<TypeReference> opt, IEnumerable<TypeReference> req)
{
foreach (var mod in req)
{
type = type.MakeRequiredModifierType(mod);
}
foreach (var mod in opt)
{
type = type.MakeOptionalModifierType(mod);
}
return type;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
module.Dispose();
}
disposedValue = true;
}
}
~VirtualizedModule()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

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

@ -1,151 +1,146 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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();
/// <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;
}
}
}
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;
}
}
}

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

@ -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,10 +78,18 @@ 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.2.2.0";
internal const string IPAVersion = "4.3.0.0";
// uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug,
// uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug,
// Debug.CondenseModLogs
internal static SelfConfig CommandLineValues = new();
@ -148,6 +162,11 @@ 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
@ -174,19 +193,33 @@ namespace IPA.Config
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>
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",
"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" };


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

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


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

@ -46,6 +46,7 @@ namespace IPA.Config.Stores
var endLabel = il.DefineLabel();
// TODO: when we have a nullable, we need to save to a local to call methods
if (member.IsNullable)
{
il.Emit(OpCodes.Dup);


+ 1
- 0
IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs View File

@ -112,6 +112,7 @@ namespace IPA.Config.Stores
{
il.Emit(OpCodes.Stloc, mapLocal);
// TODO: handle creating a nullable, when applicable
EmitLoad(il, member, thisarg);
il.Emit(OpCodes.Stloc, resultLocal);


+ 1
- 1
IPA.Loader/IPA.Loader.csproj View File

@ -50,7 +50,7 @@
<ItemGroup>
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" />
<!--<PackageReference Include="Lib.Harmony" Version="2.0.2" />-->
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<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>


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

@ -1,44 +0,0 @@
using HarmonyLib;
using System.Reflection;
namespace IPA.Loader
{
internal static class HarmonyProtectorProxy
{
public static void ProtectNull() => HarmonyProtector.Protect();
}
internal static class HarmonyProtector
{
private static Harmony instance;
private static Assembly selfAssem;
private static Assembly harmonyAssem;
public static void Protect(Harmony inst = null)
{
selfAssem = Assembly.GetExecutingAssembly();
harmonyAssem = typeof(Harmony).Assembly;
if (inst == null)
{
if (instance == null)
instance = new Harmony("BSIPA Safeguard");
inst = instance;
}
var target = typeof(PatchProcessor).GetMethod("Patch");
var patch = typeof(HarmonyProtector).GetMethod(nameof(PatchProcessor_Patch_Prefix), BindingFlags.NonPublic | BindingFlags.Static);
inst.Patch(target, new HarmonyMethod(patch));
}
private static bool PatchProcessor_Patch_Prefix(MethodBase ___original, out MethodInfo __result)
{
var asm = ___original.DeclaringType.Assembly;
__result = ___original as MethodInfo;
return !(asm.Equals(selfAssem) || asm.Equals(harmonyAssem));
}
}
}

+ 13
- 4
IPA.Loader/Loader/PluginLoader.cs View File

@ -104,7 +104,9 @@ namespace IPA.Loader
throw new InvalidOperationException()))
manifest = manifestReader.ReadToEnd();
selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest) ?? throw new NotSupportedException();
var manifestObj = JsonConvert.DeserializeObject<PluginManifest>(manifest);
selfMeta.Manifest = manifestObj ?? throw new InvalidOperationException("Deserialized manifest was null");
PluginsMetadata.Add(selfMeta);
SelfMeta = selfMeta;
@ -288,10 +290,17 @@ namespace IPA.Loader
IsSelf = false,
IsBare = true,
};
metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest)) ?? throw new NotSupportedException();
var manifestRelative = manifest.Replace(UnityGame.InstallPath, "").TrimStart(Path.DirectorySeparatorChar);
var manifestObj = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
if (manifestObj is null)
{
Logger.Loader.Error($"Bare manifest {manifestRelative} deserialized to null");
continue;
}
metadata.Manifest = manifestObj;
if (metadata.Manifest.Files.Length < 1)
Logger.Loader.Warn($"Bare manifest {manifestRelative} does not declare any files. " +


+ 4
- 2
IPA.Loader/Loader/PluginManager.cs View File

@ -199,7 +199,8 @@ namespace IPA.Loader
var res = TaskEx.WhenAll(exec.Dependents.Select(d => Disable(d, alreadyDisabled)))
.ContinueWith(t =>
{
if (t.IsFaulted) {
if (t.IsFaulted)
{
return TaskEx.WhenAll(t, TaskEx6.FromException(
new CannotRuntimeDisableException(exec.Executor.Metadata, "Dependents cannot be disabled for plugin")));
}
@ -208,7 +209,8 @@ namespace IPA.Loader
{
foreach (var feature in exec.Executor.Metadata.Features)
{
try {
try
{
feature.AfterDisable(exec.Executor.Metadata);
}
catch (Exception e)


+ 1
- 1
IPA.Loader/Loader/manifest.json View File

@ -7,7 +7,7 @@
],
"id": "BSIPA",
"name": "Beat Saber IPA",
"version": "4.2.2",
"version": "4.3.0",
"icon": "IPA.icon_white.png",
"features": {
"IPA.DefineFeature": [


+ 31
- 53
IPA.Loader/Logging/ConsoleWindow.cs View File

@ -22,14 +22,12 @@ namespace IPA.Logging
internal static bool IsInitialized;
public static void Initialize(bool alwaysCreateNewConsole = false)
public static void Initialize(int processId, bool alwaysCreateNewConsole = false)
{
bool consoleAttached = true;
if (alwaysCreateNewConsole
|| (AttachConsole(AttachParent) == 0
&& Marshal.GetLastWin32Error() != ErrorAccessDenied))
bool consoleAttached;
if (alwaysCreateNewConsole || !(consoleAttached = AttachConsole(processId)))
{
consoleAttached = AllocConsole() != 0;
consoleAttached = AllocConsole();
}
if (consoleAttached)
@ -39,7 +37,7 @@ namespace IPA.Logging
}
}
public static void InitializeStreams()
private static void InitializeStreams()
{
InitializeOutStream();
InitializeInStream();
@ -62,7 +60,7 @@ namespace IPA.Logging
if (!SetConsoleMode(handle, mode))
{
UseVTEscapes = false;
Console.Error.WriteLine("Could not enable VT100 escape code processing (maybe you're running an old Windows?): " +
Console.Error.WriteLine("Could not enable VT100 escape code processing (maybe you're running an old Windows?): " +
new Win32Exception(Marshal.GetLastWin32Error()).Message);
}
}
@ -87,18 +85,11 @@ namespace IPA.Logging
private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode,
FileAccess dotNetFileAccess, out SafeFileHandle handle)
{
var file = new SafeFileHandle(CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OpenExisting, FileAttributeNormal, IntPtr.Zero), true);
var file = new SafeFileHandle(CreateFile(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OpenExisting, FileAttributeNormal, IntPtr.Zero), true);
if (!file.IsInvalid)
{
handle = file;
#if NET4
var fs = new FileStream(file, dotNetFileAccess);
#elif NET3
#pragma warning disable CS0618
// this is marked obsolete, and shouldn't need to be used, but the constructor used in .NET 4 doesn't exist in Unity's mscorlib.dll
var fs = new FileStream(file.DangerousGetHandle(), dotNetFileAccess);
#pragma warning restore
#endif
return fs;
}
@ -106,43 +97,31 @@ namespace IPA.Logging
return null;
}
#region Win API Functions and Constants
[DllImport("kernel32.dll",
EntryPoint = "AllocConsole",
SetLastError = true,
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall)]
private static extern int AllocConsole();
[DllImport("kernel32.dll",
EntryPoint = "AttachConsole",
SetLastError = true,
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall)]
private static extern uint AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll",
EntryPoint = "CreateFileW",
SetLastError = true,
CharSet = CharSet.Unicode,
CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr CreateFileW(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile
);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
#region Win API Functions and Constants
[DllImport("kernel32.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode,
IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[DllImport("kernel32.dll", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[DllImport("kernel32.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr GetStdHandle(int nStdHandle);
private const uint EnableVTProcessing = 0x0004;
@ -153,10 +132,9 @@ namespace IPA.Logging
private const uint FileShareWrite = 0x00000002;
private const uint OpenExisting = 0x00000003;
private const uint FileAttributeNormal = 0x80;
private const uint ErrorAccessDenied = 5;
private const uint AttachParent = 0xFFFFFFFF;
#endregion
internal const int AttachParent = -1;
#endregion
}
}

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

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

+ 34
- 22
IPA.Loader/Logging/Printers/GZFilePrinter.cs View File

@ -1,5 +1,7 @@
using Ionic.Zlib;
#nullable enable
using Ionic.Zlib;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@ -29,17 +31,17 @@ namespace IPA.Logging.Printers
private const RegexOptions reOptions = RegexOptions.None;
#endif
internal static Regex removeControlCodes = new Regex("\x1b\\[\\d+m", reOptions);
internal static Regex removeControlCodes = new("\x1b\\[\\d+m", reOptions);
private FileInfo fileInfo;
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;
protected StreamWriter? FileWriter;
private FileStream fstream;
private FileStream? fstream;
/// <summary>
/// Gets the <see cref="FileInfo"/> for the file to write to.
@ -49,6 +51,7 @@ namespace IPA.Logging.Printers
private const string latestFormat = "_latest{0}";
[MemberNotNull(nameof(fileInfo))]
private void InitLog()
{
try
@ -90,21 +93,30 @@ namespace IPA.Logging.Printers
{
Logger.Default.Error("Error initializing log!");
Logger.Default.Error(e);
throw;
}
}
private static async void CompressOldLog(FileInfo file)
{
Logger.Default.Debug($"Compressing log file {file}");
try
{
Logger.Default.Debug($"Compressing log file {file}");
var newFile = new FileInfo(file.FullName + ".gz");
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);
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();
file.Delete();
}
catch (Exception e)
{
Logger.Default.Error("Error compressing old log file:");
Logger.Default.Error(e);
}
}
/// <summary>
@ -123,10 +135,10 @@ namespace IPA.Logging.Printers
/// </summary>
public sealed override void EndPrint()
{
FileWriter.Flush();
fstream.Flush();
FileWriter.Dispose();
fstream.Dispose();
FileWriter?.Flush();
fstream?.Flush();
FileWriter?.Dispose();
fstream?.Dispose();
FileWriter = null;
fstream = null;
}
@ -146,12 +158,12 @@ namespace IPA.Logging.Printers
{
if (disposing)
{
FileWriter.Flush();
fstream.Flush();
FileWriter.Close();
fstream.Close();
FileWriter.Dispose();
fstream.Dispose();
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()
};
}
}
}

+ 1
- 1
IPA.Loader/Logging/StdoutInterceptor.cs View File

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


+ 1
- 1
IPA.Loader/Utilities/AlmostVersion.cs View File

@ -190,7 +190,7 @@ namespace IPA.Utilities
/// <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)
public override bool Equals(object? obj)
{
return obj is AlmostVersion version &&
SemverValue == version.SemverValue &&


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


+ 8
- 11
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
@ -21,9 +18,9 @@ namespace IPA.Utilities
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


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


+ 73
- 9
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,12 +20,12 @@ 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)
{
@ -76,24 +79,30 @@ namespace IPA.Utilities
}
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);
}
}
}

+ 1
- 1
IPA/Program.cs View File

@ -21,7 +21,7 @@ namespace IPA
Unknown
}
public const string FileVersion = "4.2.2.0";
public const string FileVersion = "4.3.0.0";
public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!;


+ 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.Net4.dll View File


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


+ 13
- 8
docs/articles/command-line.md View File

@ -26,6 +26,16 @@ Here's a quick list of what they are and what they do.
> Makes a console appear with log information at startup.
>
> Optionally, an explicit process ID can be specified to start the game with an external console. This allows it to be
> launched via Steam without being interrupted by its "Allow game launch?" if launched directly from the `.exe`.
>
> Example for Beat Saber using PowerShell:
>
> ```
> .\steam.exe -applaunch 620980 --verbose $PID
> ```
>
> Do note that this isn't going to work from an elevated terminal.
- `--debug`
@ -35,15 +45,13 @@ Here's a quick list of what they are and what they do.
> This option also forces BSIPA to show all debug messages in the console, as well as where they were called.
>
> This overrides the config settings `Debug.ShowDebug` and `Debug.ShowCallSource`.
>
- `--trace`
> Enables trace level messages. By default, they do not ever enter the message queue, and thus cost almost nothing.
> When this or the config option is used, they are added and logged with the same rules as Debug messages.
>
> This overrides the config setting `Debug.ShowTrace`.
>
- `--mono-debug`
@ -53,7 +61,6 @@ Here's a quick list of what they are and what they do.
> debugger server running on port 10000 on `localhost`.
>
> Implies `--debug`.
>
- `--server`
@ -62,7 +69,6 @@ Here's a quick list of what they are and what they do.
> When paired with `--mono-debug`, this option makes the Mono soft debugger act in server mode. It begins listening on
> port 10000 on any address, and will pause startup (with no window) until a debugger is connected. I recommend using
> SDB, but that is a command line debugger and a lot of people don't care for those.
>
- `--no-yeet`
@ -73,7 +79,6 @@ Here's a quick list of what they are and what they do.
> behaviour is disabled.
>
> Overrides the config setting `YeetMods`.
>
- `--condense-logs`
@ -85,11 +90,11 @@ Here's a quick list of what they are and what they do.
> Overrides the config setting `Debug.CondenseModLogs`.
- `--plugin-logs`
> Causes each plugins' log messages to be written to files in their own folder for ease of debugging.
>
> This was the default through 4.1.6, however is now disabled by default.
>
> Overrides the config setting `Debug.CreateModLogs`.
***

+ 10
- 6
docs/articles/start-user.md View File

@ -77,11 +77,14 @@ uid: articles.start.user
Many plugins will come in a zip such that the root of the zip represents the game install directory, so all you may have to
do is extract the plugin into the game installation folder.
> [!NOTE]
> [!NOTE] For Linux users
>
> By default, WINE loads DLLs differently to Windows, causing issues with the injection. To make BSIPA's injection work
> with Wine, `winhttp` has to have a DLL override set to `native,builtin`. This is best achieved by putting
> `WINEDLLOVERRIDES="winhttp=native,builtin" %command%` in Beat Saber's launch options in Steam.
>
> For some reason, by default, Wine does not load DLLs in quite the same way that Windows does, causing issues with the injection.
> To make the injection work with Wine, `winhttp` has to have a DLL override set to `native,builtin`. This can be set either through
> Protontricks, or with the following `.reg` file.
> Alternatively, this can be set either through Protontricks, or using the following
> `.reg` file:
>
> ```reg
> REGEDIT4
@ -89,8 +92,9 @@ uid: articles.start.user
> "winhttp"="native,builtin"
> ```
>
> For Steam there's a per-game Wine prefix under `compatdata`. In this case `SteamLibrary/steamapps/compatdata/620980/pfx/user.reg`.
> Changes to this file will likely be ovewritten when the game updates or if local files are validated through Steam.
> For Steam, each game's Wine prefix is located under `compatdata`; in Beat Saber's case `SteamLibrary/steamapps/compatdata/620980/pfx/user.reg`.
> Changes to this file might be ovewritten when the game or Proton are updated however, so the launch options method is
> recommended.
Thats really all you have to do! The installation should persist across game updates for as long as `winhttp.dll` is present in
the game directory, though your plugins will be moved to a different folder when it does update so things don't break horribly.


+ 2
- 2
docs/build.ps1 View File

@ -7,8 +7,8 @@ if ($PSEdition -eq "Core") {
# read SelfConfig, remove wierd bits, load it, load Newtonsoft, and turn it into a schema
$newtonsoftVer = "12.0.2"
$newtonsoftSchemaVer = "3.0.11"
$newtonsoftVer = "13.0.1"
$newtonsoftSchemaVer = "3.0.15-beta2"
$codeDomProviderVer = "3.6.0"
$roslynVer = "3.10.0"
$nugetBase = "$(Get-Location)/nuget"


+ 1
- 1
docs/docfx.json View File

@ -20,7 +20,7 @@
"disableGitFeatures": false,
"disableDefaultFilter": false,
"properties": {
"TargetFramework": "net461"
"TargetFramework": "net472"
}
}],
"build": {


Loading…
Cancel
Save